Webpack Chunk 分割实战:提升前端加载性能的关键策略

艳蕾 优化 阅读 650
赞 8 收藏
二维码
手机扫码查看
反馈

项目初期的技术选型

去年接手一个中后台管理系统重构,前端用的是 React + Webpack 5。项目上线后,首屏加载时间居然要 6 秒多,用户反馈“点开就卡住”,老板直接问“能不能快点”。我一查 Network,发现 main.js 快 3MB 了——所有页面、组件、第三方库全打在一个包里,光 lodash 就占了 100KB+。

Webpack Chunk 分割实战:提升前端加载性能的关键策略

这时候自然想到代码分割(Code Splitting)。Webpack 官方文档里提过 dynamic import + React.lazy 是最简单的方案,而且不用改太多架构。于是拍板:上 Chunk 分割,优先拆路由,再拆大组件。

核心代码就这几行

其实基础用法真不难。比如首页和设置页,本来是同步 import:

import HomePage from './pages/Home';
import SettingsPage from './pages/Settings';

改成动态加载:

const HomePage = React.lazy(() => import('./pages/Home'));
const SettingsPage = React.lazy(() => import('./pages/Settings'));

然后在 Router 外面包一层 Suspense:

<Suspense fallback={<div>Loading...</div>}>
  <Routes>
    <Route path="/" element={<HomePage />} />
    <Route path="/settings" element={<SettingsPage />} />
  </Routes>
</Suspense>

Webpack 会自动把每个 lazy 的模块打成单独的 chunk。跑完 build,dist 目录里多了 10 多个 .js 文件,main.js 直接从 3MB 降到 800KB。看起来很美,对吧?

最大的坑:性能问题

结果上线后 QA 报 bug:切换到“数据分析”页面时,页面白屏 2 秒,比之前还慢。我本地复现一看,Network 里那个 data-analysis.chunk.js 要 1.2MB!原来这个页面用了 ECharts + 一堆自定义图表组件,全被 webpack 打进同一个 chunk 了。

更糟的是,有些公共依赖(比如 moment.js)被重复打包进多个 chunk。用 Webpack Bundle Analyzer 一扫,发现 moment 在 5 个 chunk 里都存在,总大小加起来比单文件还大。

折腾了半天发现,问题出在两点:

  • 动态 import 只按路由拆,但大组件内部没再细分
  • 第三方库没做共享处理,导致重复打包

这时候才意识到,光靠 React.lazy 不够,得配合 Webpack 的 splitChunks 配置精细控制。

踩坑提醒:这三点一定注意

先说 splitChunks。我一开始照着文档抄了个通用配置:

optimization: {
  splitChunks: {
    chunks: 'all',
    cacheGroups: {
      vendor: {
        test: /[\/]node_modules[\/]/,
        name: 'vendors',
        chunks: 'all',
      }
    }
  }
}

结果 vendors.js 背了 1.5MB 的锅,首屏还是慢。后来调整策略:把大库单独拆,小库合并。比如 ECharts 和 Ant Design 这种大块头,必须独立出来;而像 dayjs、lodash-es 这种小工具库,可以合在一起。

cacheGroups: {
  echarts: {
    test: /[\/]node_modules[\/]echarts/,
    name: 'echarts',
    chunks: 'async',
    priority: 20
  },
  antd: {
    test: /[\/]node_modules[\/]antd/,
    name: 'antd',
    chunks: 'async',
    priority: 15
  },
  defaultVendors: {
    test: /[\/]node_modules[\/]/,
    name: 'vendors',
    chunks: 'async',
    priority: 10,
    reuseExistingChunk: true
  }
}

注意这里 chunks 设为 ‘async’,只对异步 chunk 生效,避免影响首屏同步加载的资源。

第二个坑是预加载(prefetch/preload)。用户大概率会点“设置”页,但默认不会预加载。我加了 magic comment:

const SettingsPage = React.lazy(() => import(
  /* webpackPrefetch: true */
  './pages/Settings'
));

这样浏览器会在空闲时偷偷下载 settings.chunk.js,实测切换速度提升明显。但别滥用,prefetch 太多反而吃带宽。

第三个坑最隐蔽:动态 import 的路径不能写变量。比如我原本想按权限动态加载页面:

// 错误示范!webpack 无法静态分析
const Page = React.lazy(() => import(./pages/${pageName}));

这会导致整个 pages 目录被打进一个 chunk,完全失去分割意义。最后只能老老实实用 switch 或 map 显式写死路径。

最终的解决方案

综合下来,我的策略是三层拆分:

  1. 路由级拆分:每个顶级路由一个 chunk,用 React.lazy + Suspense
  2. 大组件拆分:ECharts 图表、富文本编辑器等重型组件单独 lazy 加载
  3. 依赖精细化分组:通过 splitChunks 把大库独立,小库合并,避免重复

关键配置如下(webpack.config.js):

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'async',
      minSize: 20000,
      cacheGroups: {
        // 单独拆 ECharts
        echarts: {
          test: /[\/]node_modules[\/]echarts/,
          name: 'echarts',
          chunks: 'async',
          enforce: true
        },
        // Ant Design 拆出来
        antd: {
          test: /[\/]node_modules[\/]antd/,
          name: 'antd',
          chunks: 'async',
          enforce: true
        },
        // 其他 node_modules 合并
        defaultVendors: {
          test: /[\/]node_modules[\/]/,
          name: 'vendors',
          chunks: 'async',
          reuseExistingChunk: true
        }
      }
    }
  }
};

同时在入口处加 preload 提示:

<link rel="prefetch" href="/static/js/settings.chunk.js" as="script">

(实际项目中用 HtmlWebpackPlugin 自动生成)

回顾与反思

改完后首屏 JS 降到 400KB,Lighthouse 性能分从 35 提到 78。用户反馈“快多了”,虽然数据分析页首次加载还是稍慢(毕竟 1MB 的图表数据),但加了 loading skeleton 后体验好多了。

不过有几个小问题没彻底解决:

  • 某些低配手机上,chunk 切换时还是有轻微卡顿(可能是解析 JS 耗时)
  • splitChunks 配置太复杂,新同事容易改错

但整体收益远大于成本。如果重来一次,我会更早介入——在项目初期就规划好 chunk 策略,而不是等包炸了才救火。

另外,现在 Vite 用 Rollup 做分包更简单,但老项目迁移到 Vite 成本太高,只能先这么凑合着。

以上是我踩坑后的总结,希望对你有帮助。splitChunks 配置千人千面,建议结合 Bundle Analyzer 多试几次。有更优的实现方式欢迎评论区交流,比如你们怎么处理超大组件的?

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论