- 原文地址: introduction
- 原文做者: Addy Osmani
- 譯文地址: 介紹
- 譯者: 閆萌
- 校對者: 周文康、楊建
現代 web 應用常用打包工具來建立生產環境的「打包」文件(腳本、樣式等等),這些文件通過優化和壓縮以後可以極快的被用戶下載。在使用 webpack 進行 web 性能優化系列文章中,咱們將介紹如何使用 webpack 高效的優化站點資源。這將會幫助用戶更快的加載網站以及交互。javascript
webpack 是當下最流行的打包工具之一。咱們能夠利用其特性來優化代碼,經過代碼拆分能夠將腳本拆分爲核心和非核心部分,而且去除無用的代碼(這僅僅是一小部分的優化案例),從而確保你的應用具備最小的網絡負擔和處理成本。php
受 Susie Lu 的在 Bundle Buddy 中進行代碼拆分的啓發。css
⭐️ 注意: 咱們建立了一個可供練習的應用來演示這篇文章中講到的內容。請充分利用它來練習這些技巧:
webpack-training-project
html
讓咱們從現今應用中最耗費資源之一的 JavaScript 開始優化。前端
- 原文地址: decrease frontend size
- 原文做者: Ivan Akulov
- 譯文地址: 減少前端大小
- 譯者: 楊建
- 校對者: 泥坤、周文康
當你正在優化一個應用時,第一件事就是儘量地減小它的大小。這裏介紹如何利用 webpack 來實現。vue
webpack 4 引入了新的 mode
標誌。你能夠將這個標誌設置爲 'development'
或者 'production'
來告訴 webpack 你正在爲特定環境構建應用:java
// webpack.config.js
module.exports = {
mode: 'production',
};
複製代碼
當構建生產環境的應用時,請確保你開啓了 production
模式。 這將讓 webpack 開啓它的優化項,好比:縮小尺寸、移除庫中只在開發者模式纔有的代碼等等。node
⭐️ 注意: 這些大部分只適用於 webpack 3。若是你在 webpack 4 中開啓了 production 模式,bundle-level 最小化已經啓用 – 你只須要啓用 loader 特定(loader-specific)的選項。react
最小化尺寸是在你經過移除多餘的空格、縮短變量的命名等方式壓縮代碼的時候進行的。例如這樣:webpack
// 原來的代碼
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;
}
複製代碼
↓
// 最小化後的代碼
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 支持兩種方式最小化代碼:bundle-level 最小化和 loader 特定的選項。它們應該同時使用。
當編譯完成後,bundle-level 最小化功能會壓縮整個 bundle。這裏展現了它是如何工做的:
你的代碼是這樣的:
// 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!');
}
複製代碼
minifier 大體會壓縮成下面那樣:
// 最小化過的 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 中, bundle-level 最小化功能是自動開啓的 – 不管是否在生產模式。它在底層使用的是 UglifyJS 最小化。(若是你須要禁用最小化,只要使用開發模式或者將 optimization.minimize
選項設置爲false
。)
在 webpack 3 中, 你須要直接使用 UglifyJS 插件。這個插件是 webpack 自帶的;將它添加到配置的 plugins
部分便可啓用:
// webpack.config.js
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.optimize.UglifyJsPlugin(),
],
};
複製代碼
⭐️ 注意: 在 webpack 3 中,UglifyJS 插件不能編譯版本超過 ES2015 (即 ES6) 的代碼。這意味着若是你的代碼使用了類、箭頭函數或者其它新的語言特性,你不能將它們編譯成 ES5 版本的代碼, 不然插件將拋出一個錯誤。
若是你須要編譯包含新的語法(的代碼),使用 uglifyjs-webpack-plugin 插件。 這一樣是 webpack 自帶的插件,可是版本更新,而且能夠編譯 ES2015+ 的代碼。
最小化代碼的第二種方法是 loader 特定的選項(loader 是什麼)。利用 loader 選項,你能夠壓縮 minifier 不能最小化的東西。例如,當你利用 css-loader
導入一個 CSS 文件時,該文件會被編譯成一個字符串:
/* comments.css */
.comment {
color: black;
}
複製代碼
↓
// 最小化後的 bundle.js (部分代碼)
exports=module.exports=__webpack_require__(1)(),
exports.push([module.i,".comment {\r\n color: black;\r\n}",""]);
複製代碼
Minifier 不能壓縮該代碼,由於它是一個字符串。爲了最小化文件內容,咱們須要像這樣配置 loader:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
{ loader: 'css-loader', options: { minimize: true } },
],
},
],
},
};
複製代碼
⭐️ 注意: 這隻適用於 webpack 3。若是你在 production 模式下使用 webpack 4,
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 (基於 webpack 4)
module.exports = {
optimization: {
nodeEnv: 'production',
minimize: true,
},
};
複製代碼
在 webpack 3 中, 則使用 DefinePlugin
來替代:
// webpack.config.js (基於 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.');
}
複製代碼
而後 minifier 將會移除全部像 if
這樣的分支 – 由於 "production" !== 'production'
老是錯誤的,插件明白這些分支中的代碼永遠不會執行:
// 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 模塊, webpack 就能夠進行 tree-shaking。Tree-shaking 是當 bundler 遍歷整個依賴樹時,檢查使用了什麼依賴,並移除無用的。因此,若是你使用了 ES 模塊語法, webpack 能夠去掉未使用的代碼:
你寫了一個帶有多個 export 的文件,可是應用只使用它們其中的一個:
// comments.js
export const render = () => { return 'Rendered!'; };
export const commentRestEndpoint = '/rest/comments';
// index.js
import { render } from './comments.js';
render();
複製代碼
webpack 明白 commentRestEndpoint
沒有用到而且不會在 bundle 中生成單獨的 export:
// bundle.js (和 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 */
})
複製代碼
minifier 移除未使用的變量:
// bundle.js (part that corresponds to comments.js)
(function(n,e){"use strict";var r=function(){return"Rendered!"};e.b=r})
複製代碼
即便是對用 ES 模塊寫成的庫也是有效的。
⭐️ 注意: 在 webpack 中,tree-shaking 沒有 minifier 是不會起做用的。Webpack 僅僅移除沒有被用到的 export 變量;是 minifier 移除未使用的代碼的。因此,若是你在沒有使用 minifier 的狀況下編譯 bundle,是不會減少的。
然而,你不須要特定使用 webpack 內置的 minifier (
UglifyJsPlugin
)。任意的 minifier 都支持移除無用代碼(例如 Babel Minify plugin 或 Google Closure Compiler plugin) 均可以奏效。
❗ 警告: 不要將 ES 模塊編譯爲 CommonJS 模塊。
若是你使用 Babel 的
babel-preset-env
或babel-preset-es2015
, 檢查它們預先的設置。默認狀況下, 它們將 ES 的import
和export
轉譯爲 CommonJS 的require
和module.exports
。經過{ modules: false }
選項來禁用它。
與 TypeScript 相同:記得在你的
tsconfig.json
中設置{ "compilerOptions": { "module": "es2015" } }
。
圖片佔頁面大小的一半以上。 儘管它們不如 JavaScript 關鍵(例如,它們不會阻塞渲染),但仍然消耗了帶寬的一大部分。能夠在 webpack 中使用 url-loader
、svg-url-loader
和 image-webpack-loader
來優化它們。
url-loader
將小的靜態文件內聯進應用。沒有配置的話,它須要經過傳遞文件,將它放在編譯後的打包 bundle 內並返回一個這個文件的 url。然而,若是咱們指定了 limit
選項,它會將文件編碼成比無配置更小的 Base64 的數據 url 並將該 url 返回。這樣能夠將圖片內聯進 JavaScript 代碼中,並節省一次 HTTP 請求:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g|png|gif)$/,
loader: 'url-loader',
options: {
// 小於 10kB(10240字節)的內聯文件
limit: 10 * 1024,
},
},
],
}
};
複製代碼
// index.js
import imageUrl from './image.png';
// → 若是圖片小於 10kB, `imageUrl` 將包含
// 編碼後的圖片: 'data:image/png;base64,iVBORw0KGg…'
// → 若是圖片大於 10B,該 loader 將建立一個新文件,
// 而且 `imageUrl` 將會包含它的 url: `/2fcd56a1920be.png`
複製代碼
⭐️ 注意: 內聯圖片減小了單獨請求的數量,這是好的(即便經過 HTTP/2),可是增長了 bundle 和內存消耗的下載/解析時間。確保不要嵌入大的或者不少的圖片,不然增長的 bundle 時間可能超過內聯帶來的好處。
svg-url-loader
的工做原理相似於 url-loader
– 除了它利用 URL encoding 而不是 Base64 對文件編碼。對於 SVG 圖片這是有效的 – 由於 SVG 文件剛好是純文本,這種編碼規模效應更加明顯:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.svg$/,
loader: 'svg-url-loader',
options: {
// 小於 10kB(10240字節)的內聯文件
limit: 10 * 1024,
// 移除 url 中的引號
// (在大多數狀況下它們都不是必要的)
noquotes: true,
},
},
],
},
};
複製代碼
⭐️ 注意: svg-url-loader 擁有改善 IE 瀏覽器支持的選項,可是在其餘瀏覽器中更糟糕。若是你須要兼容 IE 瀏覽器,設置
iesafe: true
選項。
image-webpack-loader
會壓縮檢查到的全部圖片。它支持 JPG、PNG、GIF 和 SVG 格式的圖片,所以咱們在碰到全部這些類型的圖片都會使用它。
這個 loader 不能將圖片嵌入應用,因此它必須和 url-loader
以及 svg-url-loader
一塊兒使用。爲了不同時將它複製粘貼到兩個規則中(一個針對 JPG/PNG/GIF 圖片, 另外一個針對 SVG ),咱們使用 enforce: 'pre'
做爲單獨的規則涵蓋在這個 loader:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g|png|gif|svg)$/,
loader: 'image-webpack-loader',
// 這會應用該 loader,在其它以前
enforce: 'pre',
},
],
},
};
複製代碼
加載器的默認設置已經很好了 - 可是若是你想更進一步去配置它,參考插件選項。要選擇指定選項,請查看 Addy Osmani 的圖像優化指南。
平均一半以上的 Javascript 體積大小來源於依賴包,而且這其中的一部分可能都不是必要的。
例如,Lodash (自 v4.17.4 版本起) 增長了 72KB 的最小化代碼到 bundle 中。可是若是你僅僅用到它的 20 種方法,那麼大約 65KB 的代碼是無用的。
另外一個例子是 Moment.js。2.19.1 版本有 223KB 大小,這是巨大的 - 截至 2017 年 10 月,一個頁面的 JavaScript 平均體積是 452 KB。然而,其中的 170KB 是本地化文件。若是你沒有用到多語言版 Moment.js,這些文件都將毫無目的地使 bundle 更臃腫。
全部的這些依賴均可以輕易地優化。咱們已經在 GitHub 倉庫中收集了優化方法 - 來看一下!
⭐️ 注意: 若是在生產模式下使用 webpack 4,模塊串聯已經啓用。自由地跳過該部分。
當你構建 bundle 時,webpack 將每一個 module 包裝進一個函數中:
// 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 module,它們能夠在不將每一個模塊都封裝進函數中的狀況下進行打包。而且 webpack 3 使這樣的捆綁變得可能 - 經過模塊鏈接。這是模塊鏈接的工做原理:
// index.js
import {render} from './comments.js';
render();
// comments.js
export function render(data, target) {
console.log('Rendered!');
}
複製代碼
↓
// 與前面的代碼段不一樣,此包只有一個模塊
// 它包含來自兩個文件的代碼
// bundle.js (部分; 經過 ModuleConcatenationPlugin 編譯)
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
// 級聯模塊: ./comments.js
function render(data, target) {
console.log('Rendered!');
}
// 級聯模塊: ./index.js
render();
})
複製代碼
看到不一樣了嗎?在普通綁定中,模塊 0 須要模塊 1 的 render
。使用模塊鏈接,require
只需用所須要的功能替換,模塊 1 就被移除了。bundle 擁有更小的模塊 – 以及更少的模塊開支!
要在 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(),
],
};
複製代碼
⭐️ 注意: 想知道爲何默認不啓用這個行爲嗎?鏈接模塊是很棒, 可是它增長了構建時間並破壞了模塊熱替換。這是爲何它只在生產下啓用。
externals
,若是同時包含 webpack 和非 webpack 代碼你可能有一個大的項目,其中有些代碼是用 webpack 編譯的,有些不是。相似於視頻託管網站,播放器小部件多是 webpack 構建的,而周圍的頁面不是:
(徹底隨機的視頻託管網站)
若是代碼塊有公共的依賴,你能夠共享它們以免屢次下載其代碼。這是經過 webpack 的 externals
選項完成的 – 它經過變量或其它的額外導入來替換模塊。
window
中可得到若是你的 non-webpack 代碼依賴於某些依賴,這些依賴在 window
中能夠做爲變量得到,將依賴名別名爲變量名:
// webpack.config.js
module.exports = {
externals: {
'react': 'React',
'react-dom': 'ReactDOM',
},
};
複製代碼
經過這個配置, webpack 不會打包 react
和 react-dom
包。相反,它們將被替換成下面這樣的東西:
// bundle.js (part of)
(function(module, exports) {
// 導出 `window.React` 的模塊。 沒有 `externals`,
// 這個模塊會包含整個的 React 包
module.exports = React;
}),
(function(module, exports) {
// 導出 `window.React` 的模塊。 沒有 `externals`,
// 這個模塊會包含整個的 ReactDOM 包
module.exports = ReactDOM;
})
複製代碼
若是你的 non-webpack 代碼沒有將依賴暴露於 window
,事情就變得更加複雜。然而,若是這些非 webpack 代碼將這些依賴做爲 AMD 包,你仍然能夠避免相同的代碼加載兩次。
具體如何作呢,將 webpack 代碼編譯成一個 AMD bundle ,同時將模塊別名爲庫的 URLs:
// webpack.config.js
module.exports = {
output: { libraryTarget: 'amd' },
externals: {
'react': { amd: '/libraries/react.min.js' },
'react-dom': { amd: '/libraries/react-dom.min.js' },
},
};
複製代碼
webpack 將把 bundle 包裝進 define()
並讓其依賴於這些 URLs:
// bundle.js (開始)
define(["/libraries/react.min.js", "/libraries/react-dom.min.js"], function () { … });
複製代碼
若是 non-webpack 代碼使用了相同的 URLs 來加載它的依賴,那麼這些文件只會加載一次 - 額外的請求將使用加載器緩存。
⭐️ 注意: Webpack 僅替換那些明確匹配
externals
對象的鍵的導入。這意味着若是你編寫import React from 'react/umd/react.production.min.js'
,這個庫不會從 bundle 中排除。這是合理的 - webpack 不知道import 'react'
和import 'react/umd/react.production.min.js'
是不是同一個東西 - 因此保持當心。
externals
NODE_ENV
替換成 production
externals
,若是這對你有效果