本文主要介紹 webpack5 項目的打包優化方案
打包優化
速度分析:要進行打包速度的優化,首先我們需要搞明白哪一些流程的在打包執行過程中耗時較長。
這里我們可以借助 speed-measure-webpack-plugin
插件,它分析 webpack 的總打包耗時以及每個 plugin 和 loader 的打包耗時,從而讓我們對打包時間較長的部分進行針對性優化。
通過以下命令安裝插件:
yarn add speed-measure-webpack-plugin -D
在 webpack.config.js
中添加如下配置
// ...
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap({
// ...
plugins: [
// ...
],
module: {
// ...
}
});
執行 webpack 打包命令后,如下圖可以看到各個 loader 和 plugin 的打包耗時:
cdn 分包
對於項目中我們用的一些比較大和比較多的包,例如 react 和 react-dom,我們可以通過 cdn 的形式引入它們,然后將 react
、react-dom
從打包列表中排除,這樣可以減少打包所需的時間。
排除部分庫的打包需要借助 html-webpack-externals-plugin
插件,執行如下命令安裝:
yarn add html-webpack-externals-plugin -D
以 react 和 react-dom 為例,在 webpack 中添加如下配置:
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
module.exports = {
// ...
plugins: [
new HtmlWebpackExternalsPlugin({
externals: [
{
module: 'react',
entry: 'https://unpkg.com/react@17.0.2/umd/react.production.min.js',
global: 'React',
},
{
module: 'react-dom',
entry:
'https://unpkg.com/react-dom@17.0.2/umd/react-dom.production.min.js',
global: 'ReactDOM',
},
],
}),
],
};
效果對比如下:
優化前打包時間約為 2s: 優化后打包時間不到 1s:
多進程構建
對於耗時較長的模塊,同時開啟多個 nodejs 進程進行構建,可以有效地提升打包的速度。可以采取的一些方式有:
- thread-loader
- HappyPack(作者已經不維護)
- parallel-webpack
下面以官方提供的 thread-loader 為例,執行以下命令安裝 thread-loader
:
yarn add thread-loader -D
在 webpack.config.js
中添加如下配置:
module.exports = {
module: {
rules: [
{
test: /.js$/,
include: path.resolve('src'),
use: [
"thread-loader",
// 耗時的 loader (例如 babel-loader)
],
},
],
},
};
使用時,需將此 loader 放置在其他 loader 之前,放置在此 loader 之后的 loader 會在一個獨立的 worker 池中運行。
每個 worker 都是一個獨立的 node.js 進程,其開銷大約為 600ms 左右。同時會限制跨進程的數據交換。所以請僅在耗時的操作中使用此 loader!(一般只在大型項目中的 ts、js 文件使用)
並行壓縮
一些插件內置了 parallel
參數(如 terser-webpack-plugin
, css-minimizer-webpack-plugin
, html-minimizer-webpack-plugin
),開啟后可以進行並行壓縮。
webpack5版本內置了 terser-webpack-plugin
的配置,如果是 v4 或者更低版本,執行以下命令安裝 terser-webpack-plugin
:
yarn add terser-webpack-plugin -D
在 webpack.config.js
進行如下配置:
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
optimization: {
minimize: true,
minimizer: [new TerserPlugin()],
},
};
預編譯資源模塊
通過預編譯資源模塊,可以代替 cdn 分包的方式,解決每個模塊都得引用一個 script 的缺陷。
還是以 react 和 react-dom 為例,新建一個 webpack.dll.js
文件,用於預編譯資源的打包,例如要對 react 和 react-dom 進行預編譯,配置如下:
const path = require('path');
const webpack = require('webpack');
module.exports = {
mode: 'production',
entry: {
library: ['react', 'react-dom'],
},
output: {
filename: 'react-library.dll.js',
path: path.resolve(__dirname, './dll'),
library: '[name]_[hash]', // 對應的包映射名
},
plugins: [
new webpack.DllPlugin({
context: __dirname,
name: '[name]_[hash]', // 引用的包映射名
path: path.join(__dirname, './dll/react-library.json'),
}),
],
};
在 package.json
中新增一條如下命令:
{
// ...
"scripts": {
// ...
"build:dll": "webpack --config ./webpack.dll.js"
},
// ...
}
執行 npm run build:dll
后,會在 /build/library
目錄下生成如下內容,library.js
中打包了 react 和 react-dom 的內容,library.json
中添加了對它的引用:
然后在 webpack.config.js
中新增如下內容:
const webpack = require('webpack');
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');
module.exports = {
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./dll/react-library.json'),
}),
// 打包后的 .dll.js 文件需要引入到 html中,可以通過 add-asset-html-webpack-plugin 插件自動引入
new AddAssetHtmlPlugin({
filepath: require.resolve('./dll/react-library.dll.js'),
publicPath: '',
}),
],
};
效果對比如下:
使用 dll 預編譯資源之前,打包效果如下,總打包耗時 1964ms,且需要打包 react: 使用 dll 預編譯資源之后,打包效果如下,總打包耗時 1148ms,不需要打包 react:
使用緩存
通過使用緩存,能夠有效提升打包速度。緩存主要有以下幾種方案:
- 使用 webpack5 內置的 cache 模塊
- cache-loader(webpack5內置了 cache 模塊后可棄用 cache-loader)
內置的 cache 模塊
webpack5 內置了 cache 模塊,緩存生成的 webpack 模塊和 chunk,來改善構建速度。它在開發環境下會默認設置為 type: 'memory'
而在生產環境中被禁用。cache: { type: 'memory' }
與 cache: true
作用一樣,可以通過設置 cache: { type: 'filesystem' }
來開放更多配置項。
例如在 webpack.config.js
中作如下配置:
module.exports = {
cache: {
type: 'filesystem',
},
};
會在 node_modules 目錄下生成一個 .cache 目錄緩存文件內容,且二次打包速度顯著提升:
cache-loader
在一些性能開銷較大的 loader 之前添加 cache-loader
,能將結果緩存到磁盤里。保存和讀取這些緩存文件會有一些時間開銷,所以請只對性能開銷較大的 loader 使用。
執行如下命令安裝 cache-loader
:
npm install cache-loader -D
在 webpack.config.js
對應的開銷大的 loader 前加上 cache-loader
:
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [
'cache-loader',
'babel-loader'
]
}
]
}
}
同樣會在 node_modules 目錄下生成一個 .cache 目錄緩存文件內容,且二次打包速度顯著提升:
縮小構建范圍
通過合理配置 rules
中的文件查找范圍,可以減少打包的范圍,從而提升打包速度。
在 webpack.config.js
中新增如下配置:
module.exports = {
// ...
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
exclude: /node_modules/,
},
],
},
};
效果對比如下:
配置前,編譯總耗時 1867ms: 配置后,編譯總耗時 1227ms:
加快文件查找速度
通過合理配置 webpack 的 resolve 模塊,可以加快文件的查找速度,例如可以對如下的選項進行配置:
- resolve.modules 減少模塊搜索層級,指定當前 node_modules,慎用。
- resovle.mainFields 指定包的入口文件。
- resolve.extension 對於沒有指定后綴的引用,指定解析的文件后綴查找順序
- 合理使用 alias,指定第三方依賴的查找路徑
對 webpack.config.js
作如下配置:
module.exports = {
resolve: {
alias: {
react: path.resolve(__dirname, './node_modules/react/dist/react.min.js'),
},
modules: [path.resolve(__dirname, './node_modules')],
extensions: ['.js', '.jsx', '.json'],
mainFields: ['main'],
},
};
打包體積優化
體積分析
同速度優化一樣,我們要對體積進行優化,也需要了解打包時各個模塊的體積大小。這里借助 webpack-bundle-analyzer
插件,它可以分析打包的總體積、各個組件的體積以及引入的第三方依賴的體積。
執行如下命令安裝 webpack-bundle-analyzer
:
yarn add webpack-bundle-analyzer -D
在 webpack.config.js
中添加如下配置:
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
// ...
plugins: [
new BundleAnalyzerPlugin()
],
};
然后執行 webpack 打包命令,會在 localhost:8888
頁面看到打包后的體積分析:
提取公共模塊
假如我們現在有一個 MPA(多頁面應用) 的 react 項目,每個頁面的入口文件及其依賴的組件中都會引入一份 react
和 react-dom
,那最終打包后的每個頁面中同樣也會有一份以上兩個包的代碼。我們可以將這兩個包單獨抽離出來,最終在每個打包后的頁面入口文件中引入,從而減少打包后的總體積。
在 webpack.config.js
中添加如下配置:
module.exports = {
optimization: {
splitChunks: {
minSize: 20000,
cacheGroups: {
react: {
test: /(react|react-dom)/,
name: 'vendors',
chunks: 'all',
},
},
},
}
};
效果對比:
優化前總體積 473 kb: 優化后總體積 296 kb:
壓縮代碼
html 壓縮
安裝 html-webpack-plugin
插件,生產環境下默認會開啟 html 壓縮:
npm install html-webpack-plugin
webpack.config.js
做如下配置:
module.exports = {
// ...
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, '../', 'public/index.html'),
}),
],
};
css 壓縮
css-minimizer-webpack-plugin
插件可以壓縮 css 文件代碼,但由於壓縮的是 css 代碼,所以還需要依賴 mini-css-extract-plugin
將 css 代碼單獨抽離:
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: "[name].css",
chunkFilename: "[id].css",
}),
],
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
],
},
optimization: {
minimizer: [
// webpack5 可以使用 '...' 訪問 minimizer 數組的默認值
'...',
new CssMinimizerPlugin(),
],
},
};
js 壓縮
生產環境下會默認開啟 js 的壓縮,無需單獨配置。
圖片壓縮
使用 image-minimizer-webpack-plugin
配合 imagemin
可以在打包時實現圖片的壓縮。
執行如下命令安裝 image-minimizer-webpack-plugin
配合 imagemin
:
npm install image-minimizer-webpack-plugin imagemin imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo --save-dev
在 webpack.config.js
中新增如下配置:
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
const { extendDefaultPlugins } = require("svgo");
module.exports = {
module: {
rules: [
{
test: /\.(jpe?g|png|gif|svg)$/i,
type: "asset",
},
],
},
optimization: {
minimizer: [
"...",
new ImageMinimizerPlugin({
minimizer: {
implementation: ImageMinimizerPlugin.imageminMinify,
options: {
// Lossless optimization with custom option
// Feel free to experiment with options for better result for you
plugins: [
["gifsicle", { interlaced: true }],
["jpegtran", { progressive: true }],
["optipng", { optimizationLevel: 5 }],
// Svgo configuration here https://github.com/svg/svgo#configuration
[
"svgo",
{
plugins: extendDefaultPlugins([
{
name: "removeViewBox",
active: false,
},
{
name: "addAttributesToSVGElement",
params: {
attributes: [{ xmlns: "http://www.w3.org/2000/svg" }],
},
},
]),
},
],
],
},
},
}),
],
},
};
效果對比如下:
壓縮前圖片打包后 1.1m: 壓縮后 451kb:
移除無用的 css
通過 purgecss-webpack-plugin
,可以識別沒有用到的 class,將其從 css 文件中 treeShaking 掉,需要配合 mini-css-extract-plugin
一起使用。
執行如下命令安裝 purgecss-webpack-plugin
:
npm install purgecss-webpack-plugin -D
在 webpack.config.js
文件中做如下配置:
const path = require('path')
const glob = require('glob')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const PurgecssPlugin = require('purgecss-webpack-plugin')
const PATHS = {
src: path.join(__dirname, 'src')
}
module.exports = {
module: {
rules: [
{
test: /.css$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader"
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].css",
}),
new PurgecssPlugin({
paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }),
}),
]
}
在 css 文件中添加一段未用到的 css 代碼:
div {
font-size: 44px;
display: flex;
}
// 此段為用到:
.unuse-css {
font-size: 20px;
}
使用 purgecss-webpack-plugin
之前,打包結果如下: 使用
purgecss-webpack-plugin
之后,打包結果如下,無用代碼已經移除:
polyfill service
我們在項目使用了 es6+ 語法時,往往需要引入 polyfill 去兼容不同瀏覽器。目前我們常采用的方案一般是 babel-polyfill
或者 babel-plugin-transform-runtime
,然而在部分不同的瀏覽器上,它們一般都會與冗余,從而導致項目一些不必要的體積增大。
以下是幾種常見 polyfill 方案的對比:
方案 | 優點 | 缺點 |
---|---|---|
babel-polyfill | 功能全面 | 體積太大超過200kb,難以抽離 |
babel-plugin-transform-runtime | 只polyfill用到的類或者方法,體積相對較小 | 不能polyfill原型上的方法,不適合復雜業務 |
團隊維護自己的polyfill | 定制化高,體積小 | 維護成本太高 |
polyfill service | 只返回需要的polyfill,體積最小 | 部分奇葩瀏覽器的UA不識別,走優雅降級方案返回全部polyfill |
這里我們可以采用 polyfill service 方案,它能夠識別 User Agent,下發不同的 polyfill,做到按需加載需要的 polyfill,從而優化我們項目的體積。
去 polyfill.io/ 查看最新的 polyfill service 的 url,例如目前是:
https://polyfill.io/v3/polyfill.min.js
直接在項目的 html 中通過 script 引入即可:
<script src="https://polyfill.io/v3/polyfill.min.js"></script>