老项目迁移 Webpack5 的经验总结
之前公司老项目重新启用并且迭代开发新功能,采用的构建工具是Webpack2,开发阶段启动速度和构建工程的速度都是非常慢的。因为这个项目的新需求基本都是我在对,所以我也是被这项目烦的要吐血。公司现有比较新的项目用的AntdPro是基于Webpack5的,速度还可以,所以就打算迁移了。迁移的过程中少不了翻阅文档,也学到了不少东西。
本文着重对自己学到的一些关于Webpack5的特性和优化技巧进行整理。当然,关于对Webpack5优化写的比较好的文章比比皆是。这里推荐一篇文章Webpack5.0 优化指南 - 构建效率篇 - 掘金 (juejin.cn),下面的文字内容也在一定程度上借鉴(chaoxi)了这篇文章。
关于缓存
这里是关于Webpack5相较于先前版本做的一些改动:Webpack 5 发布 (2020-10-10) | webpack 中文文档 (docschina.org)。增加的东西不少,不过对一般公司项目而言真正有用的还是与构建过程相关的特性,比如:
- 尝试用持久性缓存来提高构建性能。
- 尝试用更好的算法和默认值来改进长期缓存。
- 尝试用更好的 Tree Shaking 和代码生成来改善包大小。
这三个特性是与构建速度和产物体积有关的,也是我们最常关心的。而缓存是最老生常谈的优化手段,为了不做重复的工作,能用缓存的话干嘛不用。在Webpack5之前,loader在工作中使用缓存需要引入cache-loader,但是现在不需要那么多配置,只需要手动开启:
// webpack.config.js
module.exports = {
cache: {
type: "filesystem",
},
};当然,设置type为filesystem会有更多的可配置项,其实按默认值就已经可以了。cache的详细配置可见:Cache | webpack 中文文档 (docschina.org)。
关于编译速度
在我先前做过的React项目中,基本都是使用babel对项目代码进行解析。Webpack中提供了babel-loader来做这个工作。下面是babel-loader的常见配置:
module.exports = {
resolve: {
// require文件的时候不需要写后缀了,可以自动补全
extensions: [".js", ".jsx", ".css"],
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /(node_modules)/,
use: [
{
loader: "babel-loader",
options: {
babelrc: false,
presets: [
require.resolve("@babel/preset-react"),
[
require.resolve("@babel/preset-env"),
{
useBuiltIns: "usage",
corejs: 3,
modules: false,
},
],
],
cacheDirectory: true,
},
},
],
},
],
},
};这里顺带附上babel-loader的文档链接:webpack 中文文档 (docschina.org)。
不过这里要介绍比babel-loader更快的代码解析方案swc。在文章Why you should use SWC (and not Babel) - LogRocket Blog中,有两者性能的比较数据。文章的后面还有这么一段概括性的文字:
In general, we see a clear speed gap between the two tools, as SWC tends to be around 20 times faster than Babel on a single thread and CPU core basis while being around 60 times faster in a multi-core async operation process.
看这构建速度......确实是降维打击了。当然除了swc,还有基于golang的esbuild。通过使用高效的编译器进行编译和压缩混淆代码,对构建速度的提升也是明显的。
Webpack5在压缩代码时可以通过配置TenserPlugin来启用swc:
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
minify: TerserPlugin.swcMinify,
// `terserOptions` options will be passed to `swc` (`@swc/core`)
// Link to options - https://swc.rs/docs/config-js-minify
terserOptions: {},
}),
],
},
};详细配置见TerserWebpackPlugin | webpack 中文文档 (docschina.org)。
如果想使用swc取代babel进行代码编译,需要额外引入swc-loader。详见swc-loader – SWC。
关于项目模式
一般来说,一个前端项目会有两种模式:开发模式和生产构建模式。在生产构建打包的场景下,我们更倾向于把产物优化的更小,因此不可避免的会进行一些操作:压缩、代码混淆等操作。而在开发阶段,我们显然不需要这么多操作,不如说,这些操作在开发模式下就是在浪费时间。
在Webpack5中,我们可以在开发模式下的config文件中增加相应的配置:
// webpack.dev.js
module.exports = {
//...
optimization: {
removeAvailableModules: false,
removeEmptyChunks: false, //
splitChunks: false, // 代码分包
minimize: false, //代码压缩
concatenateModules: false,
usedExports: false, // Treeshaking
},
};关于optimization,详细的配置可以看这里:优化(Optimization) | webpack 中文文档 (docschina.org)。
关于资源文件的处理
在Webpack5之前,处理静态资源通常使用下面这三个loader:
raw-loader将文件导入为字符串url-loader将文件作为 data URI 内联到 bundle 中file-loader将文件发送到输出目录
不过在Webpack5中增加了资源模块的特性,我们可以在不使用loader的情况下使用资源文件。这个新特性通过添加 4 种新的模块类型,来取代上面提到的loader:
asset/resource发送一个单独的文件并导出 URL。之前通过使用file-loader实现。asset/inline导出一个资源的 data URI。之前通过使用url-loader实现。asset/source导出资源的源代码。之前通过使用raw-loader实现。asset在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用url-loader,并且配置资源体积限制实现。
下面是公司项目迁移Webpack5后,webpack.common.js的代码片段:
module.exports = {
module: {
rules: [
{
test: /\.ttf|eot|woff2?$/i,
type: "asset/resource",
generator: {
filename: "iconfont/[name].[ext]",
},
},
{
test: /\.(png|jpg|jpeg|gif|svg)$/i,
type: "asset",
parser: {
dataUrlCondition: {
// 模块小于 maxSize,会被作为Base64编码的字符串注入到包中,
// 否则模块文件会被生成到输出的目标目录中
maxSize: 1 * 1024,
},
},
generator: {
filename: "images/[name].[ext]",
},
},
],
},
};附上相应的文档:资源模块 | webpack 中文文档 (docschina.org)。
关于引入的库
我们通过npm安装的有一些库已经是打包完成的产物,按理来说这部分可以不用再重新打包。Webpack5可以通过配置module.noParse来跳过编译,如:
module.exports = {
//...
module: {
noParse: /(^vue$)|(^pinia$)|(^vue-router$)/,
},
};注意
忽略的文件中不应该含有import, require, define 的调用,或任何其他导入机制。
关于产物优化
所谓产物优化,当然是越小越好。不过小是一方面,要真正考虑产物优化的事情,实际用户加载产物的过程也要考虑进去。下面是一些常见的优化手段:
配置 external+CDN 引入库
我们可以通过配置Webpack的external,然后把一部分的库丢到CDN里之后通过<script>标签的形式引入根html文件,这样能显著减少打包产物的体积。
比如我优化的这个项目,有个库@antv/data-set,阿里官方有提供稳定的CDN服务。所以可以在生产打包的配置文件增加配置:
externals: {
"@antv/data-set": "DataSet"
},然后在index.ejs模版文件中增加代码:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>xxx</title>
</head>
<body>
<div id="root"></div>
<script
type="text/javascript"
src="https://gw.alipayobjects.com/os/antv/pkg/_antv.data-set-0.9.6/dist/data-set.min.js"
></script>
</body>
</html>这样就实现了通过CDN引入外部库。至于其他更加详尽的用法,看看官方文档就够了:webpack 中文文档 (docschina.org)。
MiniCssExtractPlugin 插件
这个插件有以下作用:
本插件会将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载。
注意
注意,这个插件只有在Webpack5才可以用,其他版本用不了。
官方推荐的用法描述:
推荐
production环境的构建将CSS从你的bundle中分离出来,这样可以使用CSS/JS文件的并行加载。 这可以通过使用mini-css-extract-plugin来实现,因为它可以创建单独的CSS文件。 对于development模式(包括webpack-dev-server),你可以使用 style-loader,因为它可以使用多个 标签将CSS插入到DOM中,并且反应会更快。
使用示例如下(结合sass预编译语言):
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const devMode = process.env.NODE_ENV !== "production";
module.exports = {
module: {
rules: [
{
test: /\.(sa|sc|c)ss$/,
use: [
devMode ? "style-loader" : MiniCssExtractPlugin.loader,
"css-loader",
"postcss-loader",
"sass-loader",
],
},
],
},
plugins: [].concat(devMode ? [] : [new MiniCssExtractPlugin()]),
};完整文档见:webpack 中文文档 (docschina.org)。
CompressionWebpackPlugin 插件
首先介绍下g-zip:
gzip 是 GNUzip 的缩写,最早用于 UNIX 系统的文件压缩。HTTP 协议上的 gzip 编码是一种用来改进 web 应用程序性能的技术,web 服务器和客户端(浏览器)必须共同支持 gzip。目前主流的浏览器,Chrome,firefox,IE 等都支持该协议。常见的服务器如 Apache,Nginx,IIS 同样支持 gzip。
gzip 压缩比率在 3 到 10 倍左右,可以大大节省服务器的网络带宽。而在实际应用中,并不是对所有文件进行压缩,通常只是压缩静态文件
详细的原理不过多介绍,可以看看:你真的了解 gzip 吗? - 知乎 (zhihu.com)。
我们可以利用compression-webpack-plugin插件来对产物进行gZip压缩。基本的用法如下:
const CompressionPlugin = require("compression-webpack-plugin");
module.exports = {
plugins: [
new CompressionPlugin({
filename: "[path][base].gz",
algorithm: "gzip",
// 要压缩的文件(正则)
test: /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i,
// 最小文件开启压缩
threshold: 10240,
minRatio: 0.8,
}),
],
};打包部署后,还需要在Nginx的配置文件中增加以下配置:
server{
# ....
gzip on;
gzip_buffers 32 4K;
gzip_comp_level 6;
gzip_min_length 100;
gzip_types application/javascript text/css text/xml;
gzip_disable "MSIE [1-6]\."; #配置禁用gzip条件,支持正则。此处表示ie6及以下不启用gzip(因为ie低版本不支持)
gzip_vary on;
}关于这个插件的其他配置项,见CompressionWebpackPlugin | webpack 中文文档 (docschina.org)。
其他一些优化的小技巧
这部分记录的小技巧比较零碎,也懒得展开说,所以直接堆在这里了:
配置
resolve.modules:可以配置resolve.modules,指定webpack查找模块的路径。通过减少查找路径,可以提高构建速度。配置
resolve.alias:可以配置resolve.alias,将一些常用的模块路径指定为别名,加快构建速度。充分利用
tree shaking和代码分割:通过减少不必要的代码,只引入项目中使用到的模块,可以减少产物体积和构建时间。
