前言:相信绝大部分前端工程师对webpack基本配置已经是烂熟于心了,所以webpack系列的第一篇,我们基于构建优化应用打包开始,然后一点一点补充完成整个webpack体系。
而使用webpack打包躲不开的就是webpack优化这个话题,无论是面试还是实际开发,优化都是非常重要的事情,毕竟提升用户体验是我们前端工程师的职责
基于构建策略优化应用打包
本篇将从3个大方向进行webpack优化,分别是:减少打包体积、优化构建时间、优化用户体验。
减少打包体积
splitChunks
最初,chunks(以及内部导入的模块)是通过内部 webpack 图谱中的父子关系关联的。CommonsChunkPlugin 曾被用来避免他们之间的重复依赖,但是不可能再做进一步的优化。
开箱即用的 SplitChunksPlugin 对于大部分用户来说非常友好。
默认情况下,它只会影响到按需加载的 chunks,因为修改 initial chunks 会影响到项目的 HTML 文件中的脚本标签。
webpack 将根据以下条件自动拆分 chunks:
- 新的 chunk 可以被共享,或者模块来自于
node_modules文件夹 - 新的 chunk 体积大于 20kb(在进行 min+gz 之前的体积)
- 当按需加载 chunks 时,并行请求的最大数量小于或等于 30
- 当加载初始化页面时,并发请求的最大数量小于或等于 30
当尝试满足最后两个条件时,最好使用较大的 chunks。
Tree Shaking
tree shaking - 摇树优化 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块语法的静态结构特性,例如import 和export。这个术语和概念实际上是由 ES2015 模块打包工具rollup 普及起来的。
在webpack中只需将打包环境设置为生产环境就能让摇树优化生效,同时业务代码使用ESM编写,使用import导入模块,使用export导出模块。
按需加载
将路由页面/触发性功能单独打包为一个文件,使用时才加载,好处是减轻首屏渲染的负担。因为项目功能越多其打包体积越大,导致首屏渲染速度越慢。
首屏渲染时只需首页的JS代码而无需其他页面的JS代码,所以可用按需加载实现。webpack v4+提供模块按需切割加载功能,配合import()可做到首屏渲染减包的效果,以加快首屏渲染速度。只有当触发某些功能时才会加载当前功能的JS代码。
作用提升
分析模块间依赖关系,把打包好的模块合并到一个函数中,好处是减少函数声明与内存花销。作用提升首次出现于rollup,是rollup的核心概念,后来在webpack v3中借鉴过来使用。
在未开启作用提升前,构建后的代码会存在大量函数闭包。因为模块依赖,通过webpack打包后会转换为IIFE,大量函数闭包包裹代码会导致打包体积增大,模块越多越明显。在运行代码时创建的函数作用域变多,导致更大的内存开销。
在开启作用提升后,构建后的代码会根据引入顺序放到一个函数作用域中,通过适当重命名某些变量以防止变量名冲突,以减少函数声明与内存花销。
在webpack中只需将打包环境设置为生产环境就能让作用提升生效,或显式设置concatenateModules。
压缩资源
压缩HTML/CSS/JS代码,压缩字体/图像/音频/视频,好处是更有效减少打包体积。极致地优化代码都有可能不及优化一个资源文件的体积更有效。
针对CSS/JS代码,分别使用以下插件开启压缩功能。其中OptimizeCss基于cssnano封装,Uglifyjs与Terser都是webpack官方插件,同时需注意压缩JS代码需区分ES5与ES6。
- optimize-css-assets-webpack-plugin:压缩
CSS代码,在webpack v5中请使用css-minimizer-webpack-plugin代替 - uglifyjs-webpack-plugin:压缩
ES5版本的JS代码 - terser-webpack-plugin:压缩
ES6版本的JS代码
优化构建时间
thread-loader
配置Thread将Loader单进程转换为多进程,好处是释放CPU多核并发的优势。在使用webpack构建项目时会有大量文件需解析与处理,构建过程是计算密集型的操作,随着文件增多会使构建过程变得越慢。
在Node中运行的webpack是单线程模型。简而言之,就是webpack待处理的任务需一件件处理,不能同一时刻处理多件任务。
文件读写与计算操作无法避免,能不能让webpack同一时刻处理多个任务,发挥多核CPU电脑的威力以提升构建速度呢?thread-loader来帮你,根据CPU个数开启线程。
在 worker 池中运行的 loader 是受到限制的。例如:
- 这些 loader 不能生成新的文件。
- 这些 loader 不能使用自定义的 loader API(也就是说,不能通过插件来自定义)。
- 这些 loader 无法获取 webpack 的配置。
每个 worker 都是一个独立的 node.js 进程,其开销大约为 600ms 左右。同时会限制跨进程的数据交换。
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /.js$/,
include: path.resolve('src'),
use: [
"thread-loader",
// 耗时的 loader (例如 babel-loader)
],
},
],
},
};
// with options
use: [
{
loader: "thread-loader",
// 有同样配置的 loader 会共享一个 worker 池
options: {
// 产生的 worker 的数量,默认是 (cpu 核心数 - 1),或者,
// 在 require('os').cpus() 是 undefined 时回退至 1
workers: 2,
// 一个 worker 进程中并行执行工作的数量
// 默认为 20
workerParallelJobs: 50,
// 额外的 node.js 参数
workerNodeArgs: ['--max-old-space-size=1024'],
// 允许重新生成一个僵死的 work 池
// 这个过程会降低整体编译速度
// 并且开发环境应该设置为 false
poolRespawn: false,
// 闲置时定时删除 worker 进程
// 默认为 500(ms)
// 可以设置为无穷大,这样在监视模式(--watch)下可以保持 worker 持续存在
poolTimeout: 2000,
// 池分配给 worker 的工作数量
// 默认为 200
// 降低这个数值会降低总体的效率,但是会提升工作分布更均一
poolParallelJobs: 50,
// 池的名称
// 可以修改名称来创建其余选项都一样的池
name: "my-pool"
},
},
// 耗时的 loader(例如 babel-loader)
];
缓存副本
配置cache缓存Loader对文件的编译副本,好处是再次编译时只编译修改过的文件。未修改过的文件干嘛要随着修改过的文件重新编译呢?
大部分Loader/Plugin都会提供一个可用编译缓存的选项,通常包括cache字眼。以babel-loader与eslint-webpack-plugin为例。
export default {
// ...
module: {
rules: [{
// ...
test: /\.js$/,
use: [{
loader: "babel-loader",
options: { cacheDirectory: true }
}]
}]
},
plugins: [
new EslintPlugin({ cache: true })
]
};
优化用户体验
模块懒加载
如果不进行模块懒加载的话,最后整个项目代码都会被打包到一个js文件里,单个js文件体积非常大,那么当用户网页请求的时候,首屏加载时间会比较长,使用模块懒加载之后,大js文件会分成多个小js文件,网页加载时会按需加载,大大提升首屏加载速度
合理配置hash
我们要保证,改过的文件需要更新hash值,而没改过的文件依然保持原本的hash值,这样才能保证在上线后,浏览器访问时没有改变的文件会命中缓存,从而达到性能优化的目的
// webpack.base.js
output: {
path: path.resolve(__dirname, '../dist'),
// 给js文件加上 contenthash
filename: 'js/chunk-[contenthash].js',
clean: true,
},
小结
最后,让我们一起加油吧!
都看到这了,不如顺手点个赞再走 ( *ˇωˇ* )