ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Webpack 4多页面项目构建优化实战:从80秒到20秒

Webpack 4多页面项目构建优化实战:从80秒到20秒 前段时间在搞一个内部代号叫“新东方”的教育类Web前端项目技术栈是Vue2 Webpack 4后端接口是Java那一套。项目本身不算大但涉及多页面入口、大量第三方依赖还有不少老的jQuery插件要兼容。整个构建配置从零开始搭前前后后踩了不少坑最后把打包时间从80多秒压到了20秒以内首屏体积也砍掉了差不多三分之一。这篇就把整个webpack配置和优化过程完整记录下来包括每一步的思路、关键参数的选择、实测数据以及一些你搜文档不一定能搜到的经验。如果你是刚接触webpack没多久、正在准备接手或改造一个老前端项目的这篇文章应该能帮你少走很多弯路。文章里所有的配置代码都是项目里实际跑过的直接抄作业基本没问题但更建议你跟着我一起把原理捋清楚这样遇到别的场景也能自己改。1. 项目定位与整体设计思路1.1 为什么选webpack而不是别的打包器先说一个很多人会纠结的问题现在Vite已经这么流行了新项目为什么还要用webpack“新东方”这个项目的情况比较典型——它不是新起炉灶而是接手一个已经跑了三四年的老项目。老项目里到处都是CommonJS模块、自定义的webpack插件、各种改自webpack的构建脚本甚至有几个第三方库的代码必须依赖webpack的ProvidePlugin才能跑起来。vite的dev server虽快但Rollup生态和CommonJS的兼容性在处理老库的时候需要打各种patch投入产出比不划算。webpack的生态最成熟Loader和Plugin几乎能覆盖所有历史包袱而且团队里的人基本都有webpack经验接手成本最低。所以就一句话老项目改造稳定性和兼容性优先webpack是当时的最优选。1.2 项目结构与环境划分整个项目拆成了三个webpack配置文件各司其职webpack.base.js公共配置entry、output、resolve、loader这些两边共用的都放这里。webpack.dev.js开发环境配置主要加devServer、HMR、更快的sourcemap、更宽松的压缩策略。webpack.prod.js生产环境配置压缩、代码分割、文件名带hash、静态资源CDN前缀等。不需要再单独搞一个webpack.common.js加webpack.dll.js的组合因为实际测试下来DLL在Webpack 4年代的提速收益已经被cache: true加thread-loader替代得差不多了还徒增维护成本。设计上还有一个重要决策多页面入口。项目有12个页面入口每个入口对应一个业务模块比如首页、课程列表、学员中心这些。入口设计直接决定了后续代码分割的方式这个下面单独讲。2. 配置文件拆解与核心参数详解2.1 入口、出口与resolve设计入口这块我没有用那种把所有入口列成一个长数组的写法而是用一个glob匹配src/pages/*/index.js自动生成entries。这样新增一个业务模块只需要新建目录不需要手动改配置。// webpack.base.js const glob require(glob); const path require(path); const entries {}; glob.sync(./src/pages/*/index.js).forEach((file) { const name file.match(/src\/pages\/(.*?)\/index\.js/)[1]; entries[name] path.resolve(__dirname, file); }); module.exports { entry: entries, output: { path: path.resolve(__dirname, dist), filename: js/[name].[contenthash:8].js, chunkFilename: js/[name].[contenthash:8].js, publicPath: process.env.NODE_ENV production ? https://cdn.example.com/static/ : /, }, // ... };入口文件filename里的[contenthash:8]是给生产环境用的内容变了hash才变方便浏览器缓存。这里注意webpack 4里hash和contenthash是有区别的hash是整个构建的hash任何文件变了它都变chunkhash是入口chunk的hashcontenthash是针对文件内容的最适合长缓存。用contenthash:8不要用完整的hash8位足够防碰撞还能控制文件名长度。publicPath这个很多人忽略其实特别关键。生产环境所有静态资源都是走CDN的如果这里没配你会发现页面能打开但所有的js/css都404因为路径请求到了你的服务端而不是CDN。这个坑我在项目上线前最后一个晚上踩过印象非常深。resolve配置是整个构建速度的重要源头之一。我当时做了一件事把resolve.modules显式指定为[node_modules]然后加上resolve.mainFields: [browser, module, main]。前者告诉webpack只在node_modules里找依赖不要往上层层查找后者保证优先使用库的浏览器版本避免打包Node环境专用代码导致polyfill满天飞。注意resolve.alias不要滥用。我给项目配过几个alias比如把指向src但后来发现某个老库内部路径和alias冲突了导致模块被重复打包。最后只保留了 - src和vue$ - vue/dist/vue.runtime.esm.js这两个其余全部移除。2.2 loader链路的搭配逻辑Loader是整个webpack配置里最值得花时间调优的部分。该项目里最关键的是babel-loader。// webpack.base.js module.exports { module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: [ { loader: thread-loader, options: { workers: 3 } }, { loader: babel-loader, options: { cacheDirectory: true, cacheCompression: false, }, }, ], }, { test: /\.vue$/, use: [ { loader: thread-loader, options: { workers: 3 } }, vue-loader, ], }, // 处理图片、字体等静态资源 { test: /\.(png|jpe?g|gif|webp|svg)$/, use: [ { loader: url-loader, options: { limit: 4096, name: img/[name].[contenthash:8].[ext], publicPath: /, }, }, ], }, ], }, };为什么要用thread-loaderWebpack处理文件时是单线程的大量js文件要做babel转译CPU的一次次转译就是构建时间的大头。thread-loader把后续loader放到worker池里并行跑等于把单个转译任务拆给多个进程。options.workers: 3这个数字不是拍脑袋定的。我机器的CPU是6核官方建议worker数等于os.cpus().length - 1留一个核心给webpack主进程和其他系统任务避免电脑直接卡死。如果强行开满6个编译时间可能反而变慢因为线程调度和通信有额外开销。cacheDirectory: true的作用是让babel-loader把转译结果缓存到node_modules/.cache里二次构建直接读缓存。这里有一个很多人不知道的小经验cacheCompression一定要设成false因为默认true会对缓存文件做gzip压缩压缩本身消耗的时间比你省下的那点磁盘空间更值钱关掉后二次构建明显更快。Vue文件用vue-loader处理这个loader要求.vue文件的template等部分还需要配合VueLoaderPlugin使用忘了加这个plugin会直接报错。项目刚开始接手时就是少了这一句导致所有.vue文件全部编译失败报错信息还特别迷惑只说“You may need an additional loader”排查了半天。所以配置里const VueLoaderPlugin require(vue-loader/lib/plugin); // plugins里一定要加 new VueLoaderPlugin(),2.3 插件体系的核心选择Webpack插件的选择重点看它对你项目的实际贡献而不是越全越好。我给“新东方”项目配的插件列表是这样的// webpack.prod.js const HtmlWebpackPlugin require(html-webpack-plugin); const MiniCssExtractPlugin require(mini-css-extract-plugin); const OptimizeCssAssetsPlugin require(optimize-css-assets-webpack-plugin); const TerserPlugin require(terser-webpack-plugin); const { CleanWebpackPlugin } require(clean-webpack-plugin); module.exports merge(baseConfig, { mode: production, plugins: [ new CleanWebpackPlugin(), // 每个页面入口生成对应的html ...Object.keys(entries).map((name) { return new HtmlWebpackPlugin({ filename: ${name}.html, template: ./src/pages/${name}/index.html, chunks: [name, vendor, common], minify: { removeComments: true, collapseWhitespace: true, removeAttributeQuotes: false, }, }); }), new MiniCssExtractPlugin({ filename: css/[name].[contenthash:8].css, chunkFilename: css/[name].[contenthash:8].css, }), ], optimization: { minimizer: [ new TerserPlugin({ parallel: true, terserOptions: { compress: { drop_console: true }, }, }), new OptimizeCssAssetsPlugin(), ], }, });这里有几个关键点值得展开说。**第一个是HTML模板和chunks的配合。**每个页面入口的HtmlWebpackPlugin都要显式指定chunks否则构建出来的html会把所有入口的js/css都塞进去不仅首屏加载一堆没用代码页面之间还会互相污染。指定chunks: [name, vendor, common]的含义是当前页面的入口js 公共vendor包 公共业务包。这个后面会在代码分割里详细说。**第二个是mini-css-extract-plugin。**开发环境不需要这个css直接用style-loader注入style标签热更新更爽生产环境才抽出来成独立css文件利用浏览器并行加载。开发和生产两边用的loader不一样这个注意在dev配置里覆盖掉。**第三个是drop_console。**生产环境去掉所有console.log这个很多人知道。但如果你项目里有用console.error做错误上报的地方建议改成只droplog和info保留error和warn。我是吃过这个亏的上线后线上错误日志全没了排查问题全靠猜。3. 打包提速实战从编译到产出的时间优化3.1 多进程与持久化缓存先说结论这一套组合拳打完冷启动构建时间从原来的84秒降到了22秒二次构建基本稳定在9秒以内。刚才提到了thread-loader和babel-loader的cache这只是第一步。再往上加的是webpack自己自带的持久化缓存。注意webpack 4不自带cache选项需要单独配webpack 5才内置了cache: { type: filesystem }。如果你还在用webpack 4推荐装一个hard-source-webpack-plugin。这个插件我第一次用的时候遇到一个很隐蔽的问题配置好后一次构建正常第二次构建报错说是某个模块的transformed code不一致。其实是写缓存的时候部分模块读取了变化中的文件状态后来我把HardSourcePlugin的configHash用构建脚本的修改时间生成就稳定了// webpack.base.js const HardSourcePlugin require(hard-source-webpack-plugin); const webpackConfigHash require(crypto) .createHash(md5) .update(fs.readFileSync(require.resolve(./webpack.base.js))) .digest(hex); // plugins中 new HardSourcePlugin({ configHash: webpackConfigHash, cacheDirectory: path.resolve(__dirname, node_modules/.cache/hard-source), }),原理其实很简单webpack每次构建都要重新解析模块依赖图、执行loader转译。hard-source把每个模块的编译结果存到磁盘上二次构建时只要源文件没变就直接从磁盘读省掉了最耗时的babel转译环节。这里强调一下踩过的一个重要结论**先开缓存再开多进程。**很多人一上来就加thread-loader说怎么速度没提升多少。因为如果loader结果没有缓存多进程确实能加速第一次构建但第二次构建的收益就很小。而缓存单独开第二次构建就已经能快上不少。两者叠加才是正向最优解顺序错了效果会打折。3.2 模块范围控制与目录瘦身有一类“隐形”的性能杀手在构建日志里看不出来叫无效模块解析。比如项目里某个入口import了一个老库那个老库里引用了一堆Node内置模块或json文件webpack会去尝试解析、甚至polyfill这部分耗时非常可憎。解决思路是尽量减少webpack解析和扫描的文件范围module.exports { module: { noParse: /jquery|lodash|moment/, }, };noParse的含义是这些库不需要解析内部依赖直接当整体打包。像jQuery、lodash这种已经是UMD格式的库内部没有require调用关掉解析完全不影响功能但构建速度能涨一截。对moment这个库我还做了更狠一点的处理moment默认会加载所有语言包一个locale文件就是几条命令在跑而且它内部还用了动态requirewebpack会把几十种语言全打包进去。最终处理是// webpack.base.js resolve: { alias: { moment: moment/min/moment-with-locales.min.js, }, }, // 业务代码里只引入需要的语言 import moment from moment; import moment/locale/zh-cn;还有一类问题是模块解析效率。项目里有个业务组件引用了某目录下的几十个文件每个文件都写了一大串相对路径比如../../../utils/api.js。这种写法本身没错但webpack每次都要一遍遍去解析这种相对路径、逐级找文件成本比用alias高得多。所以resolve: { alias: { : path.resolve(__dirname, src), utils: path.resolve(__dirname, src/utils), api: path.resolve(__dirname, src/api), }, },然后把业务代码里的相对路径引用全改成/components/xxx这种写法。实测解析这一项也能省下大概5%的构建时间积少成多。注意resolve.extensions不要配置太多。我之前在一份配置里见过extensions: [.js, .jsx, .json, .vue, .ts, .tsx, .css, .scss]这种写法本身没问题但每个import都要遍历一遍这些后缀顺序靠前的命中率高顺序靠后的基本没用。最后我只保留[.js, .vue, .json]其他特例用完整路径引入。3.3 dev模式下的按需编译开发阶段和生产的优化方向完全不一样dev追求的是响应的即时性不是构建产物体积。有一件很反常识的事情生产环境通常会开启代码分割和压缩但dev环境最好不要因为压缩和拆包反而会让增量编译变慢。我的dev配置核心如下// webpack.dev.js module.exports merge(baseConfig, { mode: development, devtool: eval-cheap-module-source-map, devServer: { contentBase: ./dist, hot: true, inline: true, historyApiFallback: true, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, }, }, }, module: { rules: [ { test: /\.css$/, use: [style-loader, css-loader], }, ], }, });devtool的选择非常关键。eval-cheap-module-source-map在webpack 4里是个性价比极高的配置source map质量足够定位到具体行编译速度又比完整的source-map快不少。如果你在开发阶段发现控制台报错位置总是定位到打包后的代码就是这里配置的问题。proxy解决的就是前端本地调试时接口跨域的问题。把/api开头的请求全部转发到后端服务changeOrigin: true这个选项容易漏不写的话有些后端会因为你请求头里的Host不对而拒绝。我在项目里遇到过接口全部401的情况排查半天发现是Host头部被带成了localhost:8080后端认证逻辑校验的是真实域名。另外dev环境会配置HMR模块热替换。Vue项目要在入口文件里加这么一段// main/index.js if (module.hot) { module.hot.accept(); }没有这一句Vue组件文件改了之后不会局部刷新而是整页reload之前的编辑状态、弹窗、表单输入全丢开发体验极差。这个细节如果你接手别人配好的环境可能根本不会注意到但自己从零搭的时候很容易漏。4. 体积优化把首屏体积降下来的几种招4.1 代码分割的正确姿势构建速度搞定之后第二个大问题是体积。“新东方”项目接手时首屏js差不多有4MB未压缩前gzip后也有1.2MB页面打开白屏时间在三秒以上。这个体量明显不正常核心原因是所有页面的代码全打包在一个大bundle里了。代码分割就是拆。我用的是SplitChunksPluginwebpack 4默认内置配置方式如下// webpack.prod.js optimization: { splitChunks: { chunks: all, cacheGroups: { vendors: { test: /[\\/]node_modules[\\/]/, name: vendor, priority: 10, chunks: all, }, common: { minChunks: 2, minSize: 30000, name: common, priority: 5, }, styles: { test: /\.css$/, minChunks: 2, enforce: true, }, // 把echarts这种大库单独拆出来 echarts: { test: /[\\/]node_modules[\\/]echarts[\\/]/, name: echarts, priority: 20, chunks: all, }, }, }, },这里的核心是cacheGroups。webpack会扫描所有模块的引用关系把符合条件的模块组合成一个新的chunk。vendors组负责把node_modules里的库全部抽到vendor.js里这样业务代码和依赖库分开改业务代码时vendor的contenthash不变可以继续用缓存。common组负责把多个页面共同引用的业务代码抽出来minChunks: 2表示至少被两个页面引用的代码才抽避免把只在一个页面用到的代码也拆出去反而增加请求数。echarts单独拆是因为这个库本身就快500KB而且只在课程数据页用到放vendors里会让所有页面都背上这个重量单独拆出来只有用到echarts的页面才加载它。这里有个实战误区要提醒很多人以为拆得越细越好。实际上HTTP请求数是有成本的每个chunk都要一次网络请求。拆成二三十个小文件HTTP2下还好HTTP1.1下浏览器一个域名并发只有6个连接十几个chunk排队下载的时间比省下那点体积还亏。项目里我最终控制拆出来的chunk总数在8个以内。我实际效果是优化后首屏依赖的js体积从4MB降到1.9MB未压缩gzip后约580KB白屏时间从3.2秒降到了1秒以内。4.2 压缩配置与Tree Shaking体积优化的第二个大头是压缩和摇树。Tree Shaking在webpack 4里是生产模式自动开启的但有个前提代码必须是用ES Module写的。项目里老代码全是CommonJS的module.exports写法Tree Shaking对它们完全无效。这就导致了一些看起来没被引用的代码依然被打进了bundle。对于业务代码我没法做到短时间内全部改造完成但做了一件性价比极高的事排查那些把整个库引入却只用其中一两个函数的地方。最典型的就是lodash// 之前 import _ from lodash; const result _.cloneDeep(data); // 改成 import cloneDeep from lodash/cloneDeep; const result cloneDeep(data);就这么一条lodash的打包体积从大概150KB降到20KB以内。类似这种“按需引库”的改造我梳理了项目里12处整体体积直接降了不少。再就是压缩。TerserPlugin的配置里有一个容易被忽略的点new TerserPlugin({ parallel: true, terserOptions: { compress: { drop_console: true, drop_debugger: true, }, output: { comments: false, }, }, extractComments: false, }),extractComments: false这个一定要加。默认情况下Terser会把代码里的license等注释提取成一个.LICENSE.txt文件交给html引用。如果项目里有很多第三方库这些license文件会积累一堆而且会在产物里生成一堆碎文件看着非常乱。关掉后注释直接丢弃产物干净很多。gzip压缩这个事也要说清楚。生产环境一般由Nginx开启gzip所以webpack打包时不需要再压缩一遍。但如果你用CDN且CDN没有自动压缩需要在配置里加上compression-webpack-plugin生成.gz文件再让Nginx优先使用。我构建时顺手配了因为公司CDN没开gzip直接放.gz文件比让CDN现压缩更稳。4.3 图像与静态资源处理项目里的图片资源之前是散落在各个目录有的走url-loader转base64有的走file-loader生成文件。我当时看到一个特别严重的问题有一张背景图原图2MB的jpg被url-loader直接转成了base64塞进了css文件里css体积直接爆掉首屏css加载被硬生生拖慢了一秒多。这个问题的根源是limit配置太大。项目里原来是这么配的{ loader: url-loader, options: { limit: 1024 * 100, // 100KB }, }100KB以下全部转base64。看着挺合理但没考虑到图片的base64体积会膨胀约33%而且css里base64没法被浏览器按需加载他会跟随css的加载全量解析。我最终的策略是limit: 40964KB以下转base64小了没意义大了拖累css。4KB以上的走文件输出用name: img/[name].[contenthash:8].[ext]。大图超过200KB的顾顾不上懒加载的业务场景就配合file-loader加一个publicPath让图片走CDN地址。还有一类静态资源容易漏favicon和manifest.json。这类文件如果放在public静态目录里webpack不会处理它们生产环境需要手动复制到dist。我用copy-webpack-plugin统一处理new CopyWebpackPlugin([ { from: ./public/favicon.ico, to: ./ }, { from: ./public/manifest.json, to: ./ }, ]),这个不起眼但漏掉的话上线后favicon 404域名标签页上就是个打叉图标很掉档次。5. 优化效果与性能验证5.1 优化前后的核心指标对比数据不会骗人这个项目优化前后的核心指标如下指标优化前优化后提升比例冷启动构建时间84s22s约74%二次构建时间无改动51s9s约82%首屏未压缩JS体积约4MB约1.9MB约52%gzip后体积1.2MB580KB约52%白屏时间模拟器约3.2s约0.9s约72%最大chunk体积1.1MB约320KB约71%需要说明的是构建时间这个指标受机器性能影响很大不同机器之间横向对比没有意义。关键是同一台机器、同一个项目的纵向对比优化的效果是非常明显的。有一个容易被忽略的指标是chunk数量。优化前整个产物只有一个大bundle加上图片和css总共就七八个文件优化后拆出了vendor、common、各页面入口等加起来有15个文件左右。文件多了但总请求数是合理的。如果拆得太多我要在构建日志里留意有没有warning提示某个chunk体积超过了推荐限制。5.2 验证方法与监控工具推荐两个可以直接抄的工具用来量化优化效果。第一个是webpack-bundle-analyzer。在devDependencies里装好后在webpack.prod.js里临时加上const BundleAnalyzerPlugin require(webpack-bundle-analyzer).BundleAnalyzerPlugin; // 构建时用环境变量控制不想看的时候不用改代码 if (process.env.ANALYZE) { plugins.push(new BundleAnalyzerPlugin()); }跑ANALYZEtrue npm run build构建完会自动打开一个可视化面板能直观看到每个模块的体积占比。echarts、lodash这种大块头一目了然。我就是靠这张图定位到lodash和moment的体积问题的。第二个是性能监控。改完之后我用Lighthouse在模拟器上跑了一遍重点看三个指标FCP首次内容绘制、LCP最大内容绘制、TBT总阻塞时间。优化后FCP从3.2s降到了1.1sLCP从3.8s降到1.5sTBT从760ms降到180ms。对教育类落地页来说这个表现已经能进绿区了。这里还想分享一个从后端同事那里学到的经验打包后的体积监控最好固化成一个CI检查。我在.gitlab-ci.yml里加了一步构建完了检查gzip后超过1MB就fail防止后面的人加个依赖就把体积又拉回去。这个算是个长期保障效果比口头约定好得多。6. 常见问题与排查手册6.1 典型问题速查整个配置和优化过程中遇到的最典型问题我整理成一个速查表现象可能原因解决方向构建报“Module not found: Cant resolve xxx”依赖包本身缺文件或resolve配置没覆盖到检查resolve.extensions和resolve.mainFields开发环境正常生产环境JS报错devtool sourcemap类型差异大或生产模式下压缩代码改动先在mode: none下构建定位真实报错行CSS正常但样式错乱css-loader和style-loader顺序不对或MiniCssExtractPlugin配置未生效检查loader的use数组顺序从右往左执行打包体积莫名暴涨某个库被重复打包或动态import没生效用webpack-bundle-analyzer检查重复模块HMR不生效改vue组件整页刷新入口文件少了module.hot.accept()或vue-loader版本不兼容检查入口模块热更新代码确认VueLoaderPlugin已注册图片全部变成base64css体积爆炸url-loader的limit太大调小limit至4096大图走file-loader输出生产环境资源404publicPath配错检查CDN路径与output.publicPath的拼接规则二次构建时间和首次几乎一样缓存没有生效或hard-source和部分loader不兼容确认cache相关插件列表必要时清缓存重试6.2 排查思路与方法技巧再说几条真正有用的排查经验。第一条用webpack --profile --json输出构建过程JSON然后再解析这个JSON文件来分析各模块耗时。这个比看构建日志的秒数直观得多webpack --profile --json build-stats.json node analyze-stats.jsanalyze-stats.js脚本的思路是读取build-stats.json里的modules数组按build-time字段排序找出耗时最长的前20个模块。我靠这个找到了几个构建时间超1秒的老组件原因是有个大json文件被直接import了webpack每次构建都要序列化转换提升很快。第二条先开一个最小的复现项目。遇到webpack诡异问题最忌讳在大型项目里直接试各种配置改动因为项目里模块太多错误信息往往会被吞掉或者被其他插件干扰。我当时遇到一个sass-loader和node-sass的版本冲突问题直接在大项目里改各种版本越改越乱。后来起了一个最小的webpack项目只放一个.scss文件一分钟就复现了三分钟就定位到了是node-sass4.x绑定Node版本的问题。第三条留意webpack日志的warning。大多数warning都能直接指出问题比如Chunk.entrypoints里提示npm包体积过大Conflicting order提示模块引入顺序可能被抽包打乱。这些warning不像error那么显眼但往往是后面线上bug的隐患。我处理过一个Conflicting order的warning当时没管结果上线后某个页面偶发样式错乱排查了很久才回来处理它。第四条善用webpack-merge的覆盖顺序。多个配置文件合并的时候数组类型的配置是追加而非覆盖这是一个非常容易踩的坑。比如webpack.base.js里的module.rules是数组webpack.prod.js里如果再push一条loader规则它不会替换掉base里的而是追加在后面。这样会导致loader执行两次或者规则冲突。解决办法是const { merge } require(webpack-merge); module.exports merge({ customizeArray: (a, b, key) { if (key module.rules) { return b; // 用prod的rules整体覆盖base的 } return undefined; }, })(baseConfig, prodConfig);这个坑排查了好几小时说出来都是泪。6.3 关于缓存失效的深层原因最后单独写一下缓存失效的坑。build后文件的contenthash变了这是个让人头疼的问题。有次我什么都没改只是重新部署一遍发现很多文件的hash全变了等于所有用户都要重新下载一遍全部静态资源缓存策略失效了。排查后发现原因是构建机器时间不同。我们用的字段是[contenthash:8]但在Webpack 4里这个hash的实际自定义计算逻辑在某些情况下会依赖模块的构建顺序。如果两个chunk文件在同一秒内被构建出来而module的id是递增的一旦新增或删除了一个入口后面所有模块的id都会往后挪一个contenthash就会全变。解决办法有两个第一个是开启optimization.moduleIds: hashed让模块id基于模块路径生成而不是简单的递增数字optimization: { moduleIds: hashed, },第二个是给文件名加上chunkhash而不是contenthash。虽然contenthash适合细粒度缓存但在处理“入口新增导致所有文件hash全变”这个问题上chunkhash会更稳定。实践中我最终保留了一个混合策略——JS文件用chunkhash:8CSS用contenthash:8这样改动模块时兄弟模块的hash不会全部失效。还有一个更隐蔽的问题第三方依赖升级导致hash变化。npm包升级后虽然你没改业务代码但vendor里的内容变了vendor的hash肯定要变。这个没办法规避但可以通过一个手段保障业务代码的hash稳定——就是上面说的强制把第三方依赖放到一个固定vendors缓存组里。最后分享一个个人强烈推荐的习惯把webpack.base.js、webpack.dev.js、webpack.prod.js的配置写好后一定不要直接跑生产构建先在本地用webpack --mode production跑一次确认输出日志里没有warning再上。这能省很多后续的问题排查时间。整篇写下来核心就一句话webpack配置没有银弹所有优化都建立在理解项目实际瓶颈之上。构建慢就去找慢的模块体积大就去看大的依赖缓存失效就去查hash策略。把工具用到位问题基本都是可以量化、可以解决的。这套“新东方”项目的配置方案如果你也在改造老项目照着参考一下应该能省不少试错的时间改配置的时候多留一份心眼每个参数改动前想清楚它解决什么问题、会引入什么副作用你会少踩很多坑。
返回列表