如何使用 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 支援兩種程式碼壓縮方式:套件層級壓縮和載入器專屬選項。並同時使用這兩項工具。
套件層級壓縮
套件層級的縮減功能會在編譯後壓縮整個套件。運作方式如下:
您可以編寫類似以下的程式碼:
// comments.js import './comments.css'; export function render(data, target) { console.log('Rendered!'); }
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!'); }
而壓縮器會將其壓縮成大致如下的內容:
// 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
替換為指定的值。使用上述設定:
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.'); }
接著,縮減器會移除所有這類
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 }; }
延伸閱讀
- 「環境變數」是什麼
- Webpack 說明文件:
DefinePlugin
、EnvironmentPlugin
使用 ES 模組
另一種縮減前端大小的方法是使用 ES 模組。
使用 ES 模組時,Webpack 就能執行樹狀圖搖晃。樹狀圖搖動是指 bundler 會遍歷整個依附元件樹狀圖,檢查所使用的依附元件,並移除未使用的依附元件。因此,如果您使用 ES 模組語法,Webpack 就能刪除未使用的程式碼:
您寫入含有多個匯出項目的檔案,但應用程式只使用其中一個:
// comments.js export const render = () => { return 'Rendered!'; }; export const commentRestEndpoint = '/rest/comments'; // index.js import { render } from './comments.js'; render();
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 */ })
縮減器會移除未使用的變數:
// 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 外掛程式) 都能解決這個問題。
延伸閱讀
Webpack 說明文件:關於搖樹
最佳化圖片
圖片佔頁面大小的超過一半。雖然這些元素不像 JavaScript 那麼重要 (例如不會阻斷轉譯),但仍會佔用大量頻寬。使用 url-loader
、svg-url-loader
和 image-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-loader
和 svg-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 的優質圖片最佳化指南。
延伸閱讀
- 「Base64 編碼用途為何?」
- 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 文件關於 ModuleConcatenationPlugin
- 「簡介範圍提升」
- 這個外掛程式的用途的詳細說明
如果您同時使用 webpack 和非 webpack 程式碼,請使用 externals
您可能有一個大型專案,其中有些程式碼是使用 webpack 編譯,有些則不是。例如影片代管網站,其中的播放器小工具可能會使用 webpack 建構,而周圍的網頁可能不會:
如果兩個程式碼都有共同的依附元件,您可以共用這些依附元件,避免多次下載程式碼。這項操作是透過 Webpack 的 externals
選項完成,該選項會將模組替換為變數或其他外部匯入項目。
如果 window
中提供依附元件
如果非 webpack 程式碼依附於 window
中可用做為變數的依附元件,請將依附元件名稱別名為變數名稱:
// webpack.config.js
module.exports = {
externals: {
'react': 'React',
'react-dom': 'ReactDOM'
}
};
使用這項設定後,webpack 就不會將 react
和 react-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 說明文件在
externals
總結
- 如果您使用 webpack 4,請啟用正式版模式
- 使用套件層級的縮減器和載入器選項,縮減程式碼
- 將
NODE_ENV
替換為production
,移除僅限開發的程式碼 - 使用 ES 模組啟用樹狀圖搖動
- 壓縮圖片
- 套用依附元件專屬的最佳化
- 啟用模組連接
- 如有需要,請使用
externals