縮小前端大小

如何使用 webpack 盡可能縮小應用程式

在最佳化應用程式時,第一件要做的事就是盡可能縮小應用程式大小。以下說明如何使用 webpack 執行這項操作。

Webpack 4 推出了新的 mode 標記。您可以將這個標記設為 'development''production',向 webpack 提示您要為特定環境建構應用程式:

// webpack.config.js
module
.exports = {
  mode
: 'production',
};

建構正式版應用程式時,請務必啟用 production 模式。這樣一來,Webpack 就會套用最佳化功能,例如縮減程式碼、移除程式庫中的開發專用程式碼等

延伸閱讀

啟用精簡功能

壓縮是指移除多餘空格、縮短變數名稱等方式來壓縮程式碼。如下所示:

// Original code
function map(array, iteratee) {
  let index
= -1;
 
const length = array == null ? 0 : array.length;
 
const result = new Array(length);

 
while (++index < length) {
    result
[index] = iteratee(array[index], index, array);
 
}
 
return result;
}

↓。

// Minified code
function map(n,r){let t=-1;for(const a=null==n?0:n.length,l=Array(a);++t<a;)l[t]=r(n[t],t,n);return l}

Webpack 支援兩種程式碼壓縮方式:套件層級壓縮載入器專屬選項。並同時使用這兩項工具。

套件層級壓縮

套件層級的縮減功能會在編譯後壓縮整個套件。運作方式如下:

  1. 您可以編寫類似以下的程式碼:

    // comments.js
    import './comments.css';
    export function render(data, target) {
      console
    .log('Rendered!');
    }
  2. Webpack 會將其編譯成大致如下內容:

    // bundle.js (part of)
    "use strict";
    Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
    /* harmony export (immutable) */ __webpack_exports__["render"] = render;
    /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__comments_css__ = __webpack_require__(1);
    /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__comments_css_js___default =
    __webpack_require__
    .n(__WEBPACK_IMPORTED_MODULE_0__comments_css__);

    function render(data, target) {
    console
    .log('Rendered!');
    }
  3. 而壓縮器會將其壓縮成大致如下的內容:

    // minified bundle.js (part of)
    "use strict";function t(e,n){console.log("Rendered!")}
    Object.defineProperty(n,"__esModule",{value:!0}),n.render=t;var o=r(1);r.n(o)

在 webpack 4 中,系統會自動啟用套件層級的縮減功能,無論是否處於正式版模式皆是如此。在幕後,它會使用 UglifyJS 縮減器。(如果您需要停用精簡功能,只要使用開發人員模式或將 false 傳遞至 optimization.minimize 選項即可)。

在 webpack 3 中,您需要直接使用 UglifyJS 外掛程式。此外掛程式與 webpack 一併提供,如要啟用,請將其新增至設定的 plugins 區段:

// webpack.config.js
const webpack = require('webpack');

module
.exports = {
  plugins
: [
   
new webpack.optimize.UglifyJsPlugin(),
 
],
};

載入器專屬選項

縮減程式的第二種方法是使用載入器專屬選項 (載入器是什麼)。您可以使用載入器選項,壓縮縮減器無法縮減的內容。舉例來說,如果您使用 css-loader 匯入 CSS 檔案,檔案會編譯為字串:

/* comments.css */
.comment {
 
color: black;
}
// minified bundle.js (part of)
exports
=module.exports=__webpack_require__(1)(),
exports
.push([module.i,".comment {\r\n  color: black;\r\n}",""]);

由於這是字串,因此壓縮器無法壓縮這段程式碼。如要縮減檔案內容,我們需要設定載入器來執行這項操作:

// webpack.config.js
module
.exports = {
  module
: {
    rules
: [
     
{
        test
: /\.css$/,
        use
: [
         
'style-loader',
         
{ loader: 'css-loader', options: { minimize: true } },
       
],
     
},
   
],
 
},
};

延伸閱讀

指定 NODE_ENV=production

另一種減少前端大小的方法,是將程式碼中的 NODE_ENV 環境變數設為 production 值。

程式庫會讀取 NODE_ENV 變數,以偵測應在哪種模式下運作,也就是開發或正式版模式。部分程式庫的行為會根據這個變數而有所不同。舉例來說,如果 NODE_ENV 未設為 production,Vue.js 會進行額外檢查並顯示警告:

// vue/dist/vue.runtime.esm.js
// …
if (process.env.NODE_ENV !== 'production') {
  warn
('props must be strings when using array syntax.');
}
// …

