前端代码分割实战指南从入门到项目落地的完整解决方案

南宫玉翠 前端 阅读 1,646
赞 8 收藏
二维码
手机扫码查看
反馈

项目初期的技术选型

最近接手了一个老项目重构,原本是单页面应用,打包出来15M+,首屏加载慢得要死。客户抱怨了一年多,终于下定决心要优化。说实话,刚看到那个打包体积的时候我都惊了,这玩意儿能跑起来都算奇迹。

前端代码分割实战指南从入门到项目落地的完整解决方案

分析了一下,主要问题是所有模块都打在一起了,用户打开首页就得加载整个应用。项目用了React + Webpack,代码分割是肯定要上的,但怎么切分是个技术活。刚开始想着按路由拆,后来发现还不够细,得按组件级别拆分才行。

第一次尝试:动态import踩坑记

开始很简单粗暴,就是把那些不是首屏的组件全部改成动态import:

// 原来是这样
import HeavyComponent from './HeavyComponent';

// 改成这样
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));

本地测试看起来不错,页面确实快了些。但是部署到测试环境后发现了个大问题:chunk文件太多,HTTP请求数爆表。一个页面拆成了20多个小文件,虽然单个文件小了,但请求开销反而更大了。Chrome DevTools显示网络请求密密麻麻,看着都头疼。

折腾了半天发现是webpack配置的问题,splitChunks那套规则我基本没动,默认配置对我们的项目太激进了。后来调整了配置:

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\/]node_modules[\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: 10,
          maxInitialRequests: 5,
          maxAsyncRequests: 10
        },
        common: {
          name: 'common',
          minChunks: 2,
          chunks: 'all',
          priority: 5,
          enforce: true
        }
      }
    }
  }
};

最大的坑:第三方库的分割策略

这里真的是踩了大坑。项目里用了好几个重量级的第三方库:Chart.js、Quill编辑器、PDF预览等。开始想当然地认为这些库应该独立打包,结果发现有些库内部还有复杂的依赖关系,强行分离会导致运行时错误。

比如Chart.js依赖moment.js,但如果我把moment单独抽出去,Chart.js运行时就找不到moment了。查了很久文档才搞明白,需要在webpack配置里明确指定external dependencies的处理方式:

// 针对Chart.js的特殊处理
splitChunks: {
  cacheGroups: {
    charts: {
      test: /[\/]node_modules[\/](chart.js|moment)/,
      name: 'charts',
      chunks: 'all',
    },
    quill: {
      test: /[\/]node_modules[\/]quill/,
      name: 'quill',
      chunks: 'all',
    }
  }
}

但这样做又有新问题:用户可能只用到了图表功能,却要下载整个图表包,包括moment这种重型库。后来改了个思路,根据业务模块来切分而不是按技术栈切分。

懒加载组件的实际应用

具体到组件层面,主要是对话框、详情页这些用户不一定用的。原来是一次性全部加载,现在改为按需加载:

// Modal组件懒加载
const LazyModal = React.lazy(() => 
  import(/* webpackChunkName: "modal" */ './Modal')
);

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <LazyModal />
    </Suspense>
  );
}

这里要注意fallback的处理,不能让用户看到空白或者丑陋的loading。我们团队设计了个统一的骨架屏,至少视觉上不会卡顿。另外chunkName注释也很重要,能让生成的文件名更有意义,方便调试。

不过lazy loading也不是万能的,对于频繁切换的组件,反复加载反而会影响体验。所以后来增加了预加载逻辑:

// 鼠标悬停时预加载
function NavigationLink({ to, children }) {
  const preload = () => {
    import(./pages/${to});
  };

  return (
    <Link to={to} onMouseEnter={preload}>
      {children}
    </Link>
  );
}

服务端渲染的兼容问题

项目还涉及到SSR,这里又是一个大坑。React.lazy和Suspense在服务端不支持,直接报错。折腾了好久才找到解决方案,需要用react-loadable或者自己封装SSR友好的懒加载组件。

最终选择了loadable-components,配置如下:

import loadable from '@loadable/component';

const LoadableComponent = loadable(() => import('./MyComponent'));

function App() {
  return (
    <LoadableComponent fallback={<div>Loading...</div>} />
  );
}

配合Babel插件做静态分析,确保服务端渲染时能正确处理。这块花了不少时间,官方文档写的有点模糊,很多细节都是踩坑试出来的。

性能数据和最终效果

优化完成后,首屏加载时间从原来的8秒降到了3秒左右,打包体积从15M+减到了6M左右。具体的chunk分布是这样的:vendors.js 2.1M(第三方库),main.js 1.8M(核心业务逻辑),其余按模块拆分,单个chunk基本控制在200-500KB之间。

用户体验提升还是挺明显的,特别是移动端。不过还有些小问题,比如某些深度嵌套路由的加载时机控制不够精确,偶尔会有短暂的loading闪烁。这个问题暂时没完美解决,但影响不大,后续版本再优化吧。

监控数据显示,用户流失率下降了约15%,算是达到了预期目标。虽然代码复杂度增加了一些,但性能收益还是很值得的。

回头看的一些感悟

整个代码分割的过程比我预想的复杂多了,特别是要平衡加载速度和请求数量。一开始只考虑了文件大小,忽略了HTTP请求数的影响,走了不少弯路。

第三方库的处理也是个技术活,不是简单的按需加载就能解决问题。有时候为了保持向后兼容,还得做一些妥协。另外SSR的支持也增加了不少工作量,但最终效果还是不错的。

总体来说,这次优化让我对webpack的chunk分割机制有了更深的理解。虽然配置比较繁琐,但掌握了规律后其实也不难。最重要的还是要根据具体的业务场景来调整策略,不能照搬网上的教程。

以上是我踩坑后的总结,希望对你有帮助。代码分割这个话题其实还挺复杂的,特别是大型项目,各种边界情况要考虑周全。有更优的实现方式欢迎评论区交流。

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

暂无评论