首页 / 资讯中心 / 文章详情

webpack-bundle-analyzer实战:React老项目打包优化指南

webpack-bundle-analyzer实战:React老项目打包优化指南 ★ FEATURED ARTICLE
接手一个别人留下的 React 老项目最让人心里没底的事情是什么不是看不懂业务代码而是npm run build跑完之后浏览器一开 DevTools加载了一个 2MB 多的 JS 文件白屏三秒钟然后用户就在你耳边说“这页面怎么这么慢”。这个场景我遇到过太多次了。老项目做打包优化第一步永远不是猜而是拿 webpack-bundle-analyzer 把打包产物扒开看看看体积到底长在哪、长在谁身上。这篇就聊聊我怎么做这一整套体检和手术的从工具配置到拆包策略到常见坑尽量说得能直接拿去用。这套优化方案适合谁适合手里攥着 webpack 3/4 时代 React 项目的人也适合刚接手一个体积失控、构建慢、首屏慢的老系统准备做技术债清理的人。你不用是 webpack 专家只要会改配置文件、能跑构建跟着这篇的思路走基本能把最肥的那几块肉割下来。这篇不玩虚的直接讲工具、讲分析、讲拆包实战。1. 先用 webpack-bundle-analyzer 把问题的底牌亮出来1.1 这个工具到底在看什么webpack-bundle-analyzer 做的事情本质上就是把 webpack 构建出来的 stats.json构建统计信息画成一张可交互的 treemap 图。每个方块代表一个模块方块越大这个模块占的体积越大。双击方块还能往下钻进入子模块顶部有个搜索框可以直接搜模块名。它不直接减小体积但它在优化里的位置无可替代没有它你连敌人长什么样都不知道后面所有优化都是盲人摸象。我第一次给老项目跑分析报告的时候看到 main bundle 里最大的一块居然是 antd 的全量组件代码下面是 echarts 的全量包再往下是 moment 的完整 locale 文件。这三兄弟加起来占了整个包的大半江山而业务代码其实只有不到 20%。那一刻我就知道后面要做的事情不是优化性能而是给这个项目“拆弹”。这个工具的原理也不复杂。webpack 在构建时可以通过stats配置输出每个模块、每个 chunk 的详细信息包括模块路径、大小、依赖关系。webpack-bundle-analyzer 拿到这份 JSON 之后在前端用 canvas 渲染出树状图。你不需要理解细节只需要知道构建完一次项目就能得到一份精确到每个 npm 包甚至每个文件的体积地图。安装和接入方式分两种一种是加 plugin 到 webpack 配置里构建完自动打开报告另一种是只生成 stats.json然后用 CLI 手动生成报告。我强烈建议你在正式动手前先走后一种因为它的侵入感最小而且不用反复改配置。命令行方式是这样的# 安装 npm install --save-dev webpack-bundle-analyzer # 先构建生成 stats.json # 如果你们的 webpack 配置里没有开 stats可以用下面的方式临时生成 npx webpack --profile --json stats.json如果项目已经能正常构建更好的办法是直接在 webpack 配置里临时加上 plugin让 webpack 自己吐出 stats 文件后面再用 CLI 转成报告。我下面给的配置就是套餐版的做法。1.2 配置一个不打扰人的分析环境直接往生产配置里塞BundleAnalyzerPlugin默认模式会在构建完自动打开一个浏览器窗口http://127.0.0.1:8888这是开发机上最方便的体验但在 CI 或者别人机器上跑构建这个自动弹出的窗口就非常招人烦。所以我一般建议配成 static 模式生成一份 HTML 报告文件到指定目录随时可以打开看也不会阻塞流程。// webpack.config.js const { BundleAnalyzerPlugin } require(webpack-bundle-analyzer); module.exports { // ...其他配置 plugins: [ new BundleAnalyzerPlugin({ // static 模式构建完只生成 HTML 文件不自动开浏览器 analyzerMode: static, // 报告输出的路径相对于 output.path reportFilename: bundle-report.html, // 不自动打开浏览器 openAnalyzer: false, // 顺便生成 stats.json方便用 CLI 反复查看 generateStatsFile: true, statsFilename: stats.json, // 默认展示 parsed 体积也就是打包压缩后模块的实际大小 defaultSizes: parsed, }), ], };这里有几个参数值得单独说一下关系到你怎么读报告。defaultSizes有三种取值stat是模块源码的体积也就是它在磁盘上占的字节数parsed是经过 webpack 处理之后比如被 babel 转译过、被 uglify 压缩过一部分的体积这个更接近最终被打进包里的真实大小gzip是经过 gzip 压缩后的体积这决定了用户实际从网络上下载的大小。我建议主看parsed辅助看gzip。很多新手一上来盯着stat看看到 lodash 300KB 就慌实际上经过压缩可能只剩 70KB传输层再 gzip 一下可能才 20KB慌错地方了。generateStatsFile这个开关很有用因为stats.json不仅能喂给 analyzer还能喂给其他分析工具甚至可以做两次构建之间的体积 diff。老项目优化周期长今天拆一个包、明天换一个库每次构建生成一份 stats隔两周对比一次优化效果一目了然。1.3 命令行模式不改配置也能看报告如果你连 plugin 都不想加比如 webpack 配置被人写得特别复杂动一处怕出事那可以在构建命令行里临时追加参数# 先保证你的构建脚本能生成带统计信息的 JSON webpack --profile --json stats.json # 然后用 CLI 直接生成报告 npx webpack-bundle-analyzer stats.json dist注意CLI 的第二个参数dist是 bundle 文件所在的目录主要是为了读取实际文件计算 gzip 大小。如果只给stats.json报告也能出但 gzip 那一列就变成一个估算值不够准确。生成之后默认会打开浏览器如果你想直接输出 HTML 文件加一个-m static参数npx webpack-bundle-analyzer stats.json dist -m static -o report.html这种方式我实际中经常用特别是给同事演示“你看这里有个 1MB 的模块”的时候。不用动任何配置文件跑完看完该干嘛干嘛。对老项目来说这已经算是最小干预的体检姿势了。2. 拿到分析报告之后先学会怎么看门道2.1 报告里几个最容易骗人的地方treemap 一打开满屏五颜六色的方块第一反应往往是“哇这个地方大那个地方也大”。但你要是只盯着最大的方块看很容易做出错误判断。我总结过几个新手最容易踩的解读误区每个都来自真实项目。第一个误区是只看总大小不看 chunk 分布。报告顶部一般会列出所以 chunk 的列表每个 chunk 的体积是独立计算的。很多老项目没有做代码分割所有页面代码全打在一个 main bundle 里那么你在总览里看到一个大方块其实包含了十几个页面的业务代码。这种情况下当务之急不是去抠某个依赖的体积而是先把路由级的代码分割做了让每个页面变成独立 chunk。等 chunk 分开了你再看哪些依赖被重复打进了多个 chunk这才是第二轮重点。第二个误区是看到“gzip 后好像没那么大”就放松警惕。gzip 确实能把文本类资源压掉大半但它解决的是网络传输时间解决不了浏览器解析和执行 JS 的时间。一个 1MB 的 parsed 文件gzip 后可能只有 300KB下载很快但这 1MB 的 JS 代码要在主线程上 parse、compile、execute这一大坨时间一点没省。老项目卡白屏很多时候不是下载慢是执行慢。所以判断优化效果parsed 大小和 gzip 大小都得看。第三个误区是一看到某个包大就急着换库或者删依赖。你得先看这个包是“一次性大”还是“反复大”。一次性大指的是它只出现在一个 chunk 里那最多影响那个 chunk 的体积反复大指的是同一个包被打进了好几个 chunk这才是最伤人的。在 analyzer 报告里点开各个 chunk如果发现 echarts 同时出现在三个页面 chunk 里说明三份 echarts 代码被重复打包了这时最该做的是把它摘出来独立成 vendor chunk或者改用按需引入。2.2 定位问题模块的三个实用操作我拿到报告之后的操作顺序是固定的先看 chunk 列表再看每个 chunk 里的 top 模块最后用搜索框验证重复依赖。chunk 列表在报告左侧或顶部的位置不同版本位置略有差异它直接告诉你每个入口和异步 chunk 的大小。如果只有一个大 chunk说明项目完全没做代码分割这是第一优先级要解决的。如果有一堆 chunk那就逐个点开看谁最大。点开 chunk 之后报告变成以这个 chunk 为视角的模块树。我这时候会留意几个典型目标node_modules目录下的大块头、被业务代码直接 import 的“巨型全家桶”依赖、还有.css或者图片 base64 的异常大块。antd 全量引入时在报告里你会在node_modules/antd/es下面看到密密麻麻几百个小方块加起来一个多 MBmoment 全量引入时node_modules/moment/locale目录下会出现几十个小方块每个也就几 KB但合起来能到 200 多 KB而你实际只用了zh-cn一个语言包echarts 全量则是一个巨大的echarts.js方块点进去全是各图表类型模块。搜索框是在右上角输入模块路径关键词可以直接过滤。我会用它来验证某个包是否在多个 chunk 里重复出现。比如输入lodash如果搜出来一堆lodash/debounce、lodash/throttle之类的模块分散在不同 chunk 里那说明项目里大量import _ from lodash优化空间就藏在这种“无处不在的富依赖”上。2.3 先分清“结构问题”和“单点问题”分析报告看了一圈心里要有一个判断项目的问题是结构性的还是个别的结构性问题的典型特征没有代码分割、没有拆分 vendor、所有东西都打在一个包里、公共代码被重复打包。这种问题靠删几个依赖是救不了的必须动配置做分割和缓存策略。单点问题的典型特征主包整体结构还行就是有一个库特别大比如echarts、wangEditor、pdf.js这种重型组件。这种问题最直接的解法是换轻量替代品、按需引入或者丢到 CDN 上用 externals 排除掉。这个判断影响你后续所有动作的顺序我见过有人在结构问题没解决的情况下先去换图表库结果换了之后包还是有 1.5MB白折腾。记住先解决结构再解决单点。结构做对了单点问题解决起来效果才会肉眼可见。3. 老项目打包优化的核心实操从拆包到瘦身3.1 第一步永远是路由级代码分割对于没做过任何分割的 React 老项目第一刀切在路由上收益最大。只要你的项目是用 react-router并且路由级别分明就可以用React.lazy加Suspense做动态 import。这一步做完首屏从“加载全部页面代码”变成“只加载当前页面代码”体验提升是立竿见影的。import { lazy, Suspense } from react; const Dashboard lazy(() import(../pages/Dashboard)); const UserManage lazy(() import(../pages/UserManage)); const OrderList lazy(() import(../pages/OrderList)); function AppRouter() { return ( Suspense fallback{div classNamepage-loading页面加载中.../div} Switch Route path/dashboard render{() Dashboard /} / Route path/users render{() UserManage /} / Route path/orders render{() OrderList /} / /Switch /Suspense ); }注意几个细节。第一fallback里一定要给一个尽可能简单的 loading 组件别又扔一个 300KB 的 UI 组件进去那就本末倒置了。第二如果项目还在用很老的 react-router v3没有render这种用法建议先在路由层包一层或者用react-loadable这个库做兼容它内部也是动态 import只是 API 更老更稳。第三动态 import 是老项目升级时的常见爆点如果你的 webpack 还停留在 3 或 4 的早期版本可能需要补上babel/plugin-syntax-dynamic-import插件否则构建会直接报错。代码分割完成之后再去看 analyzer 报告你会发现主 bundle 缩了一大截原来的页面代码都变成了独立的小 chunk。这一步完成后面再谈依赖瘦身才有意义。3.2 splitChunks把公共依赖的账重新算一遍webpack 4 之后官方推荐用optimization.splitChunks来替代老掉牙的CommonsChunkPlugin。原理上说splitChunks 会自动提取被多个 chunk 共享的模块把它抽成公共 chunk。但对老项目我建议不要全靠自动规则要自己建立清晰的 cacheGroups因为老项目的依赖关系往往比新项目复杂得多。我通常在 webpack 配置里放这么一份optimization: { splitChunks: { chunks: all, cacheGroups: { // React 全家桶单独拆版本稳定几乎不变 reactVendor: { test: /[\\/]node_modules[\\/](react|react-dom|react-router|react-router-dom)[\\/]/, name: vendor-react, priority: 30, }, // 重型图表库单独拆 echartsVendor: { test: /[\\/]node_modules[\\/](echarts)[\\/]/, name: vendor-echarts, priority: 20, }, // 其他 node_modules 依赖统一扔进来 vendors: { test: /[\\/]node_modules[\\/]/, name: vendor-common, priority: 10, }, // 业务代码中被超过 2 个 chunk 使用的公共模块 common: { name: common, minChunks: 2, priority: 5, chunks: all, }, }, }, }这套配置背后的逻辑是react 全家桶和 echarts 这种大包单独拆出来之后它们自己的版本迭代频率低文件名后面的 contenthash 基本不变浏览器下次访问可以直接命中缓存不用重新下载。其他 node_modules 包合到vendor-common里也是因为第三方依赖整体变化频率低于业务代码。这里有个非常容易踩的坑不要把所有 node_modules 都塞进一个大 vendors 文件。有的老项目依赖特别多全塞一个文件会导致 vendors 本身超过 1MB这就从一个极端跳到了另一个极端。正确做法是像上面那样把最重、最稳定的几个包单独拉出来剩下的再合一个通用 vendor。如果你发现vendor-common还是太大继续在 cacheGroups 里加细分规则把 axios、lodash 之类的中型依赖也单独拆出来原则就是“体积大、变动少”的包尽量单独走缓存。3.3 CDN externals把基本盘甩出去拆包解决的是“分”的问题CDN 解决的是“减”的问题。对于 React 老项目里最核心、最不会变的那几个依赖比如 react、react-dom直接从 bundle 里排除掉改用 HTML 里script标签从 CDN 加载。这一步能把包体积最实在地砍掉一块。webpack 配置里加这一段module.exports { externals: { react: React, react-dom: ReactDOM, }, };然后在 HTML 模板里加上对应版本的 CDN scriptscript srchttps://cdn.example.com/react17.0.2/umd/react.production.min.js/script script srchttps://cdn.example.com/react-dom17.0.2/umd/react-dom.production.min.js/script加了 externals 之后webpack 遇到import React from react不会再去 node_modules 里找而是直接认为运行环境里已经有一个全局变量React可用。这样打包产物里就不会再有 react 那一大坨了。这个操作的注意事项不少。第一CDN 地址要固定版本不要用 latest 这种自动更新的链接否则某天上游发个破坏性版本你的老项目可能直接白屏。第二要跟运维确认线上网络的 CDN 可达性内网环境访问不了外网 CDN 的话得放内网静态服务器上。第三开发环境不建议开 externals因为本地开发需要更快的模块热更新和更好的报错堆栈你把 react 排除掉反而别扭。我通常是在 webpack 配置里用环境变量区分const isProd process.env.NODE_ENV production; module.exports { externals: isProd ? { react: React, react-dom: ReactDOM } : {}, };第四externals 不是越多越好。只对“体积大、引用广、版本极稳定”的库用 CDN比如 react、react-dom。如果什么包都丢 CDN反而会带来版本管理混乱、回归难排查的问题。我一般在老项目里只 externals react 和 react-dom 这两个其他包留在 bundle 里受 splitChunks 管理。3.4 组件库按需加载antd 们的正确打开方式老项目里几乎少不了 antd 或者 element 这类 UI 组件库。很多人写代码图省事直接import { Button, Table, Modal } from antd。这在 webpack 4 里其实是会分析并 tree-shaking 掉没用到的组件的前提是你没有使用 CommonJS 的引用方式并且 antd 的模块是 ES Module 输出。但在实际老项目里由于配置混乱、依赖关系复杂tree-shaking 经常效果不好最后 antd 全量代码被打进去。想要一个确定性更强的方案用babel-plugin-import做按需引入。// .babelrc 或 babel.config.js module.exports { plugins: [ [import, { libraryName: antd, style: css }, antd], ], };这个插件的作用是帮你做语法层面的转换。你写import { Button } from antd它会编译成import Button from antd/es/button同时按配置引入对应样式。原理就是“语法糖重写”把原本的窄口大包引入路径改写成精确到具体模块的路径。这样打包时 webpack 只把用到的组件模块打进去antd 的体积可以被砍掉一大半。还有两个同类的常客。lodash 是老项目的另类毒瘤import _ from lodash一下把整个 lodash 全量拉进来。建议要么改成import { debounce } from lodash-es要么用babel-plugin-lodash自动转换。moment 则是体积黑洞光 locale 文件就好几百 KB如果只是做日期格式化推荐直接换 dayjsAPI 基本兼容体积差了十倍不止。我做过最快的一次优化就是把 moment 换成 dayjs什么都没动包体积直接掉了 300 多 KB。3.5 Tree Shaking、压缩和 sourcemap 的收尾细节前三刀切完之后包体积应该已经明显瘦下来了。接下来是收尾阶段别忽略这几个看似不起眼但能再榨出不少水分的点。第一是 tree-shaking 的前提保障。webpack 需要能看到 ES Module 语法才能做静态分析剔除无用代码。如果你的 babel 配置里把modules: commonjs开了那 ES Module 会被转换成 CommonJStree-shaking 直接失效。老项目里常见这种情况。搜索一下 babel 配置里的babel/preset-env确保modules: false或者干脆删掉这个选项。这里有副作用如果你项目里的第三方依赖使用了 CommonJS 的写法modules: false后某些依赖可能加载异常需要实际验证一遍主流程。第二是sideEffects这个配置。package.json里标记sideEffects: false可以让 webpack 大胆摇掉“看起来没用”的模块。但对老项目这个配置要谨慎尤其是使用了 CSS 的组件库如果你一刀切设置为 false可能导致样式被摇掉页面直接变裸奔。正确做法是明确声明哪些文件有副作用比如 antd 的样式文件就不能参与摇树。我通常只在自己的业务 package.json 里设置sideEffects: [*.css]第三方库尽量不动。第三是压缩插件。webpack 4 在新版本中默认使用 TerserPlugin但老项目如果是从 webpack 3 升上来的可能还是 UglifyJS 时代的东西。TerserPlugin 能用多线程压缩对构建速度也有帮助。const TerserPlugin require(terser-webpack-plugin); module.exports { optimization: { minimizer: [ new TerserPlugin({ parallel: true, sourceMap: false, }), ], }, };第四是生产环境 sourcemap。老项目为了排查问题经常开着devtool: source-map这会让产物里带一份巨大的 sourcemap 文件虽然它不影响运行时性能但会增大部署体积和构建时间。建议生产环境用hidden-source-map或者直接devtool: false。如果怕线上问题没法排查把 sourcemap 单独传到错误监控平台就行不要让浏览器直接加载。3.6 生产环境的体积压缩gzip 不要放在打包层很多教程会推荐用 compression-webpack-plugin 在构建时生成.gz文件。我的建议是这个操作要看你有没有 nginx 控制权。如果 nginx 配置了 gzip_static 并且能直接分发预压缩文件那在构建时生成 gzip 是有价值的能省掉 nginx 的运行时压缩 CPU。如果只是普通的 nginx 动态 gzip那打包时生成 gzip 文件基本没意义反而部署体积变大。真正关心的指标是 gzip 后的传输体积它决定了用户下载速度而不是让你在包里生成一堆 gz 文件自嗨。做完所有优化之后看 report 里的 gzip 列就够了。4. 从 2.8MB 到 1.1MB一次完整的旧项目改造复盘4.1 改造前先定基线拿我自己优化过的一个典型老项目举例。那个项目技术栈是 react 16 antd 3 echarts 4 lodash 全量引用 momentwebpack 4未做任何代码分割。构建完 dist 目录里只有一个 2.8MB 的main.jsgzip 后约 850KB衍生的一些 css 倒是不多。页面首屏要下载 2.8MB 的 JS然后浏览器还要在主线程上 parse、compile 这么一大坨首屏渲染耗时奔着三四秒去用户反馈“每次打开系统都要转半天圈”。改造前我用 analyzer 跑了一次基线报告结论很清晰main chunk 里 rat 占最大头ard 的组件 样式占了大约 1.2MBecharts 第二约 500 多 KBlodash 约 300 多 KBmoment 200 多 KB真正业务代码只有大约 500 多 KB。这就是一个典型的“结构没分 单点肥大”双料问题。4.2 每一刀切完都重新出一份报告我没有一口气把所有改动同时上而是分批次每上一步构建一次、用 analyzer 记录一次体积。原因很简单同时改太多出了问题你不知道是哪一步引起的。以下是我每一步的记录第一步先做路由级代码分割。把 12 个路由页面全部改成 React.lazy。主 bundle 从 2.8MB 降到 1.4MB各页面 chunk 大概在 100KB 到 250KB。这一步 gain 最大因为它直接改变了包结构。到这里用户体感是首屏从 3 秒多降到约 1.5 秒。第二步配置 splitChunks。把 react 全家桶、echarts、常见 node_modules、公共业务代码分别拆出。产物变成一堆带 hash 的 chunk主入口 bundle 进一步降到约 400KBvendor-chart 单独一个约 400KB 的文件vendor-react 约 190KBvendor-common 约 350KB。首屏只加载入口加上必要的 vendor开始在可接受范围。第三步把 moment 换成 dayjslodash 全量引用改成按需。这两个操作加起来又减了约 500KB 的 parsed 体积。moment 那步基本是零风险dayjs 的 API 兼容度很高跑了一下全量回归只有个别地方用了 moment 特有的duration方法做了一点点兼容处理。第四步接 CDN。把 react 和 react-dom 做 externals直接从 bundle 里排除。index.html 加了两条 CDN script。这步又减了约 190KB 的 parsed 体积。这里要特别说一下react 被移除后一些依赖 react 内部结构的第三方库比如某些老版本 react-router可能运行时报错我在测试环境完整跑了一遍主流程确认没问题才放线上。最后的结果入口 chunk 从 2.8MB 降到约 1.1MBgzip 约 350KB构建时间从 5 分多钟降到 2 分半。首屏可交互时间从原来的 4 秒左右降到 1.5 秒以内用户端肉眼可见地“快了很多”。4.3 改造中的几个取舍判断这个项目里我做了一些取舍值得单独说。比如 echarts其实它还可以按需引入只引入用到的 LineChart、PieChart 等但那个项目里图表类型太多了按需改造成本高而且 echarts 单独成为一个 vendor chunk 之后配合 contenthash 缓存用户只会在 echarts 升级时才重新下载它平时都用缓存。所以我保留了全量 echarts只是把它拆出去。这不是“最优”方案但它是“有效且可落地”的方案。再比如 antd当时 antd 3 在项目里已经用得非常深babel-plugin-import 的按需改造我做了它减掉了 700 多 KB 的体积但 antd 4 当时刚发布并不值得在优化过程中顺手升级 UI 库。优化和技术升级最好是两件事分开做不然出了问题你根本不知道是升级的锅还是优化的锅。这也是我给老项目做优化时的原则能用配置解决的绝不动业务代码必须动业务代码的先跑通测试再放量。5. 常见问题与排坑实录5.1 analyzer 报告相关的常见问题问题一构建完之后 analyzer 没有自动打开也没有生成报告文件。大概率是 plugin 没生效或者 analyzer 被配置成analyzerMode: disabled。有的老项目是多个 webpack 配置比如 webpack.base.js 合并 webpack.prod.js你把 plugin 加错了文件。建议先在命令行跑一次构建看输出里有没有Webpack Bundle Analyzer is started at ...或类似日志没有就说明 plugin 没被加载。问题二报告生成了但打开report.html一片空白或者控制台报跨域错误。这是因为 HTML 报告里用 fetch 加载同目录的stats.json数据在file://协议下会被浏览器拦截。解决办法很简单用npx serve或者python -m http.server起一个本地静态服务然后访问http://localhost:5000/report.html这类地址。问题三CI 上跑构建会卡住。这是analyzerMode: server的锅它默认起一个 HTTP 服务并监听端口CI 环境没有浏览器交互构建进程可能不会结束。CI 或者一键构建脚本里记得用 static 模式或者完全不启用这个 plugin。问题四stats.json文件特别大几十 MB 甚至上百 MB。这是正常现象它包含每个模块的源码路径和依赖关系模块多自然大。但这个文件千万别提交到 Git 仓库我见过有人不小心把 stats.json 提交上去仓库直接膨胀。.gitignore 里加一行stats.json和bundle-report.html是必须的。5.2 拆包改造后的运行时问题问题一代码分割之后某个路由第一次进入时白屏过几秒才好。这大概率是动态 import 的 chunk 加载失败或者 Suspense 的 fallback 设置有问题。排查方式看 Network 面板对应 chunk 是否返回 404如果 404 说明 chunk 的 publicPath 配置不对代码分割后 chunk 文件可能放在 dist 子目录而 index.html 里脚本的引用路径没跟上。老项目经常在output.publicPath上出问题改成绝对路径或正确的相对路径就行。问题二app 外壳代码没变但每次发版后所有 chunk 的 hash 都变了缓存等于没做。这种情况多半是 webpack 的 runtime 被默认打进了某个入口 chunk而 runtime 里包含了 chunk 的映射关系任何 chunk 变化都会导致 runtime 变化进而可能让依赖它的主入口文件也需要重新下载。解决方法是把 runtime 单独拆出来optimization: { runtimeChunk: single, }如果配置了 runtimeChunk 还是出现全量 hash 变化再检查 splitChunks 的 cacheGroups 里是否给vendors这类 chunk 使用了固定的name。固定 name 本身没问题问题往往出在 hash 的生成策略上。webpack 4 的contenthash要比hash更精确修改配置时确认output.filename和chunkFilename里用的是contenthash而不是hash。问题三把 react 改成 externals 之后本地构建没问题但部署到测试环境白屏。这种情况我遇到过两次一次是 CDN 域名在测试环境被防火墙拦了react 脚本没加载成功另一次是 HTML 模板里同时保留了两份 react一份 CDN、一份本地 bundle 引用的全局变量被覆盖导致React不可用。排查方式打开页面看 Console 报错如果是React is not defined或ReactDOM is not defined就是 externals 的 CDN 脚本没生效如果报Minified React error #...通常说明页面里有多个 react 副本。问题四tree-shaking 之后发现某些样式没了。这是个非常典型的陷阱。我在前面提过sideEffects配置如果你在图省事的情况下给项目设置了sideEffects: falsewebpack 会把“看起来没用”的模块直接删掉而 CSS 文件正好容易被误伤。处理方式不要盲目全量设置找到 package.json改成{ sideEffects: [*.css, *.scss, *.less] }这样 CSS 会被保留纯 JS 模块才参与摇树。如果项目里某些模块本来就有副作用比如往 window 上挂全局变量、启动时就执行初始化逻辑也要把它们列进白名单。5.3 做优化时最容易忽略的细节最后说几个我觉得比具体配置更重要的细节。第一优化前一定要有基线记录。把优化前的stats.json保留一份优化过程中每次改动也留一份后面做对比时才不是空口说白话。不然你辛苦改了一周领导问“到底快了多少”你说“感觉快了”这不行。拿出报告对比 gzip 大小、parsed 大小、chunk 数量、构建耗时这才是专业做法。第二一次只做一类改动每类改动跑一次完整回归。我见过一个同事把代码分割、CDN、换图表库、升级 webpack 一次性全部做完然后线上直接崩了排查了整整两天才发现是某个不常用的报表页面在引用一个被删除的依赖。分开做每步验证虽然慢一点但稳。第三注意看 analyzer 报告里每个 chunk 的重复模块报告。如果你发现某个模块出现在多个 chunk 里那说明 splitChunks 的规则没覆盖到位。这时候不要盲目加 cacheGroups 规则先把该模块的引用关系理一遍往往是业务代码里存在多入口引用或者动态 import 的变量写法太灵活webpack 无法静态分析才会导致重复打包。第四我在实际中还有一个习惯优化完一轮之后不会立刻全量发线上而是先发到一个测试环境用 Lighthouse 或者 Performance 面板实测几个核心页面的指标确认首屏时间真的降了、没有回归问题再推全量。优化是给人用的不是给自己看的最终指标亮出来有意义才算真的成了。根据我个人经验老项目打包优化这件事最大的难点从来不是技术而是“不知道问题在哪”和“改完不敢上线”。webpack-bundle-analyzer 解决的是前者分批验证、灰度发布解决的是后者。如果你手里正有个跑得慢的老项目别一上来就重写成 vue3 或者 React 18先把报告跑出来照着拆包、按需、外置这三板斧来一刀多半能看到很明显的改善。后面再想继续优化也有清晰的数据支撑了。
阅读完成 · 觉得有帮助?
咨询建站