4 个代码分割手段彻底拆掉 umi 首屏大包
【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/gh_mirrors/umi8/umi
首屏 JS 跑到 1.4MB,白屏就不只是几秒的问题了。umi build跑完后ls -lh dist/static,你会发现共享依赖全部塞进了umi.xxxx.js主包,页面只是分了几个小 chunk。本文基于 umi 的代码分割机制,讲 4 个实战手段:核对路由切分、组件懒加载、拆 vendor、大库外置,并教你用 ANALYZE 报告量化每一步的效果。
你将学到
- 读懂
dist产物结构,确认每个路由页面是否真的输出了独立的*.async.js - 用
umi/dynamic写组件级懒加载,避开对象签名的常见误用 - 用
commons把第三方依赖拆成vendors,用externals把大库交给 CDN - 用
ANALYZE报告与产物体积对比,量化验证瘦身效果 - 判断
publicPath、disableDynamicImport等配置的适用边界
🔍 看构建产物,确认 umi 路由切分是否真的生效
这一节解决"不清楚当前包胖在哪,路由切分到底替你做了什么"的问题。
umi 根据pages目录约定生成路由(docs/guide/router.md),构建时默认把每个路由输出为独立 chunk。命名规则在 packages/af-webpack/src/getConfig.js 的 output 部分:
// packages/af-webpack/src/getConfig.js 中的 output 相关逻辑(节选) // 仅生产环境且开启 hash 时,文件名才带 8 位 chunkhash const jsHash = !isDev && opts.hash ? '.[chunkhash:8]' : ''; filename: `[name]${jsHash}.js`, // 首屏入口 chunkFilename: `[name]${jsHash}.async.js`, // 路由/动态导入的异步 chunk注意jsHash受isDev和opts.hash双重限制,所以开发模式下、或未开hash: true的构建产物里,你看到的是不带哈希的干净文件名。
用两条命令人工核对路由切分
# 构建后查看 chunk 输出结构 $ umi build $ ls dist/static产物形态参考 docs/guide/deploy.md 中的示例:
./dist ├── index.html └── static ├── pages__index.5c0f5f51.async.js # 首页 chunk ├── pages__list.f940b099.async.js # 列表页 chunk ├── umi.2eaebd79.js # 首屏入口 └── umi.f4cb51da.cssindex.html只引用umi.{hash}.js与umi.{hash}.css,两个pages__*.async.js在访问对应路由时才下载。你可以进一步验证:在pages下新增foo.js重新构建,static目录里应出现对应的pages__foo.xxx.async.js。如果新页面代码进了入口包,说明约定被破坏,常见原因是 layout 或入口文件里直接静态 import 了页面组件。
🧩 用 umi/dynamic 写对组件级代码分割
这一节解决"页面内部的大组件如何拆出去,以及 API 怎么写才不出错"的问题。
dynamic 的签名是函数,不是对象
import dynamic from 'umi/dynamic'实际通过 alias 指向 packages/umi/src/dynamic.js(见 packages/umi/src/buildDevOpts.js 中的libraryAlias)。它接收的第一个参数是返回 Promise 的函数,第二个参数是选项——直接照搬其他库的对象写法是最常见的翻车点:
// ❌ react-loadable 风格的对象签名,umi/dynamic 不接受 const Chart = dynamic({ loader: () => import('./Chart'), }); // ✅ 第一个参数是返回 Promise 的函数,第二个参数是选项 const Chart = dynamic(() => import('./Chart'), { loading: () => <div>加载中...</div>, });loading 状态与卸载保护
实现上dynamic是一个包装类组件:构造函数里就调用resolve,Promise 未 resolve 时渲染LoadingComponent,resolve 后setState渲染目标组件;内部用mounted标记避免组件卸载后再 setState。因此有两点值得注意:loading只在请求未返回时渲染,适合放骨架屏;如果你需要控制"加载但暂不渲染",要自己通过hoc选项包一层。
官方最小示例来自 docs/guide/load-on-demand.md,实际业务中写成这样即可:
import dynamic from 'umi/dynamic'; // 图表只在页面下半部分使用,拆出去不占首屏 const ChartPanel = dynamic( () => import('./ChartPanel'), { loading: () => <div>图表加载中...</div>, } ); export default function Dashboard() { return ( <div> <OrderList /> <ChartPanel /> </div> ); }此时图表代码及其依赖进入独立异步 chunk,Dashboard 首屏只加载它此刻需要的部分。
📦 用 commons 和 externals 把第三方依赖拆出去
这一节解决"node_modules 共享代码全进了首屏入口,单个大库抬高整个首屏"的问题。
用 commons 拆出 vendor chunk
af-webpack 会把commons数组的每一项转成CommonsChunkPlugin(packages/af-webpack/src/getConfig.js 第 245 行附近)。配置写在.webpackrc或.webpackrc.js(配置项清单见 packages/af-webpack/Configuration.md):
// .webpackrc.js(可直接粘贴,按需逐项开启) export default { hash: true, // 开启后文件名带 8 位 chunkhash,长效缓存的前提 // 把 node_modules 全部归入 vendors chunk commons: [ { name: 'vendors', minChunks: ({ resource }) => resource && resource.indexOf('node_modules') > -1, }, ], // 把 echarts 交给 CDN,不进 bundle externals: { echarts: 'echarts', // 对应全局变量 window.echarts }, };拆完之后,第三方库集中在vendorschunk,每个页面的异步 chunk 只剩业务代码——改页面不会让 vendors 的缓存失效。externals接受普通对象或函数(packages/af-webpack/src/getUserConfig/configs/externals.js),对 echarts 这类单一大库,外置比留在 vendors 里更彻底。
大库的 CDN 脚本从哪来
externals只负责"不打包",脚本本身要在 HTML 里注入。umi 约定新建pages/document.ejs作为模板(docs/guide/html-template.md):
<div id="root"></div> <script src="/static/vendor/echarts.min.js"></script>把echarts.min.js放进public/目录,构建时会被拷贝到dist。另外,如果项目用了 moment,在.webpackrc加ignoreMomentLocale: true,af-webpack 会用 webpack 的 IgnorePlugin 排除 moment 的全部 locale 文件(packages/af-webpack/src/getConfig.js 第 610 行附近),这是中后台项目里便宜见效的一刀。
📊 用 ANALYZE 报告与产物体积量化代码分割效果
这一节解决"配置改完了,怎么证明它变小、小了多少"的问题。
启动 ANALYZE 报告并记录体积基线
af-webpack 内置BundleAnalyzerPlugin,由ANALYZE环境变量控制:
# 构建后启动本地 server,默认 8888 端口并自动打开浏览器 $ ANALYZE=1 umi build # 改端口 $ ANALYZE=1 ANALYZE_PORT=9999 umi build # 记录主包体积与总体积,作为对比基线 $ ls -lh dist/static | head -n 20 $ du -sh dist/static报告里能直接看到每个 chunk 的体积、入口 chunk 里还混了哪些 node_modules 模块;配合上面的两条命令,改动前后的数据就能落在同一张表里。
四条判断标准
- 主入口
umi.*.js在 vendors 拆出后体积应明显下降; - 首屏总下载量 = 入口 + vendors(如有)+ 首页对应 async chunk,改动前后分别加总对比;
pages__*.async.js的数量与pages里的路由数对应,缺了就查路由约定是否被破坏;- 开了
hash: true后,浏览器重复访问确认 vendors chunk 命中 304,说明长效缓存真正生效。
如果报告里 node_modules 模块仍然挂在入口 chunk 下,说明 commons 没生效,优先检查.webpackrc是否写错位置、minChunks函数是否对目标路径返回了true。
⚠️ 代码分割的常见坑与适用边界
这一节说明哪些手段在特定场景下会起反作用。
三个容易误用的场景
- 静态资源部署到 CDN 却没配
publicPath:异步 chunk 由浏览器按publicPath请求,只把dist/static上传 CDN 而不配publicPath,路由切换时会 404,且控制台往往没有明显报错。用.webpackrc的publicPath或PUBLIC_PATH环境变量解决,详见 docs/guide/deploy.md。 - SSR / Node 运行时:Node 侧不支持
import()。umi 提供disableDynamicImport配置,af-webpack 会为此追加babel-plugin-dynamic-import-node-sync,把动态导入降级为同步 require(packages/af-webpack/src/getConfig.js 第 282 行附近)。有服务端渲染的场景不配它,运行时会直接报错。 - 过度分割:大量小组件都套
dynamic,会产出几十个 10KB 以下的小 chunk,请求次数开销可能超过体积收益;首屏就要渲染的组件也不该懒加载。用 ANALYZE 报告判断:若异步 chunk 普遍极小且数量庞大,考虑合并。
接下来可以逐行读一遍 packages/af-webpack/src/getConfig.js,找到你业务里最需要的那组插件;如果更关心 dev 启动速度,再看 packages/umi-plugin-dll 的 DLL 思路。
【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/gh_mirrors/umi8/umi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考