利用 Webpack 來優化 Web 效能屬於載入效能優化 的一部分: ☛ Web Performance Optimization with webpack
本文目錄:
- 減少前端資源體積
- 使用長期快取
- 監控和分析應用程式
- 總結
一、減少前端資源體積
1、webpack 4 開啟 production
模式
production
模式下 webpack 會對程式碼進行優化,如減小程式碼體積,刪除只在開發環境用到的程式碼。
可以在 webpack 中指定:
module.exports = {
mode: 'production' // 或 development
};
複製程式碼
或者 package.json 中配置:
"scripts": {
"dev": "webpack-dev-server --mode development --open --hot",
"build": "webpack --mode production --progress"
}
複製程式碼
2、壓縮程式碼
使用 bundle-level minifier 和 loader options 壓縮程式碼。
- Bundle-level minification
Bundle-level 的壓縮會在程式碼編譯後對整個包進行壓縮。
在 webpack 4 中,production
模式下會自動執行 bundle-level 的壓縮,底層使用了 the UglifyJS minifier。(如果不想開啟壓縮,可以採用 development
模式或者設定 optimization.minimize
為 false)
- Loader-specific options
通過 loader 層面的選項配置來對程式碼進行壓縮,是為了壓縮 bundle-level minifier 無法壓縮的內容,比如,通過 css-loader
編譯後的檔案,會成為字串,就無法被 minifier 壓縮。因此,要進一步壓縮檔案內容,可進行如下配置:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
{ loader: 'css-loader', options: { minimize: true } },
],
},
],
},
};
複製程式碼
3、使用 ES 模組
當使用 ES 模組時, webpack 能夠進行 tree-shaking。
tree-shaking 是指 bundler 遍歷整個依賴關係樹,檢查使用了哪些依賴關係,並刪除未使用的依賴關係。因此,如果使用ES模組語法,webpack 可以消除未使用的程式碼。
★ 注意:在 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
4、壓縮圖片資源
針對具體的依賴項進行優化(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',
},
],
},
};
複製程式碼
5、優化第三方依賴
JavaScript 的大小平均有一半以上來自依賴項,而其中的一部分可能是不必要的。我們可以對這些依賴的庫進行優化➡️ webpack-libs-optimizations。
比如:moment.js 刪除未使用的地區、react-router 移除未使用的模組,生產環境去除 react propTypes 宣告等。
6、對於ES6模組開啟模組連線
也叫做作用域提升(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,
},
};
複製程式碼
★ 注意:為什麼預設情況下不啟用此行為?連線模組很酷,但是它增加了構建時間,並中斷了熱模組替換。這就是為什麼應該只在生產中啟用它。
7、如果覺得有意義的話,使用 externals
具體請參考:webpack-configuration-externals
二、使用長期快取
1、檔名輸出
快取包(
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
2、提取第三方庫和樣板程式碼
將
bundle
拆分成程式程式碼(app
)、第三方庫程式碼(vendor
)和執行時程式碼(runtime
)。
- 開啟智慧 code splitting
在 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 執行時程式碼
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(),
],
};
複製程式碼
3、程式碼懶載入
單頁應用中,使用
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
4、模組識別符號
使模組識別符號更穩定
在 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-dashboard 通過展示依賴項大小、進度和其他細節來增強 webpack 輸出,有助於跟蹤大型依賴項。
npm install webpack-dashboard --save-dev
複製程式碼
// webpack.config.js
const DashboardPlugin = require('webpack-dashboard/plugin');
module.exports = {
plugins: [
new DashboardPlugin(),
],
};
複製程式碼
- bundlesize
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 這樣的工具來了解你的應用程式有多大。
參考
- Web Performance Optimization with webpack
- Webpack docs
- webpack 4: Code Splitting, chunk graph and the splitChunks optimization
- Hash vs chunkhash vs ContentHash
- Code Splitting with React and React Router
- 一步一步的瞭解 webpack4 的 splitChunk 外掛
- Webpack 4 配置最佳實踐
- 三十分鐘掌握 Webpack 效能優化
- 使用 webpack4 提升180%編譯速度
- 【第1711期】Webpack優化——將你的構建效率提速翻倍