ArcoWebpackPlugin配置实战---踩坑记录
当 React.lazy 形同虚设一次 splitChunks 配置翻车的复盘背景我们的项目是一个基于 React 16 Arco Design React Router v5 的 B2B 后台管理系统通过 create-react-app react-app-rewired 构建。随着业务增长页面数量超过 40 个首屏加载时间已经慢到肉眼可感——白屏接近 3 秒JS 资源总量超过 3MB。我做了路由级 React.lazy 拆分也做了 splitChunks 分包结果跑完 Bundle Analyzer 一看首屏依然加载了几乎所有页面的代码。React.lazy 完全没有起到预期效果。问题出在 splitChunks 的 cacheGroups 配置上。修改前项目的config-overrides源码/* eslint-disable typescript-eslint/no-var-requires */ const path require(path); const { override, addWebpackModuleRule, addWebpackPlugin, addWebpackAlias } require(customize-cra); const ArcoWebpackPlugin require(arco-plugins/webpack-react); const addLessLoader require(customize-cra-less-loader); const setting require(./src/settings.json); const CompressionWebpackPlugin require(compression-webpack-plugin); const { BundleAnalyzerPlugin } require(webpack-bundle-analyzer); //动态配置favicon的函数 const getFaviconPath () { switch (process.env.REACT_APP_ENV) { case usa: case malaysia: case malaysia_prod: return ./public/favicon-hw.ico; case singapore: case eu: return ./public/favicon-singapore.ico; case production: return ./public/favicon-production.ico; default: return ./public/favicon.ico; } }; const isAnalyze process.env.ANALYZE true; //打包配置 const addCustomize () config { // 禁用 sourcemap(通过 GENERATE_SOURCEMAPfalse 控制,这里作为兜底) if (process.env.GENERATE_SOURCEMAP false) { config.devtool false; } //文件名加上 contenthash,便于长缓存 保证发版后用户能拿到最新代码 config.output.filename static/js/[name].[contenthash:8].js; config.output.chunkFilename static/js/[name].[contenthash:8].chunk.js; // 代码分割 switch (process.env.REACT_APP_ENV) { case dev: case development: case production: case chery: case chery_prod: case japan: case malaysia_prod: case malaysia: case singapore: case eu: case usa: case stage: config.optimization.splitChunks { chunks: all, minSize: 20 * 1024, maxSize: 500 * 1024, maxAsyncRequests: 10, maxInitialRequests: 10, automaticNameDelimiter: -, enforceSizeThreshold: 50000, cacheGroups: { //React react: { name: vendor-react, test: /[\\/]node_modules[\\/](react|react-dom|react-router|react-router-dom|react-redux|reduxjs|redux|scheduler)[\\/]/, priority: 40, reuseExistingChunk: true, chunks: all, }, //Arco Design arco: { // name: vendor-arco, //去掉固定 name让 maxSize 自由拆分 test: /[\\/]node_modules[\\/]arco-design[\\/]/, priority: 35, reuseExistingChunk: true, chunks: all, }, //Arco主题 arcoTheme: { name: vendor-arco-theme, test: /[\\/]node_modules[\\/]arco-themes[\\/]/, priority: 35, reuseExistingChunk: true, chunks: all, }, //图表库bizcharts charts: { name: vendor-charts, test: /[\\/]node_modules[\\/](bizcharts|antv)[\\/]/, priority: 30, reuseExistingChunk: true, chunks: async, enforce: true, }, // ⭐ 地理计算:turf 也不小 turf: { name: vendor-turf, test: /[\\/]node_modules[\\/]turf[\\/]/, priority: 30, reuseExistingChunk: true, chunks: all, }, // ⭐ xlsx:体积大,通常只在特定页面用 xlsx: { name: vendor-xlsx, test: /[\\/]node_modules[\\/]xlsx[\\/]/, priority: 30, reuseExistingChunk: true, chunks: all, }, // ⭐ lodash:按需加载前先单独拆出来 lodash: { name: vendor-lodash, test: /[\\/]node_modules[\\/]lodash[\\/]/, priority: 30, reuseExistingChunk: true, chunks: all, }, // ⭐ 其他工具库 utils: { name: vendor-utils, test: /[\\/]node_modules[\\/](axios|classnames|query-string|nprogress|copy-to-clipboard|react-color)[\\/]/, priority: 25, reuseExistingChunk: true, chunks: all, }, // 其他 node_modules 兜底 vendors: { name: vendors, test: /[\\/]node_modules[\\/]/, priority: 10, reuseExistingChunk: true, chunks: all, }, // 保留原有的项目内拆分 assets: { name: chunk-assets, test: /[\\/]src[\\/]assets/, priority: 20, chunks: all, }, components: { name: chunk-components, test: /[\\/]src[\\/]components/, priority: 20, chunks: all, }, pages: { name: chunk-pages, test: /[\\/]src[\\/]pages/, priority: 20, chunks: all, }, // 公共复用代码(被 2 处以上引用) commons: { name: commons, minChunks: 2, priority: 5, reuseExistingChunk: true, }, }, }; //让 webpack runtime 独立成文件,避免 vendor hash 随业务代码变化 config.optimization.runtimeChunk single; config.output.path path.join(__dirname, build); config.resolve { ...config.resolve, modules: [path.resolve(__dirname, src), node_modules], }; break; default: break; } config.plugins.push( new CompressionWebpackPlugin({ filename: [path][base].gz, algorithm: gzip, test: /\.(js|css|html|svg|json)$/, threshold: 10240, // 10KB 以上才压缩 minRatio: 0.8, deleteOriginalAssets: false, }) ); //Bundle 分析插件 if (isAnalyze) { config.plugins.push( new BundleAnalyzerPlugin({ analyzerMode: static, openAnalyzer: false, reportFilename: path.resolve(__dirname, bundle-report.html), defaultSizes: gzip, }) ); console.log([build] Bundle analyzer enabled, report will be at: ./bundle-report.html); } // 构建日志 console.log([build] REACT_APP_ENV${process.env.REACT_APP_ENV}, NODE_ENV${process.env.NODE_ENV}); return config; }; module.exports { webpack: override( addCustomize(), addLessLoader({ lessLoaderOptions: { lessOptions: {}, }, }), addWebpackModuleRule({ test: /\.svg$/, loader: svgr/webpack, }), addWebpackModuleRule({ test: /\.less$/, include: /node_modules\/arco-design\/web-react/, use: [ style-loader, css-loader, { loader: less-loader, options: { lessOptions: { javascriptEnabled: true }, }, }, ], }), addWebpackPlugin( new ArcoWebpackPlugin({ // theme: arco-themes/react-arco-pro, logo: getFaviconPath(), // webpackImplementation: this.webpack, // modifyVars: { // arcoblue-6: setting.themeColor, // }, }) ), addWebpackAlias({ : path.resolve(__dirname, src), bizcharts$: bizcharts/es, //强制 bizcharts 走 ES 入口,解锁 tree-shaking }) ), };splitChunks 参数速查在讲具体问题之前先过一遍config.optimization.splitChunks下每个参数的含义后面的分析都建立在这些概念之上。顶层参数config.optimization.splitChunks { chunks: all, minSize: 20 * 1024, maxSize: 500 * 1024, maxAsyncRequests: 10, maxInitialRequests: 10, automaticNameDelimiter: -, enforceSizeThreshold: 50000, cacheGroups: { ... } };chunks控制哪些类型的 chunk 参与分割这是最关键的参数之一async只拆分异步加载的模块通过import()动态导入的这是 webpack 的默认值initial只拆分同步引入的入口模块all两者都拆通常是我们想要的因为它能让异步 chunk 和入口 chunk 之间共享公共模块minSize是拆分的最小体积门槛。一个潜在的新 chunk 必须达到这个大小才会被真正拆出去。设成20 * 102420KB意味着太小的模块不值得单独发一个 HTTP 请求。maxSize是拆分的最大体积建议。当一个 chunk 超过这个值webpack 会尝试进一步拆分它。注意是尝试——如果一个模块自身就超过 maxSize比如一个 600KB 的单文件webpack 也拆不动它。这个值主要影响那些由多个模块组成的大 chunk。maxAsyncRequests限制按需加载时的最大并行请求数。当用户点击某个路由触发懒加载最多同时发起多少个 chunk 请求。设成 10 对 HTTP/2 来说绰绰有余。maxInitialRequests限制入口点的最大并行请求数。首屏加载 HTML 后最多同时请求多少个 JS 文件。这个值太小会导致 webpack 放弃某些本该拆出去的 chunk。automaticNameDelimiter控制自动生成的 chunk 名称中的分隔符。比如vendors-react中间的-。纯粹是命名习惯不影响功能。enforceSizeThreshold是一个强制阈值。当 chunk 体积超过这个值时webpack 会忽略minSize、maxAsyncRequests、maxInitialRequests等限制强行拆分。50KB 意味着大到这个程度就别管其他限制了必须拆。cacheGroups真正的分包规则cacheGroups: { react: { name: vendor-react, test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/, priority: 40, reuseExistingChunk: true, chunks: all, }, }cacheGroups 里的每个条目定义一条分包规则。模块被 webpack 处理时会依次匹配这些规则命中优先级最高的那条。test是匹配条件通常是一个正则用来匹配模块的文件路径。/[\\/]node_modules[\\/]react[\\/]/就是匹配 node_modules 下的 react 包。name指定输出的 chunk 名称。设了固定 name所有匹配的模块都会合并到这一个 chunk 里。如果不设namewebpack 会根据 maxSize 等条件自动拆成多个 chunk 并生成名称。priority是优先级。当一个模块同时匹配多条规则时走 priority 最高的那条。比如 react-dom 同时匹配react规则priority 40和vendors兜底规则priority 10最终进入vendor-reactchunk。reuseExistingChunk表示如果当前 chunk 包含的模块已经被拆到另一个 chunk 里了就复用那个已有的 chunk而不是重复打包。chunks可以在 cacheGroup 级别覆盖顶层的 chunks 设置。这是本次优化的核心手段之一——对特定依赖单独指定async让它们只在被动态 import 时才加载。enforce设为 true 时忽略 minSize 和 maxAsyncRequests 等限制强制按这条规则拆分。适合那些无论如何都必须独立拆出去的大型依赖。maxSize也可以在 cacheGroup 级别单独设置覆盖顶层的 maxSize。比如 arco 组件库可以设一个更大的 maxSize避免被拆成过多碎片。minChunks指定模块被引用多少次才会被拆出来。commons组里设minChunks: 2表示至少被两个 chunk 引用的公共代码才拆出去。问题一cacheGroups 的 pages 规则摧毁了路由级代码分割这是导致首屏慢的根本原因。原来的配置里有这三条规则// ❌ 问题配置 pages: { name: chunk-pages, // 固定 name test: /[\\/]src[\\/]pages/, // 匹配所有页面 priority: 20, chunks: all, }, components: { name: chunk-components, test: /[\\/]src[\\/]components/, priority: 20, chunks: all, }, assets: { name: chunk-assets, test: /[\\/]src[\\/]assets/, priority: 20, chunks: all, },name: chunk-pages这一行的意思是把所有匹配src/pages的模块合并到一个叫 chunk-pages 的文件里。而入口文件中我们的路由是这样写的const OrderList React.lazy(() import(./pages/service/order-list)); const ReportList React.lazy(() import(./pages/report/list)); const Dashboard React.lazy(() import(./pages/dashboard)); // ... 40 个页面React.lazy dynamic import 的工作原理是webpack 看到import(./pages/xxx)时会把 xxx 页面的代码拆成一个独立的异步 chunk。用户访问这个路由时才下载对应的 chunk。但 splitChunks 的 cacheGroup 规则优先级高于webpack 的默认异步拆分策略。当pages规则以 priority 20 存在时所有src/pages/下的模块都被吸进了chunk-pages这一个 chunk。webpack 的处理逻辑是import(./pages/service/order-list)→ webpack 准备拆出一个异步 chunksplitChunks 检查该模块的路径命中pages规则由于chunks: all且有固定name该模块被归入chunk-pageschunk-pages变成一个包含所有页面的巨型 chunk因为多个入口级路由都依赖chunk-pages它被提升为 initial chunk首屏加载时浏览器必须下载整个chunk-pages结果就是React.lazy 在语法层面做了动态 import但 splitChunks 在打包层面把它们又合回去了。用户打开登录页浏览器却在下载全部 40 个页面的代码。修复方案直接删除这三条规则。// ✅ 删除 pages、components、assets 三条 cacheGroup // webpack 会按 React.lazy 的 import() 边界自动拆分 // 每个页面生成独立 chunk首屏只加载当前路由删除后webpack 的默认行为接管每个import(./pages/xxx)产生一个独立的异步 chunk只在路由命中时按需下载。为什么 components 和 assets 也要删同样的道理。chunk-components把所有公共组件合并成一个 chunk即使某个组件只在一个页面用到也会被首屏加载。删除后只被单个页面引用的组件会跟着该页面的异步 chunk 走被多个页面引用的组件会被commonsminChunks: 2规则提取为公共 chunk但不会强制合并成一个巨大的文件。问题二大型依赖的 chunks 策略不对// ❌ 原配置 xlsx: { name: vendor-xlsx, test: /[\\/]node_modules[\\/]xlsx[\\/]/, priority: 30, chunks: all, // 问题在这里 }, turf: { name: vendor-turf, test: /[\\/]node_modules[\\/]turf[\\/]/, priority: 30, chunks: all, // 问题在这里 },chunks: all意味着无论 xlsx 是被同步 import 还是异步 import()都会被拆到vendor-xlsx这个 chunk 里。关键在于当这个 chunk 被标记为 initial因为有同步引用链路它就成了首屏必须下载的资源。在我们的项目里xlsx 只在导出 Excel功能中使用turf 只在地图页使用。它们不应该出现在首屏。// ✅ 修复后 xlsx: { name: vendor-xlsx, test: /[\\/]node_modules[\\/]xlsx[\\/]/, priority: 30, chunks: async, // 只在异步加载时才拆分 }, turf: { name: vendor-turf, test: /[\\/]node_modules[\\/]turf[\\/]/, priority: 30, chunks: async, // 只在异步加载时才拆分 },改成chunks: async后这些依赖只有通过import()异步引入时才会被拆成独立 chunk。如果某个页面是通过 React.lazy 加载的而那个页面 import 了 xlsx那么 xlsx 会跟着那个页面的异步加载链路走——用户访问该页面时才下载不影响首屏。有一个前提条件代码里不能有同步的顶层 import。如果某个全局工具函数写了import XLSX from xlsx那无论 splitChunks 怎么配xlsx 都会进入同步依赖图。这种情况需要把同步 import 改成动态的// ❌ 全局工具文件中的同步引入会导致 xlsx 进入首屏 import XLSX from xlsx; export function exportToExcel(data) { ... } // ✅ 改成按需动态引入 export async function exportToExcel(data) { const XLSX await import(xlsx); // ... }lodash 同理但需要视实际引用情况决定。如果 layout 层或路由守卫中直接import _ from lodash那它无论如何都会进入首屏。这时候更有效的做法是改成按路径引入import debounce from lodash/debounce从源头减小打包体积。问题三Arco Design 的 maxSize 控制Arco Design 是项目中体积最大的单一依赖。我们去掉了它的固定name让 maxSize 自动拆分——方向对但全局maxSize: 500KB对 arco 来说太激进了。// ❌ arco 被全局 maxSize 切成了 5-6 个碎片 arco: { // name 已去掉 test: /[\\/]node_modules[\\/]arco-design[\\/]/, priority: 35, chunks: all, // maxSize 继承顶层的 500KB },500KB 的 maxSize 会让 arco 被拆成多个小 chunk。对 HTTP/2 来说额外请求数不是大问题但每个 chunk 都有独立的模块加载开销过度拆分反而变慢。更务实的做法是给 arco 单独设一个宽松的 maxSize// ✅ 给 arco 单独放宽 arco: { test: /[\\/]node_modules[\\/]arco-design[\\/]/, priority: 35, reuseExistingChunk: true, chunks: all, maxSize: 800 * 1024, // arco 专属不受全局 500KB 限制 },最终的 cacheGroups 配置cacheGroups: { // React 全家桶 — 几乎不变,长缓存收益最大 react: { name: vendor-react, test: /[\\/]node_modules[\\/](react|react-dom|react-router|react-router-dom|react-redux|reduxjs|redux|scheduler)[\\/]/, priority: 40, reuseExistingChunk: true, chunks: all, }, // Arco — 体积大但首屏必需,独立拆分并放宽 maxSize arco: { test: /[\\/]node_modules[\\/]arco-design[\\/]/, priority: 35, reuseExistingChunk: true, chunks: all, maxSize: 800 * 1024, }, arcoTheme: { name: vendor-arco-theme, test: /[\\/]node_modules[\\/]arco-themes[\\/]/, priority: 35, reuseExistingChunk: true, chunks: all, }, // 以下全部 async — 只在对应页面懒加载时才下载 charts: { name: vendor-charts, test: /[\\/]node_modules[\\/](bizcharts|antv)[\\/]/, priority: 30, reuseExistingChunk: true, chunks: async, enforce: true, }, turf: { name: vendor-turf, test: /[\\/]node_modules[\\/]turf[\\/]/, priority: 30, reuseExistingChunk: true, chunks: async, }, xlsx: { name: vendor-xlsx, test: /[\\/]node_modules[\\/]xlsx[\\/]/, priority: 30, reuseExistingChunk: true, chunks: async, }, lodash: { name: vendor-lodash, test: /[\\/]node_modules[\\/]lodash[\\/]/, priority: 25, reuseExistingChunk: true, chunks: async, }, // 轻量工具库 — 体积小,合并成一个 chunk 减少请求 utils: { name: vendor-utils, test: /[\\/]node_modules[\\/](axios|classnames|query-string|nprogress|copy-to-clipboard|react-color)[\\/]/, priority: 25, reuseExistingChunk: true, chunks: all, }, // node_modules 兜底 vendors: { name: vendors, test: /[\\/]node_modules[\\/]/, priority: 10, reuseExistingChunk: true, chunks: all, }, // 被 2 chunk 引用的业务公共代码 commons: { name: commons, minChunks: 2, priority: 5, reuseExistingChunk: true, }, },可以画一张图来理解首屏加载的变化修改前首屏请求: ├── runtime.js ├── vendor-react.js ├── vendor-arco.js ├── vendor-lodash.js ← 不该在首屏 ├── vendor-xlsx.js ← 不该在首屏 ├── vendor-turf.js ← 不该在首屏 ├── chunk-pages.js ← 所有页面代码 ├── chunk-components.js ← 所有组件代码 └── main.js 修改后首屏请求以登录页为例: ├── runtime.js ├── vendor-react.js ├── vendor-arco-xxx.js ├── vendor-utils.js ├── commons.js ├── main.js └── login.chunk.js ← 只有登录页代码 用户导航到订单列表时才加载: ├── order-list.chunk.js ├── vendor-xlsx.chunk.js ← 按需 └── vendor-lodash.chunk.js ← 按需补充另一条被删除的冗余规则在 config-overrides.js 的 webpack override 链中我们还删除了一条手动添加的 arco less 规则// ❌ 删除 — ArcoWebpackPlugin 已经处理了 arco 的 less addWebpackModuleRule({ test: /\.less$/, include: /node_modules\/arco-design\/web-react/, use: [style-loader, css-loader, { loader: less-loader, ... }], }),ArcoWebpackPlugin本身会自动配置 arco 组件的样式按需加载和 less 编译。手动再加一条 less 规则会导致 arco 的 less 文件被两个 loader 链匹配和编译——虽然功能上没有报错但构建变慢且 CSS 体积翻倍。验证方式很简单删除后跑 dev如果 arco 组件样式正常渲染就说明确实是冗余的。如何验证效果跑一次 Bundle Analyzernpm run analyze然后打开生成的bundle-report.html重点看首屏 initial chunk里是否还有 xlsx、turf、bizcharts 的身影——如果有说明某处存在同步 import 链路需要从源码层面改成动态 import是否还存在 chunk-pages——如果有说明 cacheGroup 删除没有生效各 async chunk 是否合理独立——每个页面应该有自己的 chunk大小在几十 KB 到几百 KB 之间实际测试中这次修改让首屏 JS 从约 3.2MB 降到了约 800KBgzip 前白屏时间从近 3 秒降到了 1 秒以内。修改后/* eslint-disable typescript-eslint/no-var-requires */ const path require(path); const { override, addWebpackModuleRule, addWebpackPlugin, addWebpackAlias } require(customize-cra); const ArcoWebpackPlugin require(arco-plugins/webpack-react); const addLessLoader require(customize-cra-less-loader); const setting require(./src/settings.json); const CompressionWebpackPlugin require(compression-webpack-plugin); const { BundleAnalyzerPlugin } require(webpack-bundle-analyzer); // 动态配置favicon的函数 const getFaviconPath () { switch (process.env.REACT_APP_ENV) { case usa: case malaysia: case malaysia_prod: return ./public/favicon-hw.ico; case singapore: case eu: return ./public/favicon-singapore.ico; case production: return ./public/favicon-production.ico; default: return ./public/favicon.ico; } }; const isAnalyze process.env.ANALYZE true; // 打包配置 const addCustomize () config { // 禁用 sourcemap(通过 GENERATE_SOURCEMAPfalse 控制,这里作为兜底) if (process.env.GENERATE_SOURCEMAP false) { config.devtool false; } //文件名加上 contenthash,便于长缓存 保证发版后用户能拿到最新代码 config.output.filename static/js/[name].[contenthash:8].js; config.output.chunkFilename static/js/[name].[contenthash:8].chunk.js; // 代码分割 switch (process.env.REACT_APP_ENV) { case dev: case development: case production: case chery: case chery_prod: case japan: case malaysia_prod: case malaysia: case singapore: case eu: case usa: case stage: config.optimization.splitChunks { chunks: all, minSize: 20 * 1024, maxSize: 500 * 1024, maxAsyncRequests: 10, maxInitialRequests: 10, automaticNameDelimiter: -, enforceSizeThreshold: 50000, cacheGroups: { //React 全家桶:最稳定,单独拆分享受长缓存 react: { name: vendor-react, test: /[\\/]node_modules[\\/](react|react-dom|react-router|react-router-dom|react-redux|reduxjs|redux|scheduler)[\\/]/, priority: 40, reuseExistingChunk: true, chunks: all, }, //Arco Design:体积大,独立拆分,放宽 maxSize 避免过度拆分增加请求数 arco: { test: /[\\/]node_modules[\\/]arco-design[\\/]/, priority: 35, reuseExistingChunk: true, chunks: all, maxSize: 800 * 1024, }, //Arco 主题 arcoTheme: { name: vendor-arco-theme, test: /[\\/]node_modules[\\/]arco-themes[\\/]/, priority: 35, reuseExistingChunk: true, chunks: all, }, //图表库:bizcharts antv 体积很大,仅异步加载 charts: { name: vendor-charts, test: /[\\/]node_modules[\\/](bizcharts|antv)[\\/]/, priority: 30, reuseExistingChunk: true, chunks: async, enforce: true, }, //xlsx:体积大,仅异步加载(只在导出Excel的页面用到) xlsx: { name: vendor-xlsx, test: /[\\/]node_modules[\\/]xlsx[\\/]/, priority: 30, reuseExistingChunk: true, chunks: async, }, //turf完全没有使用移除 //lodash:异步加载,配合按路径引入效果更好 lodash: { name: vendor-lodash, test: /[\\/]node_modules[\\/]lodash[\\/]/, priority: 25, reuseExistingChunk: true, chunks: async, }, //其他工具库 utils: { name: vendor-utils, test: /[\\/]node_modules[\\/](axios|classnames|query-string|nprogress|copy-to-clipboard|react-color)[\\/]/, priority: 25, reuseExistingChunk: true, chunks: all, }, // 其他 node_modules 兜底 vendors: { name: vendors, test: /[\\/]node_modules[\\/]/, priority: 10, reuseExistingChunk: true, chunks: all, }, // ❌ 已删除 pages / components / assets cacheGroup // 原来的配置把所有页面代码合并成一个 chunk-pages, // 导致 React.lazy 的代码分割完全失效,首屏要加载全部页面代码。 // 删除后 webpack 会按 React.lazy 的 import() 边界自动拆分, // 每个页面生成独立 chunk,首屏只下载当前路由的代码。 // 公共复用代码(被 2 处以上引用) commons: { name: commons, minChunks: 2, priority: 5, reuseExistingChunk: true, }, }, }; //让 webpack runtime 独立成文件,避免 vendor hash 随业务代码变化 config.optimization.runtimeChunk single; config.output.path path.join(__dirname, build); config.resolve { ...config.resolve, modules: [path.resolve(__dirname, src), node_modules], }; break; default: break; } //Gzip 预压缩 config.plugins.push( new CompressionWebpackPlugin({ filename: [path][base].gz, algorithm: gzip, test: /\.(js|css|html|svg|json)$/, threshold: 10240, // 10KB 以上才压缩 minRatio: 0.8, deleteOriginalAssets: false, }) ); //Bundle 分析插件 if (isAnalyze) { config.plugins.push( new BundleAnalyzerPlugin({ analyzerMode: static, openAnalyzer: false, reportFilename: path.resolve(__dirname, bundle-report.html), defaultSizes: gzip, }) ); console.log([build] Bundle analyzer enabled, report will be at: ./bundle-report.html); } // 构建日志 console.log([build] REACT_APP_ENV${process.env.REACT_APP_ENV}, NODE_ENV${process.env.NODE_ENV}); return config; }; module.exports { webpack: override( addCustomize(), addLessLoader({ lessLoaderOptions: { lessOptions: {}, }, }), addWebpackModuleRule({ test: /\.svg$/, loader: svgr/webpack, }), // ❌ 已删除手动的 arco less 规则 // ArcoWebpackPlugin 已经处理了 arco-design 的 less 编译, // 重复的 loader 规则会导致样式被编译两次。 // 如果删除后 arco 组件样式异常,再加回来。 addWebpackPlugin( new ArcoWebpackPlugin({ // theme: arco-themes/react-arco-pro, logo: getFaviconPath(), }) ), addWebpackAlias({ : path.resolve(__dirname, src), bizcharts$: bizcharts/es, // 强制 bizcharts 走 ES 入口,解锁 tree-shaking }) ), };总结splitChunks 的 cacheGroups 是一把双刃剑用好了可以精确控制分包策略实现长期缓存用错了会静默地破坏 React.lazy 的代码分割。几条经验不要用固定 name 的 cacheGroup 匹配 src 目录。name: chunk-pagestest: /src\/pages/会把所有页面合并成一个 chunk彻底抵消路由级懒加载。chunks 参数要根据依赖的使用场景来定。首屏必需的react、arco用all只在特定页面用到的xlsx、turf、charts用async。priority 决定模块归属。确保特定规则的 priority 高于兜底的 vendors 规则否则模块可能进入错误的 chunk。删除规则也是优化。有时候最好的分包策略就是让 webpack 用默认行为——尤其是对 React.lazy 这种已经在代码层面做好了分割声明的场景。