Bundle分析工具实战指南从Webpack到Vite的构建优化之路

Des.俊俊 工具 阅读 1,936
赞 11 收藏
二维码
手机扫码查看
反馈

项目中期发现打包体积暴增

最近一个项目做到一半的时候,突然发现打包出来的文件体积有点不对劲。本来预期是1MB左右的包,结果build出来一看,竟然飙到了4MB多。这个问题一开始没太在意,想着反正还有时间优化,直到测试环境部署的时候,加载速度慢得让人怀疑人生。

Bundle分析工具实战指南从Webpack到Vite的构建优化之路

查了一下Chrome DevTools,首屏加载时间基本都在10秒以上,这明显不正常。当时第一个想法就是肯定有什么第三方库偷偷塞进来了,或者某个组件引用了不该引用的东西。毕竟项目前期为了赶进度,很多地方都是想到什么就引入什么,完全没有做bundle分析的习惯。

Webpack Bundle Analyzer初体验

开始没想到会这么复杂,就随便搜了个Webpack Bundle Analyzer来用。安装起来倒是挺简单:

npm install --save-dev webpack-bundle-analyzer

然后在webpack.config.js里面加上插件配置:

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  // 其他配置...
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      reportFilename: 'bundle-report.html',
      openAnalyzer: false,
    }),
  ],
};

跑完build之后,浏览器打开生成的报告,好家伙,那叫一个壮观。各种颜色的模块堆在一起,大的小的乱七八糟。最大的那个模块居然占了整个包的60%,仔细一看是moment.js + locale,光一个日期库就占了2MB多。这下找到问题根源了。

最大的坑:Tree Shaking不起作用

发现问题还不算最难的,真正的坑在于怎么解决这些问题。比如那个moment.js的问题,网上一堆教程说可以用dayjs替换,或者按需引入locale。但实际操作起来发现,项目里已经到处都是moment的引用,而且很多地方还用了复杂的国际化功能,改起来成本太高。

折腾了半天,决定先从tree shaking入手。按照官方文档,在package.json里加了sideEffects: false,但效果并不理想。后来才意识到,很多第三方库根本就没做ESM的打包,还是传统的CommonJS格式,tree shaking在这种情况下基本不起作用。

这里我就踩了好几次坑,特别是对于lodash这种工具库。虽然知道应该按需引入,但项目里之前都是直接import { debounce, throttle } from ‘lodash’,这样会把整个lodash都打进包里。改成import debounce from ‘lodash/debounce’确实能减少体积,但代码改动量太大,而且IDE的自动导入还会帮你导整个lodash。

动态导入的纠结

解决第三方库问题的同时,也想到了页面级的懒加载。React Router本身支持Suspense + lazy的动态导入,理论上可以把非首屏的组件拆分出去。但这里又遇到一个问题,动态导入后chunk命名特别混乱,有时候debug的时候找都找不到对应的文件。

import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import(/* webpackChunkName: "dashboard" */ './pages/Dashboard'));
const Settings = lazy(() => import(/* webpackChunkName: "settings" */ './pages/Settings'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      {/* 路由配置 */}
    </Suspense>
  );
}

这里需要注意我踩过好几次坑的地方:webpackChunkName一定要加注释形式,不能写成字符串。不然babel-loader可能会有问题。另外,fallback的组件也要尽量简单,别在loading的时候还引入一堆复杂组件,那就失去懒加载的意义了。

SplitChunks的深度定制

最头疼的其实是SplitChunks的配置。webpack默认的split策略对大部分项目来说都不够用。我需要把第三方库、公共组件、业务代码分开打包,但官方文档的参数实在太多,minChunks、minSize、cacheGroups这些配置项搞了好久才理清楚。

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\/]node_modules[\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: 10,
          maxSize: 244000, // 244KB
        },
        common: {
          name: 'common',
          minChunks: 2,
          chunks: 'all',
          priority: 5,
          maxSize: 200000,
        }
      }
    }
  }
};

这里maxSize的设置也是经过反复测试的,244KB是HTTP/2的一个推荐值,超过这个大小可能会影响传输效率。不过具体数值还得看项目的实际情况,有些CDN服务商对单个文件大小有限制,那就得相应调整。

生产环境的实际效果

改完这一套配置后,重新build了一下,包大小确实降下来不少,从4MB多降到了1.8MB左右。但说实话,这个结果并不是特别满意,因为还有几个比较大的第三方库没办法很好地优化。

比如我们用的那个图表库,占了大概600KB,但业务需求必须用,也没办法换掉。最后只能妥协,通过动态导入的方式,让用户真正需要查看图表的时候才加载相关资源。这种方式虽然能缓解首屏加载压力,但用户体验上还是有影响的。

一些未解决的小问题

最终上线后,包大小基本控制在了预期范围内,但还是有一些小问题没有完全解决。比如说某些页面的chunk命名还是不够清晰,debug的时候偶尔还是会有点困惑。另外,SplitChunks的配置在不同环境下有时候表现不太一致,开发环境和生产环境的chunk拆分结果会有差异。

这些小问题虽然不影响正常使用,但从完美主义的角度来说还是有点遗憾。不过考虑到项目进度和性价比,暂时也就这么接受了。后续如果有时间的话,可能会考虑用Rspack或者Vite来重构打包流程,听说在bundle分析方面有更好的工具支持。

回过头来看这次优化

总的来说,这次bundle优化让我对webpack的内部机制有了更深的理解。之前只是停留在会用的层面,现在才算真正搞明白了chunks、modules、dependencies这些概念之间的关系。虽然过程中踩了不少坑,但收获还是很大的。

最重要的一点经验就是:bundle分析不能等到项目后期再来做,应该从一开始就建立良好的代码分割意识。像那种一上来就把所有东西都import进来的做法,后面想优化真的会很痛苦。

以上是我踩坑后的总结,希望对你有帮助。这种技术选型类的优化其实没有标准答案,适合项目的就是最好的。有更好方案的朋友欢迎交流讨论。

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

暂无评论