如何使用 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 能夠進行樹軸作業。樹狀結構觀察是指組合器掃遍整個依附元件樹狀結構、檢查使用的依附元件,以及移除未使用的依附元件時。因此,如果您使用 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 (自 4.17.4 版起) 會在套件中加入 72 KB 的壓縮程式碼。但如果您只使用其中 20 個方法,那麼約 65 KB 的經過精簡的程式碼就會完全沒有作用。
另一個例子是 Moment.js2.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 模組不同,您可以封裝,不必為每個模組納入函式。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 程式碼使用相同的網址載入其依附元件,則這些檔案只會載入一次,其他要求則會使用載入器快取。
延伸閱讀
externals
上的 Webpack 說明文件
總結
- 如果使用 webpack 4,請啟用正式版模式
- 使用套件層級的縮減器和載入器選項,縮減程式碼
- 將
NODE_ENV
替換為production
,移除開發專用程式碼 - 使用 ES 模組啟用樹軸
- 壓縮圖片
- 套用特定依附元件的最佳化功能
- 啟用模組連接
- 如有需要,請使用
externals