性能優化方案
優化分類:
- 優化打包後的結果(分包、減小包體積、CDN 服務器) ==> 更重要
- 優化打包速度(exclude、cache-loader)
代碼分割(Code Splitting)
一、主要目的
- 減少首屏加載體積:避免一次性加載全部代碼
- 利用瀏覽器緩存:第三方庫(如 React、Lodash)變動少,可單獨緩存
- 按需加載/並行請求:路由、組件、功能模塊只在需要時加載(按需加載或者並行加載文件,而不是一次性加載所有代碼)
二、三種主要的代碼分割方式
1. 入口起點(Entry Points)手動分割
通過配置多個 entry 實現。
// webpack.config.js
module.exports = {
entry: {
main: './src/main.js',
vendor: './src/vendor.js', // 手動引入公共依賴
},
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
},
};
缺點:
- 無法自動提取公共依賴(比如
main和vendor都用了 Lodash,會重複打包)- 維護成本高
上面寫的是通用配置,但我們在公司一般會分別配置開發和生產環境的配置。大多數項目中,entry 在 dev 和 prod 基本一致,無需差異化配置。差異主要體現在 output 和其他插件/加載器行為上。
// webpack.config.prod.js
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
filename: 'js/[name].[contenthash:8].js', // 生產環境用 [contenthash](而非 [hash] 或 [chunkhash]),確保精準緩存
chunkFilename: 'js/[name].[contenthash:8].js',
path: path.resolve(__dirname, 'dist'), // 必須輸出到磁盤用於部署
publicPath: '/static/', // 用於 CDN 或靜態資源服務器
clean: true, // 清理舊文件
},
};
// webpack.config.dev.js
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'js/[name].js', // 開發環境若加 hash,每次保存都會生成新文件,可能干擾熱更新或者devtools混亂
chunkFilename: 'js/[name].js',
path: path.resolve(__dirname, 'dist'), // 通常仍寫 dist,但實際不寫入磁盤(webpack-dev-server 默認內存存儲),節省IO,提高編譯速度
publicPath: '/', // 與 devServer 一致
// clean: false (默認)
},
};
2. SplitChunksPlugin(推薦!自動代碼分割)
自動提取公共模塊和第三方庫。webpack 已默認安裝相關插件。
默認行為(僅在 production 模式生效):
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'async', // 默認只分割異步模塊
},
},
};
常用配置:
// webpack.config.prod.js
optimization: {
// 自動分割
// https://twitter.com/wSokra/status/969633336732905474
// https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366
splitChunks: {
// chunks: async | initial(對通過的代碼處理) | all(同步+異步都處理)
chunks: 'initial',
minSize: 20000, // 模塊大於 20KB 才分割(Webpack 5 默認值)
maxSize: 244000, // 單個 chunk 最大不超過 244KB(可選)
cacheGroups: { // 拆分分組規則
// 提取 node_modules 中的第三方庫
vendor: {
test: /[\\/]node_modules[\\/]/, // 匹配符合規則的包
name: 'vendors', // 拆分包的name 屬性
chunks: 'initial',
priority: 10, // 優先級高於 default
enforce: true,
},
// 提取多個 chunk 公共代碼
default: {
minChunks: 2, // 至少被 2 個 chunk 引用
priority: -20,
reuseExistingChunk: true, // 複用已存在的 chunk
maxInitialRequests: 5, // 默認限制太小,無法顯示效果
minSize: 0, // 這個示例太小,無法創建公共塊
},
},
},
// runtime相關的代碼是否抽取到一個單獨的chunk中,比如import動態加載的代碼就是通過runtime 代碼完成的
// 抽離出來利於瀏覽器緩存,比如修改了業務代碼,那麼runtime加載的chunk無需重新加載
runtimeChunk: true,
}
在開發環境下 splitChunks: false, 即可。
生產環境:
- 生成
vendors.xxxx.js(第三方庫)- 生成
default.xxxx.js(項目公共代碼)- 主 bundle 體積顯著減小
3. 動態導入(Dynamic Imports)—— 按需加載
使用 import() 語法(符合 ES Module 規範),實現懶加載。
Webpack 會為每個
import()創建一個獨立的 chunk,並自動處理加載邏輯。
三、魔法註釋(Magic Comments)—— 控制 chunk 名稱等行為
// 自定義 chunk 名稱(便於調試和長期緩存)
const module = await import(
/* webpackChunkName: "my-module" */
'./my-module'
);
其他常見註釋:
/* webpackPrefetch: true */:空閒時預加載(提升後續訪問速度)/* webpackPreload: true */:當前導航關鍵資源預加載(慎用)
// 預加載“下一個可能訪問”的頁面
import(
/* webpackChunkName: "login-page" */
/* webpackPrefetch: true */
'./LoginPage'
);
詳細比較:
- preload chunk 會在父 chunk 加載時,以並行方式開始加載。prefetch chunk 會在父 chunk 加載結束後開始加載。
- preload chunk 具有中等優先級,並立即下載。prefetch chunk 在瀏覽器閒置時下載。
CND
內容分發網絡(Content Delivery Network 或 Content Distribution Network)
它是指通過相互連接的網絡系統,利用最靠近每個用户的服務器;更快、更可靠地將音樂、圖片、視頻、應用程序及其他文件發送給用户;提供高性能、可擴展性及低成本的網絡內容傳遞。
工作中,我們使用 CDN 的主要方式有兩種:
- 打包所有靜態資源,放到 CDN 服務器,用户所有資源都是通過 CND 服務器加載的
- 通過
output.publicPath改為自己的的 CDN 服務器,打包後就可以從上面獲取資源 - 如果是自己的話,一般會從阿里、騰訊等買 CDN 服務器。
- 通過
- 一些第三方資源放在 CDN 服務器上
- 一些庫/框架會將打包後的源碼放到一些免費的 CDN 上,比如 JSDeliver、bootcdn 等
- 這樣的話,打包的時候就不需要對這些庫進行打包,直接使用 CDN 服務器中的源碼(通過 externals 配置排除某些包)
CSS 提取
將 css 提取到一個獨立的 css 文件。
npm install mini-css-extract-plugin -D
// webpack.config.prod.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
mode: 'production',
module: {
rules: [
// 生產環境:使用 MiniCssExtractPlugin.loader
{
test: /\.css$/i,
use: [
MiniCssExtractPlugin.loader, // 替換 style-loader
'css-loader',
'postcss-loader',
],
},
{
test: /\.s[ac]ss$/i,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
'sass-loader',
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[name].[contenthash:8].css',
}),
],
};
Terser 代碼壓縮
Terser 可以幫助我們壓縮、醜化(混淆)我們的代碼,讓我們的 bundle 變得更小。
Terser 是一個單獨的工具,擁有非常多的配置,這裏我們只講工作中如何使用,以一個工程的角度學習這個工具。
真實開發中,我們不需要手動的通過 terser 來處理我們的代碼。webpack 中 minimizer 屬性,在 production 模式下,默認就是使用的 TerserPlugin 來處理我們代碼的。我們也可以手動創建 TerserPlugin 實例覆蓋默認配置。
// webpack.prod.js
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
mode: 'production',
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true, // 多核 CPU 並行壓縮,默認為true,併發數默認為os.cpus().length-1
terserOptions: {
compress: { // 壓縮配置
drop_console: true,
drop_debugger: true, // 刪除debugger
pure_funcs: ['console.info', 'console.debug'], // 只刪除特定的函數調用
},
mangle: true, // 是否醜化代碼(變量)
toplevel: true, // 頂層變量是否進行轉換
keep_classnames: true, // 是否保留類的名稱
keep_fnames: true, // 是否保留函數的名稱
format: {
comments: /@license|@preserve/i, // 保留含 license/preserve 的註釋(某些開源庫要求保留版權註釋)
},
},
extractComments: true, // 默認為true會將註釋提取到一個單獨的文件(這裏用於保留版權註釋),false表示不希望保留註釋
sourceMap: true, // 需要 webpack 配置 devtool 生成 source map
}),
],
},
};
不要在開發環境啓動 terser,因為:
- 壓縮會拖慢構建速度
- 混淆後的代碼無法調試
- hmr 和 source-map 會失效
CSS 壓縮
CSS 壓縮通常是去除無用的空格等,因為很難去修改選擇器、屬性的名稱、值等;我們一般使用插件 css-minimizer-webpack-plugin;他的底層是使用 cssnano 工具來優化、壓縮 CSS(也可以單獨使用)。
使用也是非常簡單:
minimizer: [
new CssMiniMizerPlugin()({
parallel: true
})
]
Tree Shaking 搖樹
詳情見之前文章:《簡單聊聊 webpack 搖樹的原理》
HTTP 壓縮
HTTP 壓縮(HTTP Compression)是一種 在服務器和客户端之間傳輸數據時減小響應體體積 的技術,通過壓縮 HTML、CSS、JavaScript、JSON 等文本資源,顯著提升網頁加載速度、節省帶寬。
一、主流壓縮算法
| 算法 | 兼容性 | 壓縮率 | 速度 | 説明 |
|---|---|---|---|---|
| gzip | ✅ 幾乎所有瀏覽器(IE6+) | 高 | 快 | 最廣泛使用,Web 標準推薦 |
| Brotli (br) | ✅ 現代瀏覽器(Chrome 49+, Firefox 44+, Safari 11+) | ⭐ 更高(比 gzip 高 15%~30%) | 較慢(壓縮),解壓快 | 推薦用於靜態資源 |
| deflate | ⚠️ 支持不一致(部分瀏覽器實現有問題) | 中 | 中 | 已基本淘汰,不推薦使用 |
二、工作原理(協商壓縮)
HTTP 壓縮基於 請求頭 ↔ 響應頭協商機制:
- 客户端請求(表明支持的壓縮格式)
GET /app.js HTTP/1.1
Host: example.com
Accept-Encoding: gzip, deflate, br // 客户端支持的壓縮算法列表
- 服務端響應(返回壓縮後的內容)
HTTP/1.1 200 OK
Content-Encoding: br // 服務端使用的壓縮算法
Content-Type: application/javascript
Content-Length: 102400 // 注意:這是壓縮後的大小!
...(二進制壓縮數據)...
- 瀏覽器自動解壓,開發者無感知
三、如何啓用 HTTP 壓縮?
我們一般會優先使用 Nginx 配置做壓縮(生產環境最常用),這樣就無需應用層處理。
除此之外,我們還會進行預壓縮 + 靜態文件服務,這主要就是 webpack 要做的工作。
在構建階段(Webpack/Vite)就生成 .gz 和 .br 文件,部署到 CDN 或靜態服務器。
// webpack.config.js
const CompressionPlugin = require('compression-webpack-plugin');
module.exports = {
plugins: [
// 生成 .gz 文件
new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 8192, // 大於 8KB 才壓縮
minRatio: 0.8, // 至少的壓縮比例
}),
// 生成 .br 文件(需額外安裝)
new CompressionPlugin({
algorithm: 'brotliCompress',
test: /\.(js|css|html|svg)$/,
compressionOptions: { level: 11 }, // 最高壓縮率
}),
],
};
Nginx 配合預壓縮文件:
gzip_static on; # 優先返回 .gz 文件
brotli_static on; # 優先返回 .br 文件
打包分析
打包時間分析
我們需要藉助一個插件 speed-measure-webpack-plugin,即可看到每個 loader、每個 plugin 消耗的打包時間。
// webpack.config.js
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();
const config = {
// 你的正常 Webpack 配置
entry: './src/index.js',
module: { /* ... */ },
plugins: [ /* ... */ ],
};
// 僅當環境變量 ANALYZE_SPEED=1 時包裹配置
module.exports = process.env.ANALYZE_SPEED ? smp.wrap(config) : config;
打包文件分析
方法一、生成 stats.json 文件
"build:stats": "w--config ./config/webpack.common.js --env production --profile --json=stats.json",
運行 npm run build:stats,可以獲取到一個 stats.json 文件,然後放到到 http://webpack.github.com/analyse 進行分析。
方法二、webpack-bundle-analyzer
更常用的方式是使用 webpack-bundle-analyzer 插件分析。
// webpack.prod.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
mode: 'production',
plugins: [
// 其他插件...
new BundleAnalyzerPlugin({
analyzerMode: 'static', // 生成靜態 HTML 報告(默認)
openAnalyzer: false, // 不自動打開瀏覽器
reportFilename: 'bundle-report.html',
generateStatsFile: true, // 可選:同時生成 stats.json
statsFilename: 'stats.json',
}),
],
};