前端打包构建优化
现在前端框架项目,都是经过构建工具(Webpack、Vite、Rollup 等)打包生成。这一步也是 优化网站访问速度的主要手段。
如果你是使用 Webpack 打包工具,优化和分析构建产物可以使用 webpack-bundle-analyzer
插件。
下面是我经历过后的一些经验和建议,一起来看下~
1、按需加载路由
在一个大型应用中,通常会设定多个路由页面。默认这些页面会被 Webpack 打包到同一个 chunk 文件中(main.js)。
在这种情况下,首屏渲染时就会出现白屏时间过长问题。
因此,我们可以将每个页面路由组件,拆成单独的一个个 chunk 文件,这样 main.js 文件体积降低,在首屏加载时,不会再加载其他页面的资源,从而提升首屏渲染速度。
要将路由组件拆分成 chunk 也很简单,使用异步 API import()
函数导入组件,Webpack 在打包时会将异步导入拆分成单独的 chunk
文件。
const Home = LazyComponent("Home", () => import(/* webpackChunkName: "Home" */ "@/pages/Home"));
... 其他路由组件导入
<Route exact path="/" component={Home} />
... 其他路由组件
LazyComponent
是自行封装的一个懒加载组件,在拿到 import()
异步加载组件内容后,渲染对应组件。
2、合理进行分包
在 Webpack5 中有一个配置选项 splitChunks
,可以用来拆包和提取公共代码。
- 拆包:将一些模块拆分到单独的 chunk 文件中,如将第三方模块拆分到
vendor.js
中; - 提取公共代码:将多处引入的模块代码,提取到单独的 chunk 文件中,防止代码被重复打包,如拆分到
common.js
中。
如下是一个分包配置的示例:
// 拆分 chunks
splitChunks: {
// cacheGroups 配置拆分(提取)模块的方案(里面每一项代表一个拆分模块的方案)
cacheGroups: {
// 禁用默认的缓存组,使用下方自定义配置
defaultVendors: false,
default: false,
// 将 node_modules 中第三方模块抽离到 vendors.js 中
vendors: {
// chunk 名称
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'all',
},
// 按照模块的被引用次数,将公共模块抽离到 common.js 中
common: {
// chunk 名称
name: 'chunk-common',
priority: -20,
chunks: "all",
// 模块被引入两次及以上,拆分到 common chunk 中
minChunks: 2,
},
// 将 React 生态体系作为一个单独的 chunk,减轻 chunk-vendors 的体积
react: {
test: /[\\/]node_modules[\\/](react|react-dom|redux|react-redux|history|react-router|react-router-dom)/,
name: 'chunk-react-package',
priority: 0,
chunks: 'all',
enforce: true,
},
}
},
3、启用代码压缩
使用 TerserWebpackPlugin
和 CssMinimizerPlugin
插件对 JS/CSS 代码进行压缩,降低打包资源体积。
const TerserWebpackPlugin = require("terser-webpack-plugin"); // JS 压缩
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); // CSS 压缩
...
optimization: {
minimize: isEnvProduction,
// 配置压缩工具
minimizer: [
new TerserWebpackPlugin({
// 不提取注释到单独的 .txt 文件
extractComments: false,
// 使用多进程并发运行以提高构建速度。并发运行的默认数量:os.cpus().length - 1。
parallel: true,
}),
new CssMinimizerPlugin(), // CSS 压缩
],
...
}
4、配置特定模块查找路径
如果你的项目或老项目中有使用过 moment.js 时,默认 Webpack 打包会包含所有语言包,导致打包文件非常大。通过配置 ContextReplacementPlugin
,可以仅包含特定的语言包,如 zh-cn 和 en,从而减小打包文件的大小 。
plugins: [
new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /zh-cn|zh-hk|en/),
];
5、externals 指定外部模块
比如 JQuery
,一般我们为了减少 vendors chunk
文件的体积,会采用 CDN script
方式引入。
为了在模块中继续使用 import
方式引入 JQuery(import $ from 'jquery'
),同时期望 Webpack 不要对路径为 jquery
的模块进行打包,可以配置 externals
外部模块选项。
module.exports = {
//...
externals: {
// jquery 通过 script 引入之后,全局中即有了 jQuery 变量
jquery: "jQuery",
},
};
6、静态图片管理
在你的项目中可能有这样一个 loader
配置:使用 url-loader
将小于某一阈值的图片,打包成 base64
形式到 chunk 文件中。
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/, /\.webp$/],
loader: require.resolve('url-loader'),
options: {
limit: '10000', // 大于10k,打包单独静态资源,否则处理成 base64
name: 'static/media/[name].[contenthash:8].[ext]', // 图片资源添加版本号
},
},
看着好像没啥问题,不过现在有一场景:有一组(20 个)文件封面图,平均文件体积大小在 9kb 左右,由于不满足阈值,最终会被打包成 base64 形式放到 chunk 中。
这会导致 chunk 文件中多了将近 200kb 的大小。建议这类一组文件图的情况,可以选择存放到 CDN 静态资源服务器上进行使用。
7、明确目标环境
我们编写的 ES6+ 代码会经过 Babel
进行降级转换和 polyfill
后得到 ES5 代码运行在浏览器上,不过这会增大 chunk 资源的体积。
但是如果明确我们的网站仅运行在主流浏览器上,不考虑 IE11,可以将构建目标调整为 ES2015
,这样可以减少 Babel 的降级处理和 polyfill 的引入,减小打包后的 chunk 资源体积。
以下是一个 Babel
配置的示例:
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
include: [path.resolve(context, 'src')],
loader: require.resolve('babel-loader'),
options: {
presets: [
['@babel/preset-env', {
"targets": {
// - 配置表示支持市场份额大于 0.2% 的浏览器,不包括已停止维护的浏览器和 Opera Mini。(此配置打包后是 es6+ 代码)
"browsers": ["> 0.2%", "not dead", "not op_mini all"],
// - 如果期望打包后是 es5 代码,可以使用下面配置,确保 Babel 会将代码转换为 ES5 语法,以兼容 IE 11 及更旧的浏览器。
// "browsers": ["> 0.2%", "ie 11"]
},
// Babel 会根据代码中使用的特性自动引入必要的 polyfill(通过 core-js),以确保这些特性在目标环境中可用。
"useBuiltIns": "usage",
"corejs": 3
}],
'@babel/preset-react',
'@babel/preset-typescript',
],
...
}
},
8、优化构建速度
打包构建速度过慢,也会影响我们的工作效率。可以从以下几个方向入手:
1、配置模块查找范围
通过 resolve
选项配置模块的查找范围和文件扩展名。
// 模块解析
resolve: {
// 解析模块时应该搜索的目录
modules: ['node_modules'],
extensions: ['.ts', '.tsx', '.js', '.jsx'], // 查找模块时,为不带扩展名的模块路径,指定要查找的文件扩展名
...
},
2、配置 babel-loader 编译范围
通过 exclude、include
配置来确保编译尽可能少的文件。
const path = require("path");
module.exports = {
//...
module: {
rules: [
{
test: /\.js[x]?$/,
use: ["babel-loader"],
include: [path.resolve(__dirname, "src")],
},
],
},
};
3、开启 babel-loader 编译缓存
设置 cacheDirectory
属性开启编译缓存,避免 Webpack 在每次构建时产生高性能消耗的 Babel 编译过程。
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
include: [path.resolve(context, 'src')],
loader: require.resolve('babel-loader'),
options: {
...
// 开启编译缓存,缓存 loader 的执行结果(默认缓存目录:node_modules/.cache/babel-loader),提升构建速度(作用和单独使用 cache-loader 一致)
cacheDirectory: true,
// 配合 cacheDirectory 使用,设置 false 禁用缓存文件压缩,这会增加缓存文件的大小,但会减少压缩的消耗时间,提升构建速度。
cacheCompression: false,
}
},
4. 启用多进程对 JS 代码压缩
在使用 TerserWebpackPlugin
对 JS 代码进行压缩时,默认选项 parallel = true
就开启了多进程并发运行,以提高构建速度。
new TerserWebpackPlugin({
// 使用多进程并发运行以提高构建速度。并发运行的默认数量:os.cpus().length - 1。
parallel: true,
...
}),