1. 为什么构建越来越慢 —— 先找到瓶颈再谈优化
先说点实在的。很多同学一提到 Webpack 构建优化,第一反应就是网上搜一堆插件往配置里塞,什么 cache-loader、thread-loader、HardSourceWebpackPlugin 全给加上,结果构建时间不但没降下来,反而因为插件之间的兼容问题报了一堆莫名其妙的错。我见过太多这种案例了,所以想先把节奏放慢,和你聊聊我这些年做 Webpack 性能调优的真实路径。
1.1 构建链路里最容易拖后腿的三个环节
一个中型项目从点击 npm run dev 到页面能访问,中间经历的东西远比你以为的多。归纳下来,构建耗时基本被三个环节吃掉:
模块解析阶段是最容易被忽视的。Webpack 默认会从入口文件开始,沿着 import 语句递归解析每个依赖,如果项目里存在大量无谓的路径查找、目录递归、或者把不需要编译的文件也丢给了 Loader,这个阶段的耗时是呈指数级增长的。典型场景是:node_modules 里的包被重复解析,或者我们在 Loader 的 test 正则里写得太宽,把 src 目录之外的文件也带了进来。
Loader 转译阶段是真正的"时间黑洞"。尤其是 babel-loader,每转译一个文件都要跑一遍完整的语法解析、插件转换、代码生成流程。一个中等规模项目,光 src 目录下就有几百个 JS 文件,每个文件可能还有多个 import 依赖,babel 要逐个处理,而且这个过程是单线程的。还有 ts-loader、eslint-loader 这类,如果在开发环境里全量开启,构建时间轻松翻倍。
代码压缩阶段在生产构建里非常耗时。TerserPlugin 需要对每个 chunk 做完整的语法分析、去空格、缩短变量名、合并表达式,这是纯粹的 CPU 密集型计算,而且官方默认配置还是单线程的。很多项目开发模式很快,但一执行 build 就卡在 90% 左右不动,多半就是卡在压缩这一步。
所以做优化之前,先搞清楚自己的瓶颈在哪。别一上来就上多进程、上缓存,那样容易把简单问题复杂化。
1.2 用 stats 和 speed-measure 给构建做一次体检
我在接手一个老项目时,第一步从来不是改配置,而是先跑一次"体检",量化每个环节的耗时。这里有两个工具是我每次必用的。
第一个是 Webpack 内置的 stats 配置。在 webpack.config.js 里加一个字段:
javascript复制module.exports = {
stats: {
modules: true,
chunks: true,
assets: true,
timings: true,
reasons: true,
moduleTrace: true
}
}
这样在执行构建时,终端会输出每个模块的构建耗时、每个 chunk 的体积、每个 asset 的大小。不过说实话,stats 输出的信息太原始,不够直观。我更推荐配合 speed-measure-webpack-plugin 来用。
这个插件的能力非常直接——它会把每个 Loader 和插件的耗时单独列出来:
javascript复制const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap({
// 你的完整 webpack 配置
});
跑完之后,你会看到类似这样的输出:
code复制SMP ⏱
General output time took 12.45 secs
Loaders:
babel-loader took 6.82 secs
module count: 324
vue-loader took 2.15 secs
module count: 147
css-loader took 1.32 secs
module count: 89
到这里,"瓶颈在哪"已经一目了然了。如果 babel-loader 占了总耗时的一半,那优化重心就放在 Loader 转译上;如果是压缩阶段慢,那就去优化压缩插件;如果你发现有一个自定义插件耗时诡异,那可能它内部同步做了大量 IO 操作,需要优化插件本身。
还有一个笨办法但很实用——分段计时。在 package.json 里拆分脚本,比如先单独执行 webpack --config webpack.dll.config.js 测 dll 构建,再执行正式构建,对比两段时间,就能判断 dll 是否真的起到了提速作用。别嫌麻烦,很多问题用这种最原始的方法反而最快定位。
注意:speed-measure-webpack-plugin 有一个已知问题,它对 webpack 5 的某些版本兼容性不好,如果报错就检查一下插件版本,或者直接用 stats 输出做手动分析。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 必改的配置项:Loader、插件与缓存策略
体检完了,接下来就该动手改配置了。这一部分我按照收益从高到低来排序,先说最不值得投入的,再说收益最大的。
2.1 把 include/exclude 写对,比任何缓存都管用
这句我放在最前面是有原因的。很多团队找我帮忙优化构建,配置里 cache-loader、thread-loader 都加了,但就是没提速。我点开配置一看,babel-loader 的 test 写的是:
javascript复制{
test: /\.js$/,
use: ['babel-loader']
}
没有 include,也没有 exclude。这意味着 Webpack 在解析完所有模块之后,loader 会尝试处理所有命中正则的 .js 文件——包括 node_modules 里那上百个包。
node_modules 里的代码通常已经经过了 ES5 转译,不需要再走 babel-loader。就算有些包需要转译,也应该用 exclude 显式排除掉其余部分,只让 babel 处理必要的那几个包。
正确写法是这样的:
javascript复制{
test: /\.js$/,
// include 限定只处理 src 目录
include: [path.resolve(__dirname, 'src')],
// 或者用 exclude 排除掉 node_modules(推荐两者都用)
exclude: /node_modules/,
use: ['babel-loader']
}
有人会问:include 和 exclude 同时写会不会冲突?实际上 Webpack 的规则是:先匹配 test,然后 apply include/exclude 条件做二次筛选。我们通常同时写上,双保险。
对于 ts-loader 也是同样的逻辑。如果你用的是 ts-loader 而不是 babel 的 TypeScript 插件,那么 transpileOnly 这个选项很多人不知道——它让 ts-loader 只做语法转译,不做类型检查,这样能省掉大量时间。但代价是类型错误只在 IDE 里提示,不会在构建时报错。我们团队的方案是:开发环境开启 transpileOnly,CI 流水线里单独跑一次 tsc --noEmit 做完整类型校验。这样两边都不耽误。
2.2 缓存策略:babel-loader 缓存与持久化缓存的正确配置
先说结论:Webpack 5 已经把 HardSourceWebpackPlugin 这个曾经的明星插件给淘汰了,因为 webpack 5 内置了持久化缓存机制。如果你还在 webpack 5 项目里用 HardSource,基本属于白费功夫,甚至可能引发缓存冲突。
Webpack 5 的文件系统缓存配置非常简单:
javascript复制module.exports = {
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename]
},
cacheDirectory: path.resolve(__dirname, 'node_modules/.cache/webpack')
}
}
核心参数就几个:
- type: 'filesystem' 表示把缓存写到磁盘上,这样下次启动时即使进程退出也能复用。默认是 memory,只在单次进程内有效。
- buildDependencies 非常关键,它声明了哪些文件变化会导致缓存失效。如果不写 config,那么你改了 webpack.config.js,webpack 可能还在吃旧缓存,构建产物还是旧的。这算是我踩过的比较深的坑之一。
- cacheDirectory 用来指定缓存目录,默认是 node_modules/.cache/webpack。建议显式指定,方便排查时直接进目录看缓存文件。
babel-loader 本身也有一个独立的缓存机制,就是 cacheDirectory 选项:
javascript复制{
test: /\.js$/,
exclude: /node_modules/,
use: [{
loader: 'babel-loader',
options: {
cacheDirectory: true,
cacheCompression: false
}
}]
}
cacheDirectory 为 true 时,babel 会把转译结果缓存到 node_modules/.cache/babel-loader。cacheCompression 建议设为 false,因为 gzip 压缩缓存文件需要额外的 CPU 开销,而缓存的目的是提速不是省磁盘空间。
这里有个细节很多人不知道:webpack 5 的文件系统缓存虽然能缓存模块编译结果,但如果你在 babel-loader 里开了 cacheDirectory,这两个缓存是叠加的。理论上会更快,但也会带来一个问题——你改了 .babelrc 配置文件之后,babel-loader 的缓存可能不会自动失效。解决方法是改完 babel 配置,手动删一下 node_modules/.cache 目录,或者启动命令里加 --cache 参数重置。
2.3 thread-loader 多进程并行编译的适用边界
很多教程把 thread-loader 吹得神乎其神,好像加上了就能把构建时间缩短一半。实际上,thread-loader 是把后续 loader 的执行放到 worker 池里并行处理,确实能利用多核 CPU。但它有几个非常实际的限制:
- worker 进程启动和通信有额外开销,如果你处理的文件不够多、不够大,反而会变慢。我的经验是:低于 200 个模块的小项目,别用 thread-loader,真没必要。
- thread-loader 不能和某些 loader 一起用,比如 vue-loader 在 worker 池里会有问题,因为 vue-loader 内部依赖了单例的 compiler 实例。
- 每个 worker 都有自己的内存空间,大项目并发转译时内存峰值会涨得很厉害,服务器配置不够的话会直接 OOM。
我通常只在 babel-loader 或 ts-loader 非常耗时的大中项目里用 thread-loader,而且是放在 loader 链的最前面:
javascript复制{
test: /\.js$/,
exclude: /node_modules/,
use: [
{ loader: 'thread-loader', options: { workers: 2 } },
{ loader: 'babel-loader', options: { cacheDirectory: true } }
]
}
thread-loader 的 workers 参数,我建议不要贪多,设置为 CPU 核心数减 1 就够。设置成和核心数一样多,反而会因为主进程也需要 CPU 而互相争抢。如果你在 CI 机器上跑构建,CPU 核数通常比本地多,可以根据机器资源调整。
3. 打包体积与产出优化:要让打包结果瘦下来
构建速度提上来之后,另一个绕不开的问题是产物体积。同样的功能,别人打包出来是 600KB,你的 2MB,这在新项目评审和面试里都会被反复提起——webpack 打包优化配置能力直接反映工程化水平。这一部分我讲三个用过的方案,每个都有明确的使用场景。
3.1 拆包思路:splitChunks 不是越大越好
Webpack 4 之后,CommonsChunkPlugin 被废弃,取而代之的是 optimization.splitChunks。很多人看到这配置就头大,干脆不配,让 webpack 用默认规则。默认规则其实已经不错,但离"最优"还差得远。
先说一个最常见的误区:以为 splitChunks 的 chunks 参数设置为 'all' 就万事大吉了。实际上,'all' 会把同步和异步 import 的模块都纳入拆包范围,但具体怎么拆、拆成几个包,取决于 minSize、maxSize、minChunks 这些参数。
一个比较稳妥的配置思路是先区分"基础库"和"业务代码":
javascript复制module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
// 考虑使用 chunk 次数,避免把每次都用到的核心库单独拆出来
minChunks: 1,
// 控制拆出来的包体积,这个值不要设的太低,否则会产生大量小文件
minSize: 100000
},
echarts: {
// 像 echarts 这种重的第三方库,单独拆一个包
test: /[\\/]node_modules[\\/]echarts[\\/]/,
name: 'echarts',
priority: 20,
minChunks: 1
}
}
}
}
}
这里解释一下核心参数:
- priority 是缓存组的优先级,数值越高越先匹配。echarts 的 priority 高于 vendor,所以 echarts 的代码会优先被拆到 echarts chunk,而不是被 vendor 吃掉。
- minChunks 表示模块被引用多少次才需要拆出来。设为 1 的基础库,会被优先提取。
- minSize 默认是 20KB,也就是只有超过这个体积的模块才值得拆。对于 vendor 库,我通常调大一点,避免拆出来一堆十几 KB 的小文件,增加 HTTP 请求数。
另外有个很多人踩过的坑:如果你在多个页面里通过 CDN 引入了 jQuery,同时又在 npm 里安装了 jQuery 并 import 使用,那么 splitChunks 可能会把 jQuery 拆成一个 chunk,但 CDN 的那份又在运行时加载,导致全局环境有双份 jQuery。这种情况需要配合 externals 处理,下一部分会说。
3.2 按需加载与动态 import 的正确姿势
路由级别的懒加载是现在的主流做法。Vue 里用() => import('./views/Home.vue'),React 里用 React.lazy,本质都是让 Webpack 把这部分代码拆成独立的 chunk,只有在访问对应路由时才加载。
但动态 import 不是魔法,它有一些容易被忽略的细节。
第一是 chunk 体积控制。如果每个路由组件都拆成单独 chunk,容易产生大量几十 KB 的小文件,首屏加载时并行请求太多,反而拖慢速度。我的做法是:把"首屏不直接展示"的弹窗、抽屉、详情页拆成独立 chunk;把逻辑强相关的路由组件打包到一起,比如 /user/list 和 /user/detail 走同一个 chunk。
第二是魔法注释的运用。动态 import 支持 webpackChunkName,指定 chunk 的名字,这个在实际排查问题时有很大帮助:
javascript复制const UserDetail = () => import(/* webpackChunkName: "user-detail" */ './views/UserDetail.vue');
你还可以配合 webpackPrefetch: true 做预加载,浏览器会在空闲时间提前拉取这个 chunk。这个要谨慎用,数据量大的场景下,预加载可能占用不必要的带宽。
第三是 prefetch 和 preload 的选择。简单说:preload 是"当前页面马上要用了",prefetch 是"将来可能要用"。首屏必要的资源用 preload,非首屏的资源用 prefetch。两者混用容易造成资源重复加载,别贪心。
3.3 压缩插件的选型:TerserPlugin、CssMinimizerPlugin 与 esbuild
生产构建的压缩环节,我在前面提过这是 CPU 密集型操作。Webpack 5 默认使用 TerserPlugin 压缩 JS,但默认配置下 TerserPlugin 是单线程的,速度比较一般。
在 webpack 5 中开启多进程压缩只需要一个参数:
javascript复制const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true,
terserOptions: {
compress: {
drop_console: true, // 生产环境去掉 console.log
drop_debugger: true
}
}
}),
new CssMinimizerPlugin({
parallel: true
})
]
}
};
注意:parallel 设为 true 时,TerserPlugin 会使用 os.cpus().length - 1 个进程,这个默认值一般来说是合理的。drop_console 这个选项建议不要默认开启,因为你可能还想在生产环境保留 warn 和 error 日志,可以写成:
javascript复制compress: {
pure_funcs: ['console.log', 'console.debug'],
}
只丢弃 log 和 debug 级别,保留其他。
CSS 压缩以前用 optimize-css-assets-webpack-plugin,这个插件在 webpack 5 里已经被官方整合进 css-minimizer-webpack-plugin,用法和 Terser 类似。
如果你想进一步提速,可以尝试 esbuild 来做压缩。esbuild 的压缩速度是 Terser 的 10 倍以上,但要注意它做的是"尽力而为"的压缩,不会进行 Terser 那样深度的 AST 分析和死代码消除。我个人的经验是:对构建速度极度敏感、且对产物体积不敏感的团队,可以上 esbuild;但对体积有硬性指标的项目,还是用 Terser 稳妥。
4. 手写一份可落地的 Webpack 优化配置
前面讲了那么多分散的技巧,是时候把它们拼成一份完整可用的配置了。我会给出一份经过实际项目验证、在中等规模 Vue 项目中能把构建时间降低 40%~60% 的 webpack.config.js 骨架。你可以直接拿来用,再根据自己的项目微调。
4.1 从 webpack.config.js 看完整的优化骨架
以一个基于 webpack 5 的 Vue 3 项目为例,完整配置如下:
javascript复制const path = require('path');
const { DefinePlugin, ProgressPlugin } = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { VueLoaderPlugin } = require('vue-loader');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const isProduction = process.env.NODE_ENV === 'production';
module.exports = {
mode: isProduction ? 'production' : 'development',
// 入口建议使用绝对路径,避免相对路径带来的解析开销
entry: path.resolve(__dirname, 'src/main.js'),
output: {
path: path.resolve(__dirname, 'dist'),
filename: isProduction ? 'js/[name].[contenthash:8].js' : 'js/[name].js',
chunkFilename: isProduction ? 'js/[name].[contenthash:8].chunk.js' : 'js/[name].chunk.js',
assetModuleFilename: 'assets/[name].[hash:8][ext]',
clean: true // 每次构建清空 dist,替代旧版 CleanWebpackPlugin
},
// 避免依赖体积过大时出现"性能提示"阻塞构建
performance: {
hints: false
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'@': path.resolve(__dirname, 'src')
},
// 显式指定 mainFields,避免 webpack 在解析包入口时做多余的字段探测
mainFields: ['module', 'main']
},
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename]
},
cacheDirectory: path.resolve(__dirname, 'node_modules/.cache/webpack')
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
include: path.resolve(__dirname, 'src')
},
{
test: /\.js$/,
include: path.resolve(__dirname, 'src'),
exclude: /node_modules/,
use: [
isProduction ? {
loader: 'thread-loader',
options: { workers: 2 }
} : null,
{
loader: 'babel-loader',
options: {
cacheDirectory: true,
cacheCompression: false
}
}
].filter(Boolean)
},
{
test: /\.css$/,
use: [
isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1
}
},
'postcss-loader'
]
},
{
test: /\.(png|jpe?g|gif|svg|webp)$/,
type: 'asset',
parser: {
dataUrlCondition: {
// 小于 10KB 的图片转 base64,减少请求数
maxSize: 10 * 1024
}
}
},
{
test: /\.(woff2?|eot|ttf|otf)$/,
type: 'asset/resource'
}
]
},
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'public/index.html'),
inject: true
}),
new DefinePlugin({
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false
}),
isProduction && new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[name].[contenthash:8].css'
}),
isProduction && new ProgressPlugin({
// 进度条插件,CI 时也可以关闭避免日志刷屏
active: !process.env.CI
})
].filter(Boolean),
optimization: {
minimize: isProduction,
minimizer: [
new TerserPlugin({
parallel: true,
terserOptions: {
compress: {
pure_funcs: ['console.log', 'console.debug']
}
}
}),
new CssMinimizerPlugin({
parallel: true
})
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/](vue|vue-router|pinia)[\\/]/,
name: 'vendor-core',
priority: 10,
minChunks: 1
}
}
},
runtimeChunk: 'single'
},
devServer: {
port: 8080,
hot: true,
open: false,
historyApiFallback: true
},
devtool: isProduction ? false : 'eval-cheap-module-source-map'
};
这份配置有几个值得说的点。
生产环境我用 MiniCssExtractPlugin 把 CSS 从 JS 里剥离出来,这样可以并行加载,也可以避免 FOUC(首屏无样式闪烁)。开发环境我用 style-loader,因为 MiniCssExtractPlugin 在 devServer 热更新时有延迟,体验不好。这就是明明白白的"开发环境与生产环境用不同方案"的思路。
关于 devtool 我再多说两句。很多项目无脑配置 source-map,这是生产构建最耗时的配置之一。source-map 会生成完整的源代码映射文件,构建时间可能直接多出 20%。在开发环境用 eval-cheap-module-source-map 就够定位问题了,生产环境除非有线上排查需求,否则直接 false 或 hidden-source-map。
4.2 开发环境与生产环境的差异化配置
上面那份配置里,我已经在同个文件中用变量区分了两种环境。但如果你的项目比较复杂,我更推荐拆成三个文件:webpack.base.js、webpack.dev.js、webpack.prod.js,用 webpack-merge 合并。这样代码更清晰,也方便不同环境加载不同插件。
开发环境的核心是"快",所以不应该做压缩、不应该抽 CSS 出来、不写 source-map 到磁盘。生产环境的核心是"稳、小、缓存友好",所以需要文件指纹、压缩、拆包、CSS 提取等。
有一个生产环境独有的配置值得单独说:output.publicPath。如果你把构建产物部署到 CDN,需要在这里配置 CDN 域名地址,或者配置相对路径 './'。很多同学前端项目部署到子目录后页面空白,就是这没配好。我一般使用环境变量传入:
javascript复制output: {
publicPath: process.env.PUBLIC_CDN_URL || '/'
}
部署平台不同,CDN 地址也不同,在构建时注入就好。
还有一个细节是 output.clean: true vs 手动删除。Webpack 5 里这个内置的 clean 功能,每次构建前会清空 output.path 目录。这个很简单,不要再去装 CleanWebpackPlugin 了。
5. 常见坑与排查经验实录
到这部分,我想分享一些"只有踩过才知道"的实战经验。面试时也是一样,光会配配置不算厉害,能讲清楚踩过的坑和排查思路,才显得有过真实项目经验。
5.1 缓存失效问题:改完代码构建却用旧产物
这是最让人崩溃的问题之一。你改了某个组件的代码,npm run build 之后线上还是旧版本,或者说本地 devServer 开了热更新却没生效。
原因通常集中在几个方面:
- 文件系统缓存命中错误。特别是使用 webpack 5 的 filesystem cache 时,如果运行时的内容哈希没有跟着变化,就可能复用旧缓存。解决方法是排查
cache: { type: 'filesystem' }配置中的 buildDependencies 是否包含配置文件以及 package.json 的变化。 - 手动配置了 cache-loader,又叠加了 webpack 自己的缓存。两层缓存叠加很容易出现数据不一致。实际上 webpack 5 时代已经不太需要单独的 cache-loader 了,webpack 本身的内容学缓存已经覆盖了模块编译结果。多余加一层,只会增加出问题的概率。
- DLL 插件导致的缓存不更新。DllReferencePlugin 是老项目常见的配置,它会生成一个 manifest.json 文件,指向打包好的 dll 文件。如果你改了依赖库的版本但没重新打 dll,构建产物就会一直引用旧 dll。我建议新项目别再用 dll 了,webpack 5 的文件缓存和 splitChunks 已经能覆盖大部分场景。
排查这一类问题时,最简单粗暴的方法就是删缓存目录再构建。但更好的做法是先看缓存目录里文件的 mtime,确认缓存是否真的被命中;用 webpack --debug 或者 stats 输出里的 cache 相关字段也能看出命中情况。
5.2 thread-loader 多进程导致的启动失败
前面推荐了 thread-loader,但这里要提一个它特有的坑。thread-loader 的 worker 进程不是立即启动的,它有一个预热过程。在某些 CI 环境里,比如 Docker 容器中 CPU 核数受限,worker 启动过程中的资源争抢会导致进程卡死或直接退出。
表现症状有几种:
- 构建在 Loader 阶段直接卡住,终端没有任何输出。
- 随机报内存溢出,Error: Worker terminated due to exceeding memory limits。
- 多个 worker 同时写 babel-loader 的同一个缓存目录,产生文件锁竞争,导致无法读取缓存。
我推荐的解决方案是:
- 在 server 配置足够大的 CI 上才用 thread-loader,小型机器或者容器里关掉。
- thread-loader 的 options 里可以配
workerNodeArgs: ['--max-old-space-size=1024']限制每个 worker 内存。 - 给 babel-loader 不同 worker 不同缓存目录,或者在 thread-loader 前再包一层专门处理缓存的 loader。
说白了,thread-loader 是一把好刀,但别见项目就用。小项目单人维护真的不需要它。
5.3 面试中最常被追问的优化点
最后说说面试里 webpack 到底考什么。我参加过多次前端岗位的面试,也被面试官考察过,webpack 优化方向的问题频率很高。核心考察点其实很集中:
第一,构建流程宏观理解。不是让你背"webpack 是基于事件流的打包工具",而是要你能说清从 entry 到 output 的完整链路:解析模块、构建依赖图、loader 转译、plugins 介入、chunk 拆分、资源输出。你懂了这个,面试官接下来考什么都绕不开它。
第二,loader 和 plugin 的区别。这个几乎是必考题。简单说:loader 负责把非 JS 模块转换成 webpack 能处理的模块,本质上是个转换函数;plugin 职责更广,可以在构建生命周期里做任何事,从打包优化到资源管理到注入环境变量。能举出实际例子来说明,就算答得不错。
第三,构建性能优化。这就是我们今天聊的所有内容。面试官会顺着问:"你做过哪些 webpack 打包优化配置?效果如何?"这时候别只答配置项,要讲出优化前后的数据对比,以及你踩过的坑。这就是实打实的分。
第四,webpack 和 vite 的区别。这个热搜词想必你也被问过。简单总结:webpack 在启动时需要预构建整棵依赖图,而 vite 在开发环境基于原生 ES Module 按需加载,所以 Vite 冷启动快得多;但 webpack 的生态系统更成熟,配置灵活度更高,在复杂的生产构建场景中更稳。更关键的是,vite 生产构建底层用的还是 Rollup,不是 esbuild。所以不是"vite 就全面优于 webpack",而是看场景选择。
我个人的体会是,webpack 优化是一个持续积累的过程。每次新项目我都会把配置精简一次,删掉不再需要的插件,改掉不必要的配置项。随着项目规模变大,再定期做一次 stats 分析,看看有没有新的瓶颈冒出来。构建工具是开发者的第一体验,把它调好,日常开发会舒服很多。
最后透露一个小技巧吧:如果你有个老项目,webpack 版本还停在 3 或 4,第一件事不是加优化插件,而是先把 upgrate 到 webpack 5,光这个升级动作就能带来 20%~30% 的构建性能提升。版本红利是最划算的优化。
