Webpack构建速度提升的5个实战优化技巧

Newb.怡萱 优化 阅读 828
赞 15 收藏
二维码
手机扫码查看
反馈

构建慢到怀疑人生,打包5分钟起步

昨天上线前最后优化一波,本地改完代码保存,热更新直接卡10秒起,气得我差点把键盘扔了。这哪是开发,简直是渡劫。更离谱的是 build 一次要5分多钟,CI流水线跑着都快睡着了。忍不了,必须动手干它。

Webpack构建速度提升的5个实战优化技巧

其实早几个月前就想过搞 Webpack 优化,但那时候项目小,build 40秒也能忍。现在加了一堆图表库、地图 SDK、富文本编辑器,还上了微前端,chunks 堆成山,vendor.js 直接飙到6MB,不优化真不行了。

先看问题在哪:不是玄学,是数据说话

第一步当然不能瞎改,得先分析瓶颈。我上 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',
      openAnalyzer: false,
      reportFilename: 'bundle-report.html'
    })
  ]
};

跑完 build,打开 report,好家伙,node_modules 占了85%,其中 lodashmomentecharts 是三大巨头。尤其是 moment,带了所有语言包,实际只用了中文,纯浪费。

这里我踩了个坑:之前听说 externals 能把大包提到 CDN 上,立马动手把 echartsvue 都 external 掉,结果 QA 环境报错,CDN 挂了页面直接白屏。后来想想太天真——公司内网环境根本没法稳定走外链,而且移动端弱网下雪上加霜。这个方案 PASS。

拆包策略:别让一个文件扛所有

接着试 code splitting。之前虽然开了 splitChunks,但一直用默认配置,等于没开。这次仔细调了下:

// webpack.prod.js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\/]node_modules[\/]/,
          name: 'vendors',
          priority: 10,
          reuseExistingChunk: true
        },
        echarts: {
          name: 'chunk-echarts',
          test: /[\/]node_modules[\/](echarts|zrender)[\/]/,
          priority: 20,
          chunks: 'async',
          minChunks: 1,
          minSize: 0
        },
        moment: {
          name: 'chunk-moment',
          test: /[\/]node_modules[\/](moment|moment-timezone)[\/]/,
          priority: 20,
          chunks: 'async',
          minChunks: 1,
          minSize: 0
        }
      }
    },
    runtimeChunk: {
      name: 'runtime'
    }
  }
};

这里的重点是 prioritytest 匹配要准。我一开始把 chunks: 'async' 忘了加,结果同步加载的模块也被拆了,首屏 JS 反而多了几个请求,LCP 更差了。折腾了半天发现,得控制只对异步 import 拆。

另外 minSize: 0 是为了确保哪怕很小也强制拆(比如 moment 就算只有几十KB我也想单独打)。但注意这会增加请求数,得结合 HTTP/2 来用,不然得不偿失。

moment 这种老古董怎么治?

moment 太重了,但业务里到处在用,短期没法替换成 dayjs 或 luxon。怎么办?插件救场:

// webpack.config.js
const webpack = require('webpack');

module.exports = {
  plugins: [
    // 只保留中文
    new webpack.ContextReplacementPlugin(
      /moment[/\]locale$/,
      /zh-cn/
    ),
    // 替代方案:也可以用 IgnorePlugin 干掉所有语言包
    // new webpack.IgnorePlugin({
    //   resourceRegExp: /^./locale$/,
    //   contextRegExp: /moment$/
    // }),
  ]
};

这一招直接砍掉 moment 里其他90%的语言包,体积从260KB干到30KB左右,效果立竿见影。不过要注意,如果以后真要加英文支持,得记得放开这个配置。

babel 也能压一压?

然后想到 babel 编译出来的代码是不是太“胖”了。我们用了 @babel/preset-env + useBuiltIns: 'usage',理论上已经按需引入 polyfill,但还是有点冗余。

后来试了下 babel-plugin-transform-imports,对 lodash 进行自动按需引入:

// .babelrc
{
  "plugins": [
    ["transform-imports", {
      "lodash": {
        "transform": "lodash/${member}",
        "preventFullImport": true
      }
    }]
  ]
}

配合这个,还得把原来 import { debounce } from 'lodash' 改成 import debounce from 'lodash/debounce',否则 preventFullImport 会报错。但这一步可以用 codemod 自动批量替换,我写了个 jscodeshift 脚本跑了一遍,省事。

HardSourceWebpackPlugin?快但不稳定

听说 hard-source-webpack-plugin 能缓存中间编译结果,提升二次 build 速度,立马加上:

const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');

module.exports = {
  plugins: [
    new HardSourceWebpackPlugin({
      environmentHash: {
        root: process.cwd(),
        directories: ['node_modules'],
        files: ['package-lock.json', 'yarn.lock']
      }
    })
  ]
};

第一次 build 会慢一点,第二次开始快了不少,本地 save 热更新从10秒降到3秒左右,爽。但问题来了:有时候改了 babel 配置没生效,清了 node_modules 重建也不行,最后发现是 hard-source 缓存没更新。只能手动删 node_modules/.cache/hard-source 目录,或者加个 npm script:

"scripts": {
  "clean-cache": "rm -rf node_modules/.cache/hard-source"
}

所以这玩意儿适合稳定期用,开发频繁切分支的时候容易出诡异问题,建议非必要别上。

最终成果:build 从5分钟干到1分半

综合下来,最终优化点有这些:

  • 精细化 splitChunks,大库单独拆包
  • moment 语言包裁剪
  • lodash 按需引入 + babel 插件辅助
  • 启用 HardSource 缓存(开发环境)
  • 图片资源上压缩(image-webpack-loader,略)

最终 production build 时间从 5m12s → 1m28s,gzip 后主包总大小从 6.8MB → 2.3MB,热更新响应时间从平均10s → 3s以内。虽然还没到理想状态,但至少能正常干活了。

顺带一提,后来发现 CI 上 build 慢主要是因为机器 IO 差,换了 SSD 实例后又快了20秒。所以说硬件也不能忽视……

还有两个小毛病没解决

改完后有个副作用:chunks 太多导致 index.html 里 script 标签一堆,看着闹心。试了 html-webpack-tags-plugin 自动注入倒还好,但加载顺序偶尔有问题。目前靠 initial / async 控制优先级勉强稳住。

另一个问题是某些异步组件 loading 态变多了,因为拆得太细,每个图表都单独 chunk,用户点一次才加载。这个后续打算做预加载:

// 在路由切换时提前 load
component.lazy().then(module => {
  // 可以加个 preload hint
  const link = document.createElement('link');
  link.rel = 'prefetch';
  link.href = '/static/chunk-echarts.js';
  document.head.appendChild(link);
});

不过还没上,先观察数据再说。

以上是我踩坑后的总结

Webpack 优化真是一步步试出来的,没有银弹。你现在看到的这些配置,背后是我删了十几次 node_modules 重装换来的经验。有些方案看起来漂亮,实则线上翻车;有些土办法反而最稳。

如果你有更好的方案,比如用 ESBuild 或 SWC 做 loader 替代 babel,欢迎评论区交流。我最近也在看 Vite 迁移的可能性,但老项目改造成本高,暂时先拿 Webpack 挺着。

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

暂无评论