如何使用 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 } },
],
},
],
},
};
其他資訊
- UglifyJsPlugin 文件
- 其他熱門壓縮程式:Babel Miniify、Google 閉包編譯器
指定「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 }; }
其他資訊
- 什麼是「環境變數」
- 關於
DefinePlugin
、EnvironmentPlugin
的 Webpack 文件
使用 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.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 不會封裝 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