React 的運作方式類似,會載入包含警告的開發人員版本:

// react/index.js
if (process.env.NODE_ENV === 'production') {
  module
.exports = require('./cjs/react.production.min.js');
} else {
  module
.exports = require('./cjs/react.development.js');
}

// react/cjs/react.development.js
// …
warning$3
(
    componentClass
.getDefaultProps.isReactClassApproved,
   
'getDefaultProps is only used on classic React.createClass ' +
   
'definitions. Use a static property named `defaultProps` instead.'
);
// …

這類檢查和警告通常在實際工作環境中並非必要,但會保留在程式碼中,並增加程式庫大小。在 webpack 4 中,請新增 optimization.nodeEnv: 'production' 選項來移除這些項目:

// webpack.config.js (for webpack 4)
module
.exports = {
  optimization
: {
    nodeEnv
: 'production',
    minimize
: true,
 
},
};

在 webpack 3 中,請改用 DefinePlugin

// webpack.config.js (for webpack 3)
const webpack = require('webpack');

module
.exports = {
  plugins
: [
   
new webpack.DefinePlugin({
     
'process.env.NODE_ENV': '"production"'
   
}),
   
new webpack.optimize.UglifyJsPlugin()
 
]
};

optimization.nodeEnv 選項和 DefinePlugin 的運作方式相同,兩者都會將所有 process.env.NODE_ENV 替換為指定的值。使用上述設定:

  1. Webpack 會將所有出現的 process.env.NODE_ENV 替換為 "production"

    // vue/dist/vue.runtime.esm.js
    if (typeof val === 'string') {
      name
    = camelize(val);
      res
    [name] = { type: null };
    } else if (process.env.NODE_ENV !== 'production') {
      warn
    ('props must be strings when using array syntax.');
    }

    ↓。

    // vue/dist/vue.runtime.esm.js
    if (typeof val === 'string') {
      name
    = camelize(val);
      res
    [name] = { type: null };
    } else if ("production" !== 'production') {
      warn
    ('props must be strings when using array syntax.');
    }
  2. 接著,縮減器會移除所有這類 if 分支,因為 "production" !== 'production' 一律為 false,且外掛程式會瞭解這些分支中的程式碼永遠不會執行:

    // vue/dist/vue.runtime.esm.js
    if (typeof val === 'string') {
      name
    = camelize(val);
      res
    [name] = { type: null };
    } else if ("production" !== 'production') {
      warn
    ('props must be strings when using array syntax.');
    }

    ↓。

    // vue/dist/vue.runtime.esm.js (without minification)
    if (typeof val === 'string') {
      name
    = camelize(val);
      res
    [name] = { type: null };
    }

延伸閱讀

使用 ES 模組

另一種縮減前端大小的方法是使用 ES 模組

使用 ES 模組時,Webpack 就能執行樹狀圖搖晃。樹狀圖搖動是指 bundler 會遍歷整個依附元件樹狀圖,檢查所使用的依附元件,並移除未使用的依附元件。因此,如果您使用 ES 模組語法,Webpack 就能刪除未使用的程式碼:

  1. 您寫入含有多個匯出項目的檔案,但應用程式只使用其中一個:

    // comments.js
    export const render = () => { return 'Rendered!'; };
    export const commentRestEndpoint = '/rest/comments';

    // index.js
    import { render } from './comments.js';
    render
    ();
  2. Webpack 會瞭解未使用 commentRestEndpoint,因此不會在套件中產生個別匯出點:

    // bundle.js (part that corresponds to comments.js)
    (function(module, __webpack_exports__, __webpack_require__) {
    "use strict";
    const render = () => { return 'Rendered!'; };
    /* harmony export (immutable) */ __webpack_exports__["a"] = render;

    const commentRestEndpoint = '/rest/comments';
    /* unused harmony export commentRestEndpoint */
    })
  3. 縮減器會移除未使用的變數:

    // bundle.js (part that corresponds to comments.js)
    (function(n,e){"use strict";var r=function(){return"Rendered!"};e.b=r})

即使程式庫是使用 ES 模組編寫,也能使用這項功能。

不過,您不必使用 webpack 內建的縮減器 (UglifyJsPlugin)。任何支援移除無效程式碼的縮減程式 (例如 Babel Minify 外掛程式Google Closure Compiler 外掛程式) 都能解決這個問題。

延伸閱讀

最佳化圖片

