ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

4 个代码分割手段彻底拆掉 umi 首屏大包

4 个代码分割手段彻底拆掉 umi 首屏大包

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.css

index.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。

⚠️ 代码分割的常见坑与适用边界

这一节说明哪些手段在特定场景下会起反作用。

三个容易误用的场景

  1. 静态资源部署到 CDN 却没配publicPath:异步 chunk 由浏览器按publicPath请求,只把dist/static上传 CDN 而不配publicPath,路由切换时会 404,且控制台往往没有明显报错。用.webpackrc的publicPath或PUBLIC_PATH环境变量解决,详见 docs/guide/deploy.md。
  2. SSR / Node 运行时:Node 侧不支持import()。umi 提供disableDynamicImport配置,af-webpack 会为此追加babel-plugin-dynamic-import-node-sync,把动态导入降级为同步 require(packages/af-webpack/src/getConfig.js 第 282 行附近)。有服务端渲染的场景不配它,运行时会直接报错。
  3. 过度分割:大量小组件都套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),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进