最近给项目进行webpack优化,尝试过几乎所有方法,一共26条,列举在此。
优化webpack,首先明确优化目标:
- 构建速度: 开发环境项目的启动速度,以及生产环境项目的打包速度。构建时间越短,速度越快越好。
- 增量构建速度:开发项目时,每修改一次代码,webpack会对修改的部分进重新构建。增量构建时间越短,速度越快越好。
- 打包体积:开发环境webpack打包后,生成的文件体积。打包体积越小越好
- 加载体积: 除关注打包体积外,浏览器打开页面,加载资源的体积也很重要。按需加载、缓存等技术可以减少加载体积。加载体积越小越好。
减少依赖包,缩小打包体积,构建速度会更快。使用压缩,打包体积会减小,但构建速度则变慢。讨论webpack的优化,速度和体积需同时考虑,二者有时正相关,有时负相关,需要均衡速度和体积。
下文中每个优化建议,都会列出优化目标,影响明显的会加粗。除速度和体积,还有: 分析, 即帮助分析webpack打包性能,呈现打包时间等信息;错误追踪,webpack打包后的文件与源文件不同,需要特殊的处理以方便错误定位和调试。
webpack 在生产环境更关注打包体积,而开发环境更关注打包时间,生产环境和开发环境使用不同的策略,下文每一个优化建议都列出使用环境。
webpack及相关打包工具在不断跟新优化中,不同版本可能会有很大不同,请以官方文档为准。本文以webpack4.42为准,与webpack3和webpack5区别较大的地方会标注版本变动。
1. 量化打包速度
环境:开发,生产
目标:分析,构建速度,增量构建速度
对webpack进行优化,首先要测量webpack打包耗时,发现问题所在。
- 使用
speed-measure-webpack-plugin
,快速测量各插件和loader的耗时。
speed-measure-webpack-plugin使用非常简单,直接包裹webpack配置即可
// npm i -D speed-measure-webpack-plugin
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin")
const smp = new SpeedMeasurePlugin()
const webpackConfig = smp.wrap({
// ... webpack config
})
speed-measure-webpack-plugin对webpack 4的兼容性不好,一些插件可能会发生错误,需暂停发生错误的插件。
-
使用
profilingPlugin
, 创建构建性能报告。profilingPlugin
可以生成一份JSON文件,上传到chrome devtool的performence面板, 可以看到详细的编译过程,寻找性能瓶颈:const webpack = require('webpack') module.exports = { //... plugins: [ new webpack.debug.ProfilingPlugin( { // 产生出的文件位置,相对于根目录 outputPath: 'stats/profileEvents.json' }) ] }
上传到chrome,出现漂亮的时间线,可供分析:
webpack多进程运行时,插件和loader的运行时间较难观察,比如各插件的运行时间相加会大于总时间,loader的先后顺序可能不对。使用单进程可以保证有效的测试数据。测速时请将 parallelism
配置项设为 false
(默认为 true
),确保webpack单线程运行:
module.exports = {
//...
parallelism: false
}
除调试webpack外,其他情况请开启多进程。
2. 量化打包体积
环境:开发,生产
目标:分析,打包体积,加载体积
可以通过 webpack-bundle-analyzer
查看打包后的体积。探究某个依赖是否过大,是否存在重复的依赖,是否有功能类似的依赖,是否进行有效的代码分离。
// npm i -D webpack-bundle-analyzer
const BundleAnalyzerPlugin =
require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = {
//...
plugins: [
new BundleAnalyzerPlugin({
defaultSizes: 'gzip' // 设置默认尺寸为'gzip'
})
)
]
}
webpack-bundle-analyzer
显示的尺寸有三种:
-
stat
: 未经Uglify、Terser 等插件最小化的体积。最小化js就是我们常说的压缩js,最小化(minify)是指去除注释、简化变量名等操作压缩体积,而压缩是通过gzip等算法压缩体积。 -
parsed
: 经Uglify、Terser 等插件最小化后的体积。 -
gzip
: 经过gzip压缩后的尺寸。
线上一般按照 gzip
进行数据传输,所以选择 gzip
作为衡量体积的标准。
3. 查看打包详情
环境:开发,生产
目标:分析,构建速度,增量构建速度,打包体积,加载体积
除查看打包体积和时间,有时需要更多打包信息,比如模块的依赖关系,可以用如下方式:
-
配置
profile
选项,生成详细打包报告。配置profile
选项后,每次打包会生成一个json文件,里面有打包的详细信息,也可通过stats-webpack-plugin
进行更多设置:// npm i -D stats-webpack-plugin const StatsPlugin = require('stats-webpack-plugin') module.exports = { //... profile: true // 简单的配置,将在打包文件输出报告 pulgins: [ // 更灵活的配置 new StatsPlugin( '../../stats/stats.json', { exclude: [/node_modules/], }) ] }
将产生的json文件上传至https://webpack.github.io/analyse/ 官方分析工具,查看可读的报告:
-
打包后,在terminal终端中显示打包详情。可以通过
stats
选项配置terminal中的输出结果,如:module.exports = { //... stats: 'verbose' // 将在terminal中呈现全部输出 };
每次打包后,在终端会显示:
`stats` 的可选址值有:
* `none`: 没有输出
* `errors-only` :只显示错误信息,**使用此选项,可提高webpack编译速度**
* `minimal`: 显示错误信息,或者新的输出
* `normal` : 标准输出
* `verbose` : 全部输出,建议用 `profile` 选项输出成文档。
可以个性化配置输出信息。使用Node.js API 时此配置无效,使用 `webpack-dev-server` 时,请将此选项需放入 `devServer` 中。。
Jarvis、webpack-dashboard、webpack visualizer等非官方工具也可提供打包信息,帮你分析webpack编译。
4. 使用最新版本
环境:开发,生产
目标: 构建速度,增量构建速度,打包体积,加载体积
工欲善其事,必先利其器。使用最新的版本,保证最快的速度。请检查node、 webpack、插件、 loader等的版本号,使用最新版本。推荐一款工具 npm-check
, 只要在项目根目录运行:
npm-check -u
即查看哪些包有更新:
5. 删除不必要的插件、loader等工具
环境:开发,生产
目标:构建速度,增量构建速度
任何插件和loader的启用都占用时间,使用最少的插件和loader可以减少webpack的打包时间。
- 一些工具,只对生产阶段有效,比如压缩、CSS分离、hash,请在开发环境删除。
- 一些工具,比如
progress-bar-webpack-plugin
进度提示条,请衡量对你是否真的有用,每增加一个插件都会让打包更慢。 - 对于
speed-measure-webpack-plugin
,webpack-bundle-analyzer
这类量化插件,只在调试webpack时使用,在其他时候请删除。 - 在webpack打包时,会在terminal中输出moudle、chunk等打包信息,生成这些信息会消耗一定的时间。建议设置为
stats: 'errors-only'
或者stats: 'minimal'
。
6. 设置正确的模式mode
环境:开发,生产
目标: 构建速度,增量构建速度,构建体积,加载体积
版本变动:webpack3
webpack提供开箱即用的配置环境,通过 mode
属性告知webpack你的使用环境,webpack将自动开启一系列的优化。
- 生产环境,请设置
mode:'production'
- 开发环境,请设置
mode: 'development'
module.exports = {
//...
mode: 'production' // 生产环境,也是默认值
mode: 'development' // 开发环境
}
设置 mode
后,webpack自动将 process.env.NODE_ENV
设为相应的值(生产为 production
, 开发为 develpment
),react等包根据此值进行优化,所以不要将process.env.NODE_ENV
改为其他值。
webpack3没有 mode
选项,需要用 DefinePlugin
手动设置process.env.NODE_ENV
,明确生产或者开发环境 。
webpack自动启用的优化,如无需配置,本文不再介绍。
7. 启用多进程,并行编译
环境:开发,生产
目标: 构建速度,增量构建速度
webpack默认是单进程编译,不利于发挥多核CPU的威力。可以采取些措施,使用多进程,这将大幅提高打包速度:
对于
TerserWebpackPlugin
等插件,本身支持多进程,默认开启,也可通过parallel
选项进行个性化配置。-
对于其他loader,可以使用
happyPack
或者thread-loader
进行并行编译。这两个功能类似,对打包速度的提升也类似。对于babel-loader
尤其需要并行编译,提高打包速度。happyPack
使用的人较多,使用限制较少, 但其配置较复杂,例如:// npm i -D happypack const HappyPack = require('happypack') module.exports = { // ... module: { loaders: [{ test: /\\.js$/, loader: 'babel-loader', happy: { id: 'js' } }] }, plugins: [ new HappyPack({ id: 'js' }) ] }
thread-loader
有些使用限制,但配置非常简单:// npm i -D threa-loader module.exports = { // ... module: { rules: [ { test: /\\.jsx?$/, use: ['thread-loader', 'babel-loader'] } ] } }
thread-loader
将其后的loader 放在单独的池中运行,要求loader不能产生新的文件、不能使用插件、无法获取webpack选项。
开启多进程会消耗大量时间,各进程之间的通信也非常耗时。请仅对长耗时的loader启用。进程数过多会导致打包变慢,进程的默认数通常是CPU数-1,即 os.cpus().length - 1
。
8. 使用持久缓存
环境:开发,生产
目标:构建速度,增量构建速度
版本变动:webpack5
webpack构建一次耗费很长时间,如果将构建的结果进行缓存,第一次构建的时间可能略有增加,但之后的构建时间将大幅度缩短,这种技术叫持久缓存。
-
建议使用
hardSourcePlugin
进行全局缓存,开箱即用,无需配置,使用效果非常好,能减少50%以上的构建时间。hardSourcePlugin
在使用中可能存在一些问题,可能导致CSS分离失效,如有问题请查阅官方文档。使用hardSourcePlugin
后,不需再使用cache-loader
或者DLL plugin
等缓存工具。hardSourcePlugin
配置示例:// npm i -D hard-source-webpack-plugin const HardSourceWebpackPlugin = require('hard-source-webpack-plugin') module.exports = { // ... plugins: [ new HardSourceWebpackPlugin () ] }
对于
TerserWebpackPlugin
,请确保缓存开启,即cache
不为false
。hardSourcePlugin
对TaresePlugin似乎没有效果,需单独处理。对于webpack5,请直接配置
cache: { type: "filesystem” }
,不再需要hardSourcePlugin
等缓存工具,TarserPlugin
自带的缓存特性也会失效,无需开启。
另外一种缓存技术叫做”长期缓存" (long-term caching), 是指浏览器缓存一些资源,可以更快的打开页面,需与“持久缓存”相区别,后文有更多介绍。
9. 选择合适的devTool
环境:开发,生产
目标:构建速度、增量构建速度
webpack打包后的代码是经过压缩处理,缺少可读性。devTool可以控制代码的输出品质,添加 source-map
,增加代码可读性,方便错误定位。注意代码输出品质的提高,会使打包速度变慢,需均衡选择。
module.exports = {
//...
devTool: 'source-map' // 产生源码品质的source-map,有暴露源码风险,不建议使用
}
生产环境常用的值:
-
none
:不使用devtool,速度最快,无法定位错误,是生成环境默认值。 -
hidden-source-map
:产生source-map
, 需要手动加载到浏览器devtool,或使用其他工具加载,才可查看源码和追踪错误,推荐使用。 -
nosources-source-map
:无法看到源码,但能在浏览器devtoo看到错误栈,错误发生的准确位置、行号等信息。这是线上环境最简单的方案,但暴露源码结构。
开发环境常用的值:
-
eval
: 源码被放入eval
函数中,缺少可读性,只知道错误的大概位置,没有错误行号、列号提示,构建速度最快,这是开发环境默认值。 -
eval-cheap-source-map
: 看到近似源码的代码,有错误行号提示,没列号提示。较快的构建速度和增量构建速度。 -
eval-cheap-module-source-map
: 能看到源码,有行号错误提示,没列号提示。较慢的构建速度和较快的增量构建速度,推荐此选项。 -
eval-source-map
: 能看到源码,并有行号和列号的错误提示。很慢的构建速度和一般的增量构建速度。
devTool
还有其他配置值,需均衡考虑构建速度和错误定位,并防止源码在生产环境中暴露。你可以用 SourceMapDevToolPlugin
代替 devTool
进行个性化配置。
如未能生成 source-map
,检查一些插件或者loader是否配置 sourceMap: true
,这包括postcss-loader、resolve-url-loader、TeserWebpackPlugin、OptimizeCSSAssetsPlugin 等。
10. 合理配置loader的解析范围
环境:开发,生产
目标:构建速度,增量构建速度
一些loader,比如 babel-loader
转换JS的过程是非常耗时的,如果能减少loader处理的文件数量,会大大减少编译时间。
可以通过 test
正则匹配文件, include
指定要包含的文件, exclude
指定要排除的文件。优先级为: exclude
> include
> test
,例如:
module.exports = {
//...
module: {
rules: [
{
test: /\\.styl$/,
include: [
path.resolve(__dirname, 'app/styles'),
path.resolve(__dirname, 'vendor/styles')
],
// 在app/styles 中排除掉 app/styles/common
exclude: [path.resolve(__dirname, 'app/styles/commen')],
use: ['style-loader', 'css-loader', 'stylus-loader']
}
]
}
}
顺便提下loader的执行顺序,列表中较后的选项先执行,比如在上例中,先执行 stylus-loader
解析.style,再执行 css-loader
解析CSS,最后执行 style-loader
将CSS内联处理。
11. 忽略jquery等模块的解析
环境:开发,生产
目标:构建速度、增量构建速度
jquery, lodash等是独立的、不需要外部依赖即可运行的模块。可以使用 module.noParse
避免对这些库进行解析,以提高构建性能。配置方法如下:
module.exports = {
//...
module: {
noParse: /jquery|lodash/,
}
};
module.noParse
配置后,将忽略相关模块中 import
、 reuqire
、 define
的调用,请确保配置的模块没有其他依赖。
module.noParse
只是不对jquery等模块进行解析(如通过babel-loader转换js文件),打包后的文件中仍包含jquery的代码。 module.noParse
的优先级高于上文中对loader的include
,test
配置。
12. 使用 externals
引入公共CDN
环境:开发,生产
目标:构建速度、增量构建速度
可以利用公共CDN服务加载包,而不用webpack打包。如下配置,将 jquery
变量挂载在 windows
变量上:
module.exports = {
//...
externals: {
'jquery': 'jQuery'
}
};
当然,要在HTML代码中引入jquery:
<script src="<http://libs.baidu.com/jquery/2.0.0/jquery.min.js>"></script>
webpack打包后的文件中将不包含jquery的代码。
如果你的多个应用使用相同的依赖,可以将公共依赖分离,再用此种方式单独部署使用。
13. 合理配置 reslove
字段,缩小webpack寻找模块的范围
环境: 开发、生产
目标:构建速度、增量构建速度
resolve
配置webpack如何查找模块,例如 import 'vue'
,resolve
告诉webpack在哪里找到vue。更精准的配置将减小webpack搜索模块的时间。
module.exports = {
//...
resolve: {
modules: ['node_modules'], // 仅在node_modules目录寻找模块
extensions: ['.js', '.json'], // import 'utils' 类似于 import 'utils.js'
mainFields: ['loader', 'main'], // 通过package.json 的loader、main 选项找到入口文件名
mainFiles: ['index'], // 设index为入口文件名
symlinks: false, // 忽略npm link
alias: {
// import 'Pages' 将等同于 import 'src/pages'
Pages: 'src/pages',
// webpack直接确定react模块位置,不需要层层查找
react: patch.resolve(__dirname, './node_modules/react/dist/react.min.js')
}
}
};
-
resolve.modules
:告诉webpack搜索模块的范围。 -
resolve.mainFields
和resolve.mainFiles
:告诉webpack模块的入口文件。 -
resolve.extensions
:如果引入文件时不指定文件类型后缀,将通过此字段查找相应的文件类型。 -
resolve.symlinks
:如果不使用NPM link
,可将此设为false
。 -
resolve.alias
:设置别名,这将提高代码的可读性,并加快模块的寻找速度。
如果选项是列表的,请尽量减少列表项,并将高频值写在前面, 如 extensions: ['.js', '.json']
中 js
在前。
未掌握 reslove
各选项含义前,请使用默认值, reslove
对webpack编译的提速不明显,但错误的配置可能导致一些bug,比如明明存在某个模块但webpack无法找到该模块。
14. 使用ES module,保证tree-shaking删除无用代码
环境:开发,生产
目标:构建体积
版本变动:webpack3
webpack4支持tree-shaking,可以删除未使用的代码,请使用ES module的导入方式,确保tree-shaking的有效,例如:
// common.js
const run = () => {console.log('run')}
const fly = () => {console.log('fly')}
export {run, fly}
// index.js
import {run, fly} from './common'
run()
如此,fly将会从代码中删除。按下述导入方式,tree-shaking不会生效:
// common.js 同上
// index.js
import common from './common'
common.run()
// common 依赖将被整个打包,无法实现tree-shaking
由于JS动态编译的特性,tree-shaking的效果有限。对于很多依赖,webpack无法很好的执行tree-shaking。另外webpack3不支持tree-shaking。
在开发其他项目的依赖包时,可考虑在 package.js
中添加 sideEffects:false
,其他项目在导入你的包时,会尝试进行tree-shaking。
15. 使用TerserWebpackPlugin
最小化JS
环境:生产
目标:构建速度,打包体积
TerserWebpackPlugin
可以最小化JS,即压缩js,并且支持最小化ES6,webpack4生产模式下自动启用,你也可以进行一些配置:
- 请确保
parallel
多进程开启,提高打包速度。 - 像loader一样,通过
test
,include
,exclude
缩小压缩文件范围。也可以用chunkFilter
排除一些chunk。 - 非webpack5版本,请确保
cache
缓存开启,webpack5中此选项失效。 - 如果需要source-map,请配置
sourceMap
为true
,这将生成source-map。注意cheap-source-map
等包含cheap
的source-map 不会产生source-map。请使用hidden-source-map
或者nosources-source-map
。
配置示例如下:
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
exclude: /\\/excludes/,
chunkFilter: (chunk) => {
return chunk.name !== 'vendor' // 排除名为 `vendor` 的chunk
},
parallel: true, // 多进程,默认开启
cache: true, // 持久缓存,默认开启
sourceMap: true, // 支持产生source-map,默认不开启
terserOptions: {
//... 设置terser的压缩选项,比如是否清除注释,详见官方文档
}
}),
],
},
};
不要使用 UglifyjsWebpackPlugin
,此插件不支持ES6,并已经停止维护。
16. 使用 optimize-css-assets-webpack-plugin
最小化CSS
环境:生产
目标:打包体积
使用 optimize-css-assets-webpack-plugin
最小化CSS,减少CSS代码的体积。webpack4 中不包含此插件,需先安装。
// npm i -D optimize-css-assets-webpack-plugin
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
module.exports = {
optimization: {
minimizer: [
new OptimizeCSSAssetsPlugin({})
],
},
};
17. 优化图片
环境:生产
目标: 打包体积
图片是网站重要的一部分,不仅体积大,而且数量多,占用大量带宽,下面是优化图片的一些方法:
- 使用
url-loader
将小尺寸图片进行内联。可以配置limit
选项,将小于此值的图片转换成base64 格式并存在JS代码中,以节少请求数量。file-loader
有和url-loader
类似的功能,可以解析静态文件,但不能设置limit
属性进行内联。 - 使用
svg-loader
将svg图片进行内联。此插件和url-loader
类似,但SVG是文本,压缩体积上更有效率。 - 雪碧图可将多张小图片组装合成一张大图片,减少请求数量。由于雪碧图配置较复杂,不推荐使用。可以利用
webpack-spritesmith
插件实现雪碧图。 - 使用
image-webpack-loader
压缩图片。它支持JPG,PNG,GIF和SVG等几乎所有格式。 它不会将图片内联,需与url-loader
等配合使用,即先压缩图片,再进行内联。可以通过enforce: 'pre'
确保在其他loader前执行。
配置示例如下:
// npm i -D image-webpack-loader
// npm i -D url-loader
// npm i -D svg-url-loader
module.exports = {
module: {
rules: [
// 压缩图片
{
test: /\\.(jpe?g|png|gif|svg)$/,
loader: 'image-webpack-loader',
enforce: 'pre', // 在其他loader调用前,先行调用,避免重复代码
},
// 内联图片
{
test: /\\.(jpe?g|png|gif)$/,
loader: 'url-loader',
options: {
limit: 10 * 1024, // 小于10kb的会被内联
},
}
// 内联svg
{
test: /\\.svg$/,
loader: 'svg-url-loader',
options: {
limit: 10 * 1024, // 小于10kb的会被内联
noquotes: true, // 删除引号
iesafe: true, // 支持IE,但会增加体积
},
},
],
},
};
18. 优化第三方依赖包
环境: 开发,生产
目标: 构建速度、增量构建速度、打包体积
几乎一半以上的JS体积来源于第三方依赖包,如果能优化依赖包体积,会大幅减少构建体积。我们通常只用到包的几个方法,但却引入包的全部内容。比如,我们只在中文环境使用Moment.js,却引入了Moment.js的各国语言包,使得项目臃肿。
Googel 在Github repo上收集了一些优化建议,其中包括babel-loard、loadsh、react、bootstrap等的优化建议。请在github上搜索“webpack-libs-optimizations”查看。
19. 对html-webpack-plugin的优化
环境:生产
目标:打包体积
hteml-webpack-plguin
可以很方便的创建入口html文件,但它对自定义模板默认不开启压缩的,请配置 minify
选项,进行最小化代码
// npm i -D html-webpack-plugin
// npm i -D uglifyjs-webpack-plugin 压缩js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')
module.exports = {
// ...
plugins: [
new HtmlWebpackPlugin({
// 使用'ejs'后缀防止模板文件被html—loader 解析
template: path.join(__dirname, 'index.ejs'),
filename: 'index.html',
minify: { // 开启最小化操作
collapseWhitespace: true,
removeComments: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
useShortDoctype: true,
minifyCSS: true,
minifyJS: true, // 使用uglifyjs进行压缩,所以需要安装相关依赖
}
})
]
}
20. 优化Node核心库的polyfill
环境:生产
目标:打包体积,加载体积
版本变动:webpack5
webpack打包时会自动加入Node环境的polyfill,所以在浏览器中也可使用Node核心库,比如 process.env.node_env
,这会略微增加打包的体积。如果不需要浏览器使用Node的核心库,请配置:
module.exports = {
// ...
node: false
}
你也可以对每一个核心库进行精确配置,以下是webpack4的默认配置值(webpack5有差别):
module.exports = {
// ...
node: {
console: false,
global: true,
process: true,
__filename: "mock",
__dirname: "mock",
Buffer: true,
setImmediate: true
}
}
其中的 console
,global
,process
等为node核心库中的相应功能,更多选项需查找 node
核心库。配置的值的含义如下:
-
true
:提供polyfill。 -
mock
:提供 mock 实现预期接口,但功能很少或没有。 -
empty
:提供一个空对象。 -
false
:不提供polyfill,如果使用相应的node库,会触发错误。
21. 分离代码
环境:生产
目标:打包体积,加载体积
版本变动:webpack3,webpack5
webpack将各种资源打包成一个JS文件,合并请求,优化用户体验。现在的web应用体积越来越大,单一的js文件体积过大,需拆分成多个js文件,减少加载体积。这也是懒加载、长久缓存等技术使用的基础。分离出的每一个js文件,称之为chunk。
webpack3使用 CommonsCunksPlugn
配置复杂,webpack4提供开箱即用的 optimization.splitChunks
。生产环境自动启用 optimization.splitChunks
,默认配置足以应对大多数情况,不需要专门配置。默认配置遵照的主要规则:
- 主入口会打包成单独的chunk。
- 动态导入的每一个模块,会打包成一个chunk。例如按路由分离3个页面,将会打包出3个chunk。
- 每个chunk中用到的node_modules依赖,会被打单独打包成以
vendors~
开头的chunk - 如果两个chunk中包含相同的内容,相同的内容会被单独打包成一个共享chunk。
比如有个项目:
- index 入口页,用到react,index组件
- about 介绍页,用到react,about组件,common组件
- detail 详情页,用到angular,detail组件,common组件
将打包成:
- index.js: react, index组件
- about~detail.js: common组件
- about.js: about组件
- detail.js: detail组件
- vendors~detail.js: angular
如修改默认规则,请详细阅读官方文档,并注意评估打包结果,防止负优化,例如:
module.exports = {
//...
optimization: {
splitChunks: {
//... 一些配置
}
}
}
webpack5对optimization.splitChunks
进行一些优化,配置略有不同。
22. 分离CSS
环境:生产
目标:打包体积,加载体积
版本变动:webpack3
在分离JS的基础上,有时需分离CSS,以实现CSS的按需加载。在webpack3 时使用 ExtractTextWebpackPlugin
, 在webpack4 时请使用 mini-css-extract-plugin
:
// npm i -D mini-css-extract-plugin
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: '[hash].css',
chunkFilename: '[contenthash].css' // 注意使用contenthash
})
],
module: {
rules: [
{
test: /\\.css$/i,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
esModule: true,
hmr: process.env.NODE_ENV === 'development' // 热响应
},
},
'css-loader',
],
},
],
},
};
CC将分离成一个个chunk。在开发阶段可以考虑不分离CSS,以提高打包速度。
23. 使用按需加载,懒加载,预加载
环境:生产
目标:加载体积
版本变动:webpack5
现在网页应用功能越来越多,尺寸越来越大,我们可以只加载用户用到的部分,而不加载其他部分以提高性能。比如只加载首屏需要的chunk,而其他部分的chunk暂时不加载。
我们可以使用上文所说的 split-chunk
分离代码,并通过动态导入实现按需加载/懒加载。Vue,React等都提供动态导入组件的方案,也可以用 function import() 实现懒加载,比如:
import(/* webpackChunkName: "lodash" */ 'lodash')
webpack5使用更加智能的chunk命名方式,不需要诸如 /* webpackChunkName: "lodash" */
的代码。
对于分离的代码,我们也可在空闲时间,将其预先加载,以提高用户体验,比如:
import(/* webpackPrefetch: true */ 'lodash')
24. 使用长久缓存
环境:生产
目标:加载体积
版本变动:webpack3,webpack5
浏览器打开应用时会下载资源,这会占用大量的时间。如果将一部分资源在浏览器中缓存,下次打开页面时不需要重下载,可以降低首屏渲染时间。
我们在修改项目时,通常只修改一部份,让浏览器仅更新修改部分即可,其余资源使用缓存。webpack打包后,会给每一个静态资源添加hash后缀,如果修改该资源,hash会发生改变,浏览器会重新加载该资源。如果该资源没被修改,hash会保持不变,浏览器会使用缓存,这种技术叫做“长久缓存”(long cache)。需与前文提到的"持续缓存”相区别,持续缓存是用于提高打包速度的。
webpack5对长久缓存进行大量优化,配置更为方便。下面的步骤限于webpack4版本,webpack3也需要类似复杂的配置,但略有不同。
长久缓存依靠 splitChunk
代码分离,请参考上文。
-
第一步,告知浏览器缓存文件
需要在服务器上设置缓存文件时长,比如通过
Cache-Control
// Server header Cache-Control: max-age=31536000 // 设置缓存一年时间
有多种设置缓存的方法,请查看相关文章
-
第二步,使用
recordsPath
配置项,方便调试在设置长久缓存时,我们需要知道webpack打包后的文件是如何划分的,模块id是否改变,
recordsPath
配置项可以产生一份json文件,记录每次打包后的结果。const path = require('path') module.exports = { // ... recordsPath: path.join( process.cwd(), // npm run build 在terminal中执行时的目录路径 'records.json' // 输出的文件名 ) }
-
第三步,为文件添加hash
需要为打包后的文件设置hash后缀,相当于给该文件添加一个版本号,告知浏览器该文件是否更新,比如设置
[name].[hash].js
。hash值有三种:-
[hash]
: webpack每次编译都会产生一个唯一的hash值,每次编译hash都会改变 -
[chunkhash]
: webpack每次编译,对每一个chunk(每一个js文件)生成不同的hash值,如果修改chunk,会重新计算chunkhash。如果没修改chunk,chunkhash不会变。对JS资源我们通常添加[chunkhash]
后缀。 -
[contenthash]
:webpack根据输出的文件内容,计算并生成contenthash,每次编译后都会根据内容生成contenthash,如果改文件内容不变,生成的contenthash是相同的。通常对图片、字体、样式等资源添加[contenthash]
后缀。
module.exports = { // ... output: { chunkFilename: '[chunkhash].js' // 为chunk添加chunkhash }, module: { rules: [ { test: /\\.ttf/, loader: 'file-loaser', options: { name: '[contenthash].ext'} //为字体添加contenthash } ] } }
由于不同的系统、计算机使用的hash计算方式不同,在不同的计算机对相同的项目打包,产生的hash值可能不同,导致长久缓存失效。
-
-
第四步,分离runtime
runtime记录所有的资源文件名和路径,以帮助浏览器找到最新版本的chunk,比如:
{ "0":"js/0.840dc3db.js", "common":"js/common.50055e90.js", "detail":"js/detail.dd333b62.js" //... }
通常每个chunk中都包含runtime信息,只要有一个chunk发生变化,runtime就会变化,进而使所有chunk发生变化。可以通过
optimization.runtimeChunk
将runtime分离为一个单独的chunk,我们将将这个chunk称为runtime.js
,有时也称为manifest.js
(比如vue-cli中)。module.exports = { // ... optimization: { runtimeChunk: { name: 'runtime' // 分离runtime chunk,并命名为 'runtime' }, } }
-
第五步,内联runtime文件
分离的runtime文件,将作为一个独立的js加载,浏览器的请求顺序变为:
index.html → runtime.js → 首屏chunk。
可以看出多了次请求,使用
inline-manifest-webpack-plugin
将runtime内联进html文件,浏览器的请求顺序修改为:index.html (包含runtime的代码) → 首屏chunk
也可以使用
webpack-manifest-plugin
实现同样功能,inline-manifest-webpack-plugin
的配置示例:// npm i -D inline-manifest-webpack-plugin const InlineManifestWebpackPlugin = require('inline-manifest-webpack-plugin') module.exports = { // ... plugins: [ new InlineManifestWebpackPlugin(), ], };
-
第六步,稳定moudle id
webpack会给每一个moudle分配一个id,id是按照顺序计算的,如 0,1,2。如果你删除了某个moudle,或者新增某个moudle,可能导致module id的顺序发生变化,比如:
moudleId 变动情况 0 原来的0 1 新增的module 2 原来的1, id变化 3 原来的2,id变化
moudle id的变化会导致chunk文件发生变化,我们可以用
hashed-module-ids-plugin
或者用optimization.moduleIds
来稳定moudle id:const webpack = require('webpack') module.exports = { // ... plugins: [ new webpack.HashedModuleIdsPlugin(), ] }
经过复杂的配置,终于可以实现长久缓存。
25. 使用性能预算,控制体积
环境:生产
目标:打包体积
项目初期的体积较小,但随项目发展,可能没注意到,安装的某个依赖,使项目打包体积急剧增加。使用性能预算,及时发现打包体积的增加。
配置 performance
即可启用性能预算,生成环境自动开启,默认配置如下:
module.exports = {
// ...
performance: {
hints: 'warning', // 尺寸过大后警告,
// 设为'errors'时体积过大将报错
// 设为false时关闭性能预算
maxEntrypointSize: 250000, // 最大入口体积
maxAssetSize:250000 // 单个资源最大体积
}
}
打包后,如果有体积过大,将显示:
使用 bundlesize
工具,可以进行更灵活的配置,比如单独设置某个文件的体积预算,同时支持自动化CI。
26. 其他优化建议
-
并行运行多个webpack实例 。打包多个版本,比如打包国内版本应用、国际版本应用,可以使用
parallel-webpack
并行运行多个webpack实例。 -
指定browserslist。指定合适的browserslist,babel将据此选择核合适的降级工具,有助于代码体积的减少和打包速度的提升。比如开发环境需要更快的打包速度,可以设为
'last 2 Chrome versions'
;生产环境需考虑兼容性,可设为'> 1%, ie >= 11, not dead'
。 - 考虑为不同的浏览器打包不同版本,可以为IE、现代浏览器分别打包,再根据浏览器的标识加载不同的版本。也可以根据浏览器的尺寸将图片打包成不同的尺寸,对PC端浏览器返回大图,而对移动端浏览器返回较小尺寸的图片。
-
如果担心打包后的文件包含多个相同模块,可以用
duplicate-package-checker-webpack-plugin
或者bundle-duplicates-plugin
进行检查。