圖片佔頁面大小的超過一半。雖然這些元素不像 JavaScript 那麼重要 (例如不會阻斷轉譯),但仍會佔用大量頻寬。使用 url-loadersvg-url-loaderimage-webpack-loader,在 webpack 中進行最佳化。

url-loader 會將小型靜態檔案內嵌至應用程式。如果沒有設定,它會取得傳遞的檔案,將檔案放在已編譯的套件旁邊,並傳回該檔案的網址。不過,如果我們指定 limit 選項,系統會將小於此限制的檔案編碼為 Base64 資料網址,並傳回此網址。這會將圖片內嵌至 JavaScript 程式碼,並儲存 HTTP 要求:

// webpack.config.js
module
.exports = {
  module
: {
    rules
: [
     
{
        test
: /\.(jpe?g|png|gif)$/,
        loader
: 'url-loader',
        options
: {
         
// Inline files smaller than 10 kB (10240 bytes)
          limit
: 10 * 1024,
       
},
     
},
   
],
 
}
};
// index.js
import imageUrl from './image.png';
// → If image.png is smaller than 10 kB, `imageUrl` will include
// the encoded image: 'data:image/png;base64,iVBORw0KGg…'
// → If image.png is larger than 10 kB, the loader will create a new file,
// and `imageUrl` will include its url: `/2fcd56a1920be.png`

svg-url-loader 的運作方式與 url-loader 相同,唯一的差別是它會使用 網址編碼,而非 Base64 編碼來編碼檔案。這對 SVG 圖片很有幫助,因為 SVG 檔案只是純文字,因此這種編碼方式更節省空間。

module.exports = {
  module
: {
    rules
: [
     
{
        test
: /\.svg$/,
        loader
: "svg-url-loader",
        options
: {
          limit
: 10 * 1024,
          noquotes
: true
       
}
     
}
   
]
 
}
};

image-webpack-loader 會壓縮經過該函式的圖片。它支援 JPG、PNG、GIF 和 SVG 圖片,因此我們會將其用於所有這些類型的圖片。

這個載入器不會將圖片嵌入應用程式,因此必須搭配 url-loadersvg-url-loader 使用。為避免將其複製貼上至兩個規則 (一個用於 JPG/PNG/GIF 圖片,另一個用於 SVG 圖片),我們會使用 enforce: 'pre' 將此載入器納入為個別規則:

// webpack.config.js
module
.exports = {
  module
: {
    rules
: [
     
{
        test
: /\.(jpe?g|png|gif|svg)$/,
        loader
: 'image-webpack-loader',
       
// This will apply the loader before the other ones
        enforce
: 'pre'
     
}
   
]
 
}
};

載入器的預設設定已可使用,但如果您想進一步設定,請參閱外掛程式選項。如要選擇要指定的選項,請參閱 Addy Osmani 的優質圖片最佳化指南

延伸閱讀

最佳化依附元件

平均 JavaScript 大小的一半以上來自依附元件,而其中部分大小可能只是不必要的。

舉例來說,Lodash (截至 v4.17.4) 會將 72 KB 的經過精簡的程式碼新增至套件。但如果您只使用其中 20 個方法,則約 65 KB 的經過精簡的程式碼就會完全無用。

另一個例子是 Moment.js。其 2.19.1 版的壓縮程式碼大小為 223 KB,這實在太大了,因為網頁上的 JavaScript 平均大小在 2017 年 10 月為 452 KB。不過,其中 170 KB 是本地化檔案。如果您未使用多語言的 Moment.js,這些檔案會無故擴大套件。

所有這些依附元件都可以輕鬆最佳化。我們已在 GitHub 存放區中收集了最佳化方法,歡迎查看

為 ES 模組啟用模組串接功能 (又稱為範圍提升)

建構套件時,Webpack 會將每個模組包裝成函式:

// index.js
import {render} from './comments.js';
render
();

// comments.js
export function render(data, target) {
  console
.log('Rendered!');
}

↓。

// bundle.js (part  of)
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {
 
"use strict";
 
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
 
var __WEBPACK_IMPORTED_MODULE_0__comments_js__ = __webpack_require__(1);
 
Object(__WEBPACK_IMPORTED_MODULE_0__comments_js__["a" /* render */])();
}),
/* 1 */
(function(module, __webpack_exports__, __webpack_require__) {
 
"use strict";
  __webpack_exports__
["a"] = render;
 
function render(data, target) {
    console
.log('Rendered!');
 
}
})

