进行优化的第一步需要知道我们的构建到底慢在那里。通过 speed-measure-webpack-plugin
测量构建期间各个阶段花费的时间,bundle-analyzer-plugin
可以查看构建包的bundle
的大小。我们可以根据相关信息去优化。
构建性能
缩小查找范围
exclude/include
确定 loader
的规则范围,避免不必要的转译
resolve
- 对庞大的第三方模块设置
resolve.alias
, 使webpack直接使用库的min文件,减少路径解析计算
js
resolve: {
alias:{
'react':patch.resolve(__dirname, './node_modules/react/dist/react.min.js')
}
}
- 限定文件扩展名(extensions)
js
resolve: {
extensions: ['.tsx', '.js', '.json'] // 按使用频率排序
}
- 避免无后缀文件的多重查找(如
import './data'
依次尝试.tsx → .js → .json
) - 高频扩展名前置可减少 5%-10% 的解析时间
module.noParse 减少模块解析
适用场景:处理未采用模块化标准且无依赖的第三方库(如 jQuery、Lodash)
原理:跳过对指定模块的 AST 解析和依赖分析
js
// webpack.config.js
module.exports = {
module: {
noParse:
// 正则表达式匹配
/jquery|lodash/,
// 或函数式匹配(Webpack 3+)
(content) => /jquery|lodash/.test(content)
}
};
效果:
- 减少 10%-20% 的构建时间(对大型库更明显)
- 需确保目标模块不含 require/import 等模块化语法,否则会导致运行时错
多线程操作
thread-loader(多进程Loader)
多线程运行loader
,也就是放置在这个loader
之后的 loader
就会在一个单独的 worker
池(worker pool
)中运行
注意:线程启动开销较大,小型项目优化效果不明显,如果大型项目(文件超过100个)再启动
多进程压缩JS文件
目前使用TerserPlugin
可开启parallel
属性,也可以使用ParallelUglifyPlugin
插件。
缓存
缓存loader的结果
我们可以基于一种假设:如果某个文件内容不变,经过相同的loader解析后,解析后的结果也不变
于是,可以将loader的解析结果保存下来,让后续的解析直接使用保存的结果
cache-loader
可以实现这样的功能
js
module.exports = {
module: {
rules: [
{
test: /.js$/,
use: ['cache-loader', ...loaders]
},
],
},
};
有趣的是,cache-loader
放到最前面,却能够决定后续的loader是否运行
持久化缓存
Webpack 5 引入了持久化缓存功能,可以显著提升构建性能。这个功能会将首次构建的结果保存到本地文件系统,在下次构建时,它会比对文件的哈希值或时间戳,如果文件没有变化,就直接使用缓存的结果,从而提升构建性能。
要开启 Webpack 的持久化缓存,你可以在 Webpack 配置 cache
设置:
js
module.exports = {
// ...其他配置...
cache: {
type: 'filesystem', // 将缓存存储在文件系统中,区别于默认的内存缓存
cacheDirectory: path.resolve(__dirname, '.webpack_cache'),// 可选:设置缓存文件存放的路径,默认为 node_modules/.cache/webpack
buildDependencies: {
config: [__filename], // 指定额外依赖;例如:当 webpack 配置文件本身发生变化时缓存将自动失效
},
},
};
DllPlugin(动态链接库)
减少基础模块编译次数,其原理是把网页依赖的基础模块抽离出来打包到dll文件中,当需要导入的模块存在于某个dll中时,这个模块不再被打包,而是去dll中获取。为什么会提升构建速度呢?原因在于dll中大多包含的是常用的第三方模块,如react、react-dom不会经常变动,只要编译打包过一次后续直接引用,避免反复编译。
- 打包公共模块,暴露变量名,生成资源清单
js
const webpack = require("webpack")
const path = require('path')
const ROOT_PATH = path.resolve(__dirname, '../')
const BUILD_PATH = path.resolve(ROOT_PATH, './dll')
module.exports = {
entry: {
//将 react、lodash等模块作为入口编译成动态链接库,可以理解为单独打包
lib: ['react', 'react-dom','lodash']
},
output: {
//指定路径
path: BUILD_PATH,
//指定文件名
//这个名称需要与 DllPlugin 插件中的 name 属性值对应起来
library: '_lib_[name]',
filename: 'dll.lib.js'
},
plugins: [
new webpack.DllPlugin({
//和output.library中一致,值就是输出的manifest.json中的 name值
name: '_lib_[name]',
path: path.resolve(BUILD_PATH, 'manifest.json')
})
]
}
- 利用
DLLReferencePlugin
根据资源清单文件去获取打包好的公共模块、并且动态将./static/dll
的js文件在html中引入即可
js
const webpack = require("webpack")
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');
module.exports = {
entry: {
//...
},
output: {
//...
clean: false, // 清理文件,热更新时也会清理dll引用,导致热更新再刷新浏览器异常...
},
plugins: [
new webpack.DllReferencePlugin({
manifest: './dll/manifest.json'
}),
new AddAssetHtmlPlugin(
{
filepath: './dll/dll.lib.js',
outputPath: './dll', //相对于 output.path 的相对路径。
publicPath: 'dll',
}
),
]
}
完整配置参考:
优化开发体验
watch
Webpack可以使用两种方式开启监听:
- 启动webpack时加上
--watch
参数; - 在配置文件中设置
watch:true
。
此外还有如下配置参数。合理设置watchOptions可以优化监听体验。
js
module.exports = {
watch: true,
watchOptions: {
ignored: /node_modules/,
aggregateTimeout: 300, //文件变动后多久发起构建,越大越好
poll: 1000, //每秒询问次数,越小越好
}
}
cross-env
运行跨平台设置和使用环境变量的脚本。
当我们使用在scripts
中使用 NODE_ENV = production
来设置环境变量的时候,mac上是可以正常运行,但是windows通常会报错。因此 cross-env
出现了, cross-env
提供一个可兼容多平台的设置环境变量的scripts
。
json
//package.json
"scripts": {
"dev": "cross-env NODE_ENV=development webpack-dev-server --config webpack/webpack.dev.js --open"
},
//webpack.config.js
console.log(process.env.NODE_ENV)//development
当你在不同设备运行 npm run dev
时,其都可以正常的设置NODE_ENV
注意:该参数配置的环境变量,仅是 node 环境下的变量 ,也就是我们webpack配置文件中process.env.NODE_ENV
的值。而各个模块中(我们的JS源码)中process.env.NODE_ENV
的值是通过webpack配置参数mode
或者是 DefinePlugin
定义的
BundleAnalyzerPlugin
打包分析,可以查看打包后的文件信息,方便优化。
js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
const config = {
plugins:[
// 配置打包分析
new BundleAnalyzerPlugin({
// analyzerMode: 'disabled', // 不启动展示打包报告的http服务器
// generateStatsFile: true, // 是否生成stats.json文件
})
]
}
完整配置参考:github.com/wqhui/react...
SpeedMeasurePlugin
可以使用speed-measure-webpack-plugin
来测量打包的耗费时间
js
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
const webpackConfig = smp.wrap({
plugins: [new MyPlugin(), new MyOtherPlugin()],
});
完整配置参考:github.com/wqhui/react...
生产包优化(压缩、拆包、删除无用代码)
optimization.minimizer代码压缩
该配置不仅可以配置压缩JS代码,webpack内置了TerserPlugin
;同时可以使用CssMinimizerPlugin
压缩CSS代码。
js
{
optimization: {
minimizer: [
//压缩css
new CssMinimizerPlugin(
{
exclude: PATHS.nodeModulesPath
}
),
//压缩js 也可以使用...扩展现有的
new TerserPlugin({}),
]
}
}
optimization.splitChunks 静态分析代码分包
在构建时静态地分析和拆分模块,用于优化代码的静态分割,分包是从已有的chunk中分离出新的chunk
详细可查看官方文档 和 webpack SplitChunksPlugin实用指南
js
{
optimization: {
splitChunks: {
maxInitialRequests: 5,
cacheGroups: {
//提取src代码
common: {
name: 'chunk-common',
test: /[\/]src[\/]/,
//可选值有 async、 initial 和 all。
//默认值是 async,也就是默认只选取异步加载的chunk进行代码拆分。
//initial 也就是默认同步加载的代码
//all 上述两种情况都涵盖
chunks: 'all',
// 拆分前必须共享模块的最小 chunks 数,也就当前的文件被1个以上的文件引用时才拆分
minChunks: 1,
//生成 chunk 的最小体积(以 bytes 为单位)
minSize: 0,
//一个模块可以属于多个缓存组。优化将优先考虑具有更高 priority(优先级)的缓存组
priority: 1,
},
//分割node包代码
vendors: {
name: 'chunk-vendors',
test: /[\/]node_modules[\/]/,
chunks: 'all',
priority: 2,
}
}
}
}
}
相关参数说明:
- cacheGroups
之前配置的分包策略是全局的
而实际上,分包策略是基于缓存组的
每个缓存组提供一套独有的策略,webpack按照缓存组的优先级依次处理每个缓存组,被缓存组处理过的分包不需要再次分包
- chunks
该配置项用于配置需要应用分包策略的chunk
我们知道,分包是从已有的chunk中分离出新的chunk,那么哪些chunk需要分离呢
chunks有三个取值,分别是:
- all: 对于所有的chunk都要应用分包策略
- async:【默认】仅针对异步chunk应用分包策略
- initial:仅针对普通chunk应用分包策略
所以,你只需要配置chunks
为all
即可
- maxSize
该配置可以控制包的最大字节数
如果某个包(包括分出来的包)超过了该值,则webpack会尽可能的将其分离成多个包
但是不要忽略的是,分包的基础单位是模块,如果一个完整的模块超过了该体积,它是无法做到再切割的,因此,尽管使用了这个配置,完全有可能某个包还是会超过这个体积
另外,该配置看上去很美妙,实际意义其实不大
因为分包的目的是提取大量的公共代码,从而减少总体积和充分利用浏览器缓存
虽然该配置可以把一些包进行再切分,但是实际的总体积和传输量并没有发生变化
如果要进一步减少公共模块的体积,只能是压缩和tree shaking
-
automaticNameDelimiter:新chunk名称的分隔符,默认值~
-
minChunks:一个模块被多少个chunk使用时,才会进行分包,默认值1
-
minSize:当分包达到多少字节后才允许被真正的拆分,默认值30000
import 动态引入代码分包
当涉及到运行时按需加载,可以使用 import()语法 来实现动态导入
js
import { lazy } from 'react'
export const Icon = lazy(() => import('./Icon'))
这个时候我们使用webpack
打包,webpack
会自动帮我们将代码打包到1.js
的文件中,这样就可以减少初始包中代码打大小。
项目中组件很多异步加载的组件,这部分文件可以单独打包(默认会自动打包),提高加载的速度,而这些打包出来的文件名默认为[id].js
,不好辨认.如果我们希望对导出的模块的文件进行命名,就需要用到魔法注释
魔法注释 就是在import
语句中添加注释,可以对chunk
命名或者进行其他操作,具体可查看 Magic-Comments ,我们下面讲讲常用的通过魔法注释的方式:/*webpackChunkName:"[request]"*/
,可以在代码里指定不同的文件名。
- React写法
js
import { lazy } from 'react'
export const Icon = lazy(() => import(/* webpackChunkName: "Com-1" */ './Icon'))
export const Button = lazy(() => import(/* webpackChunkName: "Com-1" */ './Button'))
Icon 和 Button 组件都会打包到Com-1.js
中。
- 原生JS写法
js
// 自定义打包chunk名
import(/*webpackChunkName:'lazy'*/ './src/Lazy.js').then(({doLazySometing})=>{
doLazySometing()
})
Tree shaking 删除未引用代码
JS
Tree-Shaking 主要用来删除ES6模块中没有引用的模块,webpack
默认在生产模式下才会启用。不过,我们可以通过调试的方式看看他是这么工作的:
- 可以使用
optimization.usedExports
和optimization.minimize
来删除export
导出了的,但是没有地方import
的代码:
js
module.exports = {
mode: 'none',//非生产、或者开发模式
optimization:{
//只导出被引用的变量
usedExports: true,
//尽可能合并模块到一个函数中
concatenateModules: true,
// usedExports后,TerserPlugin 或其它在 optimization.minimizer定义的插件压缩 bundle,删除无用代码。
minimize: true,
}
}
需要注意的是,webpack
中Tree shaking
的实现前提是模块采用ES Module
的方式实现,如果我们使用了babel-loader
去转换代码,且babel-loader
相关插件将代码转化成了commonJS
模块的话,该功能会失效。
- 如果我们有一个模块完全没有被引用,想删除整个模块文件,就需要
webpack
设置optimization.usedExports
和package.json
中设置"sideEffects":false
:
js
module.exports = {
mode: 'none',//非生产、或者开发模式
optimization:{
//只导出被引用的变量
usedExports: true,
// usedExports后,TerserPlugin 或其它在 optimization.minimizer定义的插件压缩 bundle,删除无用代码。
minimize: true,
sideEffects: true
}
}
json
// 情况1:全包无副作用(适用于纯工具库)
{
"sideEffects": false
}
// 情况2:特定文件有副作用(如全局样式、初始化脚本)
{
"sideEffects": ["./src/None.js"]
}
比如我们有个None.js
的模块,只是在index.js
引用了
js
//None.js
export function None(){
console.log('None')
}
console.log('None component sideEffects')
//index.js
import './src/None.js'
这个时候在生产时,None.js
在打包时会被整个删除。
CSS (purgecss-webpack-plugin)
删除无用的css代码可以使用插件