代码分割实战:提升前端加载性能的关键技巧

慕容景景 优化 阅读 666
赞 28 收藏
二维码
手机扫码查看
反馈

首页加载慢到离谱,一查发现 bundle 超了 3MB

上周上线一个新功能后,突然收到用户反馈:“首页打开要等好几秒”。我一开始以为是接口慢,结果打开 Network 面板一看,好家伙,主 JS 文件居然 3.2MB,gzip 后还有 800KB+。这哪能忍?首屏都卡成PPT了。

代码分割实战:提升前端加载性能的关键技巧

其实项目早就用了 React + Webpack,但一直没认真搞代码分割(code splitting)。所有页面、组件、第三方库全打包进一个 vendor.js,懒加载?不存在的。这次实在拖不下去了,得动手拆。

先试了最简单的 React.lazy,结果直接报错

我第一反应是用 React 官方推荐的 React.lazy + Suspense。比如把“设置页”拆出去:

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

结果一刷新,控制台直接红屏:「ChunkLoadError: Loading chunk settings failed」。我愣了,路径没错啊,文件也生成了。折腾了半天才发现,问题出在 publicPath 配置上。

我们项目部署在子路径下(比如 /app/),但 Webpack 的 output.publicPath 没配对,导致动态 import 的 chunk 请求路径变成了根路径 /static/js/xxx.chunk.js,而实际文件在 /app/static/js/。改完配置就好了:

// webpack.config.js
output: {
  publicPath: '/app/', // 必须和部署路径一致
}

这里我踩了个坑:本地开发时 publicPath 是 /,但线上是子路径,所以测试一定要在线上环境或模拟子路径跑,不然根本发现不了。

但光拆页面不够,还得拆第三方库

解决了 ChunkLoadError,首页 bundle 从 3.2MB 降到 2.1MB,还是太大。一分析,发现 Ant Design、Lodash、Chart.js 这些第三方库全塞进去了。特别是 Ant Design,光它一个就占了 600KB+。

我试过 Webpack 的 SplitChunksPlugin 默认配置,但效果一般。后来翻文档发现,可以手动指定哪些库单独打包。比如把体积大的、变动少的库抽成独立 chunk:

// webpack.config.js
optimization: {
  splitChunks: {
    chunks: 'all',
    cacheGroups: {
      antd: {
        test: /[\/]node_modules[\/](antd|@ant-design)[\/]/,
        name: 'vendor-antd',
        chunks: 'all',
        priority: 20,
      },
      lodash: {
        test: /[\/]node_modules[\/]lodash/,
        name: 'vendor-lodash',
        chunks: 'all',
        priority: 15,
      },
      defaultVendors: {
        test: /[\/]node_modules[\/]/,
        name: 'vendor-common',
        chunks: 'all',
        priority: 10,
      }
    }
  }
}

这样打包后,Ant Design 单独成了 vendor-antd.chunk.js,只在用到 Ant 组件的页面才加载。首页不再背这个锅,bundle 直接干到 1.4MB。

不过这里有个小问题:如果多个页面都用 Ant,那每个页面首次访问都会触发加载 vendor-antd。但考虑到用户通常不会频繁切换页面,而且 CDN 缓存后第二次就快了,暂时能接受。

动态 import 里加 magic comment,控制 chunk 名称

另一个烦人的问题是,Webpack 自动生成的 chunk 名字是数字(比如 123.chunk.js),调试和排查问题时根本不知道是哪个模块。后来发现可以用 magic comment 指定 chunk 名:

const ProfilePage = React.lazy(() =>
  import(
    /* webpackChunkName: "profile-page" */
    './pages/ProfilePage'
  )
);

这样生成的文件就是 profile-page.chunk.js,清晰多了。顺手给所有 lazy component 都加上了,维护起来省心不少。

预加载?别急,先看是不是真需要

有同事建议加 <link rel="prefetch"> 预加载关键页面。但我试了下,发现对首屏性能反而有负面影响——浏览器会优先下载 prefetch 的资源,挤占了主 bundle 的带宽。最后决定只对“大概率下一步会访问”的页面做预加载,比如登录成功后预加载首页:

// 登录成功后
useEffect(() => {
  import('./pages/HomePage'); // 触发 webpack prefetch
}, []);

配合 Webpack 的 /* webpackPrefetch: true */ magic comment,效果不错。但注意:别滥用,否则带宽被占满,首屏更慢。

最终效果:首屏 JS 从 800KB 降到 320KB

整套搞下来,首页主 bundle(含核心 runtime 和首屏组件)从 gzip 后 800KB+ 降到 320KB,FCP(First Contentful Paint)从 4.2s 优化到 1.8s。虽然还有一两个小问题——比如某些低端机上动态 import 仍有轻微卡顿,但整体体验提升巨大,用户投诉没了。

回头想想,其实代码分割的核心就两点:按需加载缓存友好。拆得越细,首屏越快;但拆太碎又会增加 HTTP 请求(虽然 HTTP/2 影响小了)。所以得平衡,优先拆大块、非首屏、低频使用的模块。

核心代码就这几行,但细节全是坑

总结一下最终用的方案,其实代码不多,但每个配置都踩过坑:

// 1. 页面级懒加载(带 chunk 命名)
const Dashboard = React.lazy(() =>
  import(/* webpackChunkName: "dashboard" */ './pages/Dashboard')
);

// 2. Webpack splitChunks 配置(重点拆第三方库)
optimization: {
  splitChunks: {
    chunks: 'all',
    cacheGroups: {
      antd: {
        test: /[\/]node_modules[\/](antd|@ant-design)[\/]/,
        name: 'vendor-antd',
        priority: 20,
      },
      // 其他库类似...
    }
  }
}

// 3. publicPath 必须配对部署路径
output: {
  publicPath: process.env.DEPLOY_PATH || '/',
}

另外,记得在路由层面包裹 Suspense,避免白屏:

<Suspense fallback={<div>Loading...</div>}>
  <Routes>
    <Route path="/dashboard" element={<Dashboard />} />
  </Routes>
</Suspense>

以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流

这次优化让我意识到,代码分割不是“加个 lazy 就完事”,而是要结合部署环境、用户行为、资源特性综合考虑。我的方案肯定不是最优的,比如现在还没处理 CSS 的拆分,某些共用 hooks 也没单独抽 chunk。如果你有更优雅的做法,或者遇到过类似 ChunkLoadError 的奇葩问题,欢迎留言聊聊!

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

暂无评论