利用 Webpack 來優化 Web 性能屬於加載性能優化 的一部分: ☛ Web Performance Optimization with webpackcss
production
模式
production
模式下 webpack 會對代碼進行優化,如減少代碼體積,刪除只在開發環境用到的代碼。html
能夠在 webpack 中指定:前端
module.exports = {
mode: 'production' // 或 development
};
複製代碼
或者 package.json 中配置:react
"scripts": {
"dev": "webpack-dev-server --mode development --open --hot",
"build": "webpack --mode production --progress"
}
複製代碼
使用 bundle-level minifier 和 loader options 壓縮代碼。webpack
Bundle-level 的壓縮會在代碼編譯後對整個包進行壓縮。git
在 webpack 4 中,production
模式下會自動執行 bundle-level 的壓縮,底層使用了 the UglifyJS minifier。(若是不想開啓壓縮,能夠採用 development
模式或者設置 optimization.minimize
爲 false)github
經過 loader 層面的選項配置來對代碼進行壓縮,是爲了壓縮 bundle-level minifier 沒法壓縮的內容,好比,經過 css-loader
編譯後的文件,會成爲字符串,就沒法被 minifier 壓縮。所以,要進一步壓縮文件內容,可進行以下配置:web
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
{ loader: 'css-loader', options: { minimize: true } },
],
},
],
},
};
複製代碼
當使用 ES 模塊時, webpack 可以進行 tree-shaking。npm
tree-shaking 是指 bundler 遍歷整個依賴關係樹,檢查使用了哪些依賴關係,並刪除未使用的依賴關係。所以,若是使用ES模塊語法,webpack 能夠消除未使用的代碼。json
★ 注意:在 webpack 中,若是沒有 minifier,tree-shaking 就沒法工做。webpack 只刪除不使用的導出語句,而 minifier 則會刪除未使用的代碼。所以,若是在編譯時不使用 minifier,代碼量並不會減少。(除了使用 wbpack 內置的 minifier,其它的插件如 Babel Minify plugin 也能對代碼進行壓縮)。
✘ 警告:不要意外地將 ES 模塊編譯成 CommonJS 模塊。若是你使用 Babel 的時候,採用了 babel-preset-env
或者 babel-preset-es2015
,請檢查這些預置的設置。默認狀況下,它們會將 ES 的導入和導出轉換爲 CommonJS 的 require
和 module.exports
,能夠經過傳遞 { modules: false }
選項來禁用它。
➹ Introduction to ES Modules ➹ 一口(很長的)氣了解 babel ➹ Webpack docs about tree shaking
針對具體的依賴項進行優化(dependency-specific optimization)
圖像佔了頁面大小的一半以上。雖然它們不像JavaScript那樣重要(例如,它們不會阻塞呈現),但它們仍然佔用了很大一部分帶寬。在 webpack 中可使用 url-loader
、svg-url-loader
和 image-webpack-loader
來優化它們。
url-loader
能夠將小型靜態文件內聯到應用程序中。若是不進行配置,它將把接受一個傳遞的文件,將其放在已編譯的包旁邊,並返回該文件的url。可是,若是指定 limit 選項,它將把小於這個限制的文件編碼爲Base64 數據的 url 並返回這個url,這會將圖像內聯到 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`
複製代碼
★ 注意:須要在增大代碼體積和減小 HTTP 請求數以前進行權衡。
svg-url-loader
的工做原理與 url-loader
相似 — 只是它使用的是URL編碼而不是Base64編碼來編碼文件。這對SVG圖像頗有用 — 由於SVG文件只是純文本,這種編碼更高效。
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.svg$/,
loader: 'svg-url-loader',
options: {
// Inline files smaller than 10 kB (10240 bytes)
limit: 10 * 1024,
// Remove the quotes from the url
// (they’re unnecessary in most cases)
noquotes: true,
},
},
],
},
};
複製代碼
★ 注意: svg-url-loader
有一些選項能夠改進Internet Explorer的支持,但會使其餘瀏覽器的內聯更加糟糕。若是須要支持此瀏覽器,請應用 iesafe: 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',
},
],
},
};
複製代碼
JavaScript 的大小平均有一半以上來自依賴項,而其中的一部分多是沒必要要的。咱們能夠對這些依賴的庫進行優化➡️ webpack-libs-optimizations。
好比:moment.js 刪除未使用的地區、react-router 移除未使用的模塊,生產環境去除 react propTypes 聲明等。
也叫作做用域提高(Scope Hoisting)
早期的時候,爲了隔離 CommonJS/AMD 模塊,webpack 在打包的時候,會把每一個模塊都打包到一個函數中,這樣就會增大每一個模塊的大小和性能開銷。webpack 2 的時候支持了 ES 模塊,而後 webpack 3 的時候使模塊鏈接成爲了可能。
【原理】:它會分析模塊間的依賴關係,儘量將被打散的模塊合併到一個函數中,但不能形成代碼冗餘,因此只有被引用一次的模塊才能被合併。因爲須要分析模塊間的依賴關係,因此源碼必須是採用了ES6模塊化的,不然Webpack會降級處理不採用Scope Hoisting。
開啓模塊鏈接以後,打出的包將會具備更少的模塊,以及更少的模塊開銷。若是在生產模式下使用 webpack 4,則模塊鏈接已經啓用。
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
concatenateModules: true,
},
};
複製代碼
★ 注意:爲何默認狀況下不啓用此行爲?鏈接模塊很酷,可是它增長了構建時間,並中斷了熱模塊替換。這就是爲何應該只在生產中啓用它。
externals
具體請參考:webpack-configuration-externals
緩存包(
bundle
),並經過更改包名稱(bundle name)來區分版本,將文件名替換成[name].[chunkname].js
[hash]
替換:能夠用於在文件名中包含一個構建相關(build-specific)的 hash; [chunkhash]
替換:在文件名中包含一個 chunk 相關(chunk-specific)的哈希,比[hash]
替換更好; [contenthash]
替換:會根據資源的內容添加一個惟一的 hash,當資源內容不變時,[contenthash]
就不會變。
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
- entry: './index.js',
+ entry: {
+ main: './index.js',
+ },
output: {
- filename: 'bundle.js',
+ filename: '[name].[contenthash].js', // / → bundle.8e0d62a03.js
path: path.resolve(__dirname, 'dist')
}
plugins: [
new HtmlWebpackPlugin({
- title: 'Output Management'
+ title: 'Caching'
})
],
};
複製代碼
➹ Hash vs chunkhash vs ContentHash
將
bundle
拆分紅程序代碼(app
)、第三方庫代碼(vendor
)和運行時代碼(runtime
)。
在 webpack 4 中添加如下的代碼,當第三方庫代碼大於 30 kb 時(未壓縮和未gzip前),webpack 可以自動提取 vendor
代碼,而且若是你在路由層面使用了代碼分割的話,它也可以提取公共代碼。
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
}
},
};
複製代碼
這樣,每次打包都會生成兩個文件:main.[chunkhash].js
和 vendors~main.[chunkhash].js
(for webpack 4). 在 webpack 4 中, 當第三方庫依賴很小的時候,vendor 包可能不會被生成,但也不要緊。
Webpack 在入口 chunk 中,包含了其運行時的引導代碼: runtime
,以及伴隨的 manifest 數據,runtime
是用來管理模塊交互的一小片斷代碼。當你將代碼分割成多個文件時,這段代碼包含了 chunk id 和模塊文件之間的映射,包括瀏覽器中的已加載模塊的鏈接,以及懶加載模塊的執行邏輯。
Webpack 會將這個運行時包含到最後生成的 chunk 中,即 vendor
。每次有任何塊發生變化時,這段代碼也會發生變化,致使 vendor
bundle 發生變化。
【解決方法】:設置 runtimeChunk
爲 true
來爲全部 chunks 建立一個單一的運行時包:
// webpack.config.js (for webpack 4)
module.exports = {
optimization: {
runtimeChunk: true,
},
};
複製代碼
webpack 運行時代碼很小,內聯它,能夠減小 HTTP 請求。
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const InlineSourcePlugin = require('html-webpack-inline-source-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
// Inline all files which names start with 「runtime~」 and end with 「.js」.
// That’s the default naming of runtime chunks
inlineSource: 'runtime~.+\\.js',
}),
// This plugin enables the 「inlineSource」 option
new InlineSourcePlugin(),
],
};
複製代碼
單頁應用中,使用
import
對不關鍵的代碼進行懶加載。
// videoPlayer.js
export function renderVideoPlayer() { … }
// comments.js
export function renderComments() { … }
// index.js
import {renderVideoPlayer} from './videoPlayer';
renderVideoPlayer();
// …Custom event listener
onShowCommentsClick(() => {
import('./comments').then((comments) => {
comments.renderComments();
});
});
複製代碼
import()
表示你想要動態加載特定模塊,當 webpack 看到 import('./module.js')
時,它會自動把該模塊從 chunk 中移除,只有在執行的時候纔會被下載。
這會使 main
模塊更小,可以減小初始加載時間,而且也能很好的提升緩存,若是你在 main chunk 中改了代碼,懶加載的模塊不會被影響。
按路由/頁面分割代碼(Code Splitting),以免加載沒必要要的內容。
單頁應用中,除了經過 import()
進行懶加載,還能夠經過框架層面的手段來進行。 React 應用懶加載——> Code Splitting(react-router) 或者 React.lazy(react doc)。
➹ WebpackGuides-Caching ➹ WebpackConcepts-The Manifest
使模塊標識符更穩定
在 webpack 構建時,每一個 module.id
會基於默認的解析順序(resolve order)進行增量,也就是說,當解析順序發生變化,ID 也會隨之改變。如:當新增一個模塊的時候,它可能會出如今模塊列表的中間,那麼它以後的模塊 ID 就會發生變化。
若是在業務代碼裏新引入一個模塊,則:
main
bundle 會隨着自身的新增內容的修改,而發生變化 ——> 符合預期vendor
bundle 會隨着自身的 module.id
的修改,而發生變化 ——> 【不符合預期】runtime
bundle 會由於當前包含一個新模塊的引用,而發生變化 ——> 符合預期+ const webpack = require('webpack');
module.exports = {
plugins: [
+ new webpack.HashedModuleIdsPlugin()
],
};
複製代碼
爲了解決這個問題,模塊 ID 經過 HashedModuleIdsPlugin
來進行計算,它會把基於數字增量的 ID 替換成模塊自身的 hash。這樣的話,一個模塊的 ID 只會在重命名或者移除的時候纔會改變,新模塊不會影響到它的 ID 變化。
[3IRH] ./index.js 29 kB {1} [built]
[DuR2] (webpack)/buildin/global.js 488 bytes {2} [built]
[JkW7] (webpack)/buildin/module.js 495 bytes {2} [built]
[LbCc] ./webPlayer.js 24 kB {1} [built]
[lebJ] ./comments.js 58 kB {0} [built]
[02Tr] ./ads.js 74 kB {1} [built]
+ 1 hidden module
複製代碼
在開發階段使用 webpack-dashboard 和 bundlesize 來調整應用程序的大小
webpack-dashboard 經過展現依賴項大小、進度和其餘細節來加強 webpack 輸出,有助於跟蹤大型依賴項。
npm install webpack-dashboard --save-dev
複製代碼
// webpack.config.js
const DashboardPlugin = require('webpack-dashboard/plugin');
module.exports = {
plugins: [
new DashboardPlugin(),
],
};
複製代碼
bundlesize 用於驗證 webpack 的資源不超過指定的大小,當應用程序變得太大時可以及時得知。
(1)運行打包命令 (2)開啓 bundlesize
npm install bundlesize --save-dev
複製代碼
(3)在 package.json
中指定文件大小限制
// package.json
{
"bundlesize": [
{
"path": "./dist/*.png",
"maxSize": "16 kB",
},
{
"path": "./dist/main.*.js",
"maxSize": "20 kB",
},
{
"path": "./dist/vendor.*.js",
"maxSize": "35 kB",
}
]
}
複製代碼
(4)執行 bundlesize
npx bundlesize
複製代碼
或者用 npm 執行:
// package.json
{
"scripts": {
"check-size": "bundlesize"
}
}
複製代碼
經過 webpack-bundle-analyzer 分析包的大小
webpack-bundle-analyzer 可以掃描 bundle 並對其內部內容進行可視化呈現,從而能夠發現大型的或者沒必要要的依賴項。
npm install webpack-bundle-analyzer --save-dev
複製代碼
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin(),
],
};
複製代碼
運行生產構建,該插件會在瀏覽器中打開可視化頁面。
默認狀況下,統計頁面顯示的是已解析文件的大小(當文件出如今包中時)。您可能想比較 gzip 以後的大小,由於它更接近實際用戶體驗,可使用左邊的邊欄來切換大小。
對於報告,咱們須要關注的點有:
大型依賴項:爲何這麼大?是否有更小的替代方案(例如,用 Preact 代替 React)?您是否使用了該庫包含的全部代碼(例如,Moment.js 包含了許多 常常不使用且可能被刪除的地區設置)?
重複的依賴關係:您是否看到同一個庫在多個文件中重複出現?(在 webpack 4 中使用 optimization.splitChunks.chunks
將重複的依賴關係移動到一個公共文件)。或者某個包具備相同庫的多個版本?
類似的依賴關係:是否有相似的庫能夠作大體相同的工做?(例如,moment
和 date-fns
,或 lodash
和 lodash-es
),試着只用一個工具。
(1)削減沒必要要的字節。壓縮全部內容,刪除未使用的代碼,明智地添加依賴項; (2)按路由拆分代碼。只加載如今真正須要的東西,稍後再加載其餘東西; (3)緩存代碼。應用程序的某些部分(如第三方庫)更新的頻率低於其餘部分,將這些部分分離到文件中,以便只在必要時從新下載; (4)追蹤代碼大小。使用像 webpack-dashboard 和 webpack-bundle-analyzer 這樣的工具來了解你的應用程序有多大。