過去,這項要求是為了隔離 CommonJS/AMD 模組。不過,這會為每個模組增加大小和效能負擔。

Webpack 2 推出了對 ES 模組的支援,與 CommonJS 和 AMD 模組不同,ES 模組可以進行捆綁,而不需要用函式包裝每個模組。而 webpack 3 則透過模組串連功能,實現了這類捆綁作業。以下是模組連接的運作方式:

// index.js
import {render} from './comments.js';
render
();

// comments.js
export function render(data, target) {
  console
.log('Rendered!');
}

↓。

// Unlike the previous snippet, this bundle has only one module
// which includes the code from both files

// bundle.js (part of; compiled with ModuleConcatenationPlugin)
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {
 
"use strict";
 
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });

 
// CONCATENATED MODULE: ./comments.js
   
function render(data, target) {
    console
.log('Rendered!');
 
}

 
// CONCATENATED MODULE: ./index.js
  render
();
})

有沒有發現差異?在一般套件中,模組 0 需要模組 1 的 render。使用模組串接功能後,require 會直接替換為必要函式,而模組 1 會遭到移除。套件中的模組較少,因此模組的額外負擔也較少!

如要啟用這項行為,請在 webpack 4 中啟用 optimization.concatenateModules 選項:

// webpack.config.js (for webpack 4)
module
.exports = {
  optimization
: {
    concatenateModules
: true
 
}
};

在 webpack 3 中,請使用 ModuleConcatenationPlugin

// webpack.config.js (for webpack 3)
const webpack = require('webpack');

module
.exports = {
  plugins
: [
   
new webpack.optimize.ModuleConcatenationPlugin()
 
]
};

延伸閱讀

如果您同時使用 webpack 和非 webpack 程式碼,請使用 externals

您可能有一個大型專案,其中有些程式碼是使用 webpack 編譯,有些則不是。例如影片代管網站,其中的播放器小工具可能會使用 webpack 建構,而周圍的網頁可能不會:

影片代管網站的螢幕截圖
(完全隨機的影片代管網站)

如果兩個程式碼都有共同的依附元件,您可以共用這些依附元件,避免多次下載程式碼。這項操作是透過 Webpack 的 externals 選項完成,該選項會將模組替換為變數或其他外部匯入項目。

如果 window 中提供依附元件

如果非 webpack 程式碼依附於 window 中可用做為變數的依附元件,請將依附元件名稱別名為變數名稱:

// webpack.config.js
module
.exports = {
  externals
: {
   
'react': 'React',
   
'react-dom': 'ReactDOM'
 
}
};

使用這項設定後,webpack 就不會將 reactreact-dom 套件打包。而是會替換成類似以下的內容:

// bundle.js (part of)
(function(module, exports) {
 
// A module that exports `window.React`. Without `externals`,
 
// this module would include the whole React bundle
  module
.exports = React;
}),
(function(module, exports) {
 
// A module that exports `window.ReactDOM`. Without `externals`,
 
// this module would include the whole ReactDOM bundle
  module
.exports = ReactDOM;
})

依附元件是否會載入為 AMD 套件

如果非 webpack 程式碼未將依附元件公開至 window,情況就會更複雜。不過,如果非 Webpack 程式碼以 AMD 套件的形式使用這些依附元件,您還是可以避免載入相同程式碼兩次。

如要這麼做,請將 webpack 程式碼編譯為 AMD 套件,並將模組別名為程式庫網址:

// webpack.config.js
module
.exports = {
  output
: {
    libraryTarget
: 'amd'
 
},
  externals
: {
   
'react': {
      amd
: '/libraries/react.min.js'
   
},
   
'react-dom': {
      amd
: '/libraries/react-dom.min.js'
   
}
 
}
};

Webpack 會將套件包裝成 define(),並使其依附於下列網址:

// bundle.js (beginning)
define
(["/libraries/react.min.js", "/libraries/react-dom.min.js"], function () { });

如果非 Webpack 程式碼使用相同的網址載入其依附元件,則這些檔案只會載入一次,其他要求則會使用載入器快取。

延伸閱讀

總結

  • 如果您使用 webpack 4,請啟用正式版模式
  • 使用套件層級的縮減器和載入器選項,縮減程式碼
  • NODE_ENV 替換為 production,移除僅限開發的程式碼
  • 使用 ES 模組啟用樹狀圖搖動
  • 壓縮圖片
  • 套用依附元件專屬的最佳化
  • 啟用模組連接
  • 如有需要,請使用 externals