縮小前端大小

如何使用 Webpack 調整應用程式大小

最佳化應用程式的第一步是盡可能縮小應用程式。以下說明如何使用 Webpack 來進行這項操作。

使用正式環境模式 (僅限 Webpack 4)

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 可以執行樹景。「樹狀結構搖動」是指組合器週遊整個依附元件樹狀結構、檢查已使用的依附元件,並移除未使用的依附元件。因此,如果您使用 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 (從 4.17.4 起) 會將 72 KB 的壓縮程式碼加入套件中。但如果只使用 20 種方法,則約 65 KB 經過壓縮的程式碼不會採取任何行動。

另一個範例是 Moment.js。它 2.19.1 版需要 223 KB 的程式碼壓縮檔,但規模龐大,2017 年 10 月網頁 JavaScript 的平均大小為 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 模組不同,可組合這些模組,而不用用函式包裝每個模組。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