Webpack打包速度优化实践

随着项目的增大,webpack的打包速度已成前端工程师的“不可承受之重”。最近对团队内某项目的打包速度进行了一些优化,本文没有具体的配置教程,只提供一些优化思路,供启发和参考。

更换更快的打包工具

1. bundler:代表webpack、parcel

parcel和webpack主要区别

  • parcel采用了类似于webpack中thread-loader的方式进行并行构建
  • parcel内建了类似于dll的缓存策略(webpack5中也内置了缓存策略)
  • parcel的HTML、JS 和 CSS 分别是通过 posthtml、babel 和 postcss处理的

更新webpack版本

webpack5内置了持久性缓存和跟好的缓存策略,Tree Shaking性能提升,摇掉更多无用代码。
尝试改善与网络平台的兼容性。

2. noBundler:代表snowpack、vite

主流的浏览器版本都支持直接使用 JavaScript Module。
HTTP/2可以合并请求来优化模块并发请求性能。
vite针对复杂的第三方库,会自动识别并提前打包缓存起来,避免过多http请求(类似于dll)

从webpack迁移到vite
  • 项目里使用了create-react-app,内置了很多配置项(迁移成本高)

  • 团队内部构建的SDK库由于打包后的语法不标准,webpack不报错,vite会报错(更改SDK)


    css import语句只能在顶层
  • 项目里使用到了monaco-editor,官方提供的插件只有webpack版本,迁移到vite不好处理(团队内部写了rollup插件)

  • vite里less没有autoimport的配置(写死在less inject里)

  • vite变量形式和webpack不一样,webpack可以识别process.env.xxx,vite是import.meta.xxx(想的是开发用vite,build用webpack,需要兼容)

  • 项目里用到的scoped-css-loader等loader没有vite版插件(类似于@vitejs/plugin-vue-jsx,写一个插件在vite里使用babel)

  • webpack可以很好的处理web和node共用变量(webpack可以转换cjs,vite需要通过optimize项配置)

web和node共用变量

由于webpack到vite的迁移成本比较高,vite build时的速度和webpack也差不多,使用hardsource等插件后webpack dev的速度也是可以接受的,决定还是在webpack体系下做优化

升级webpack

截止目前,cra正式版还停留在webpack4版本,好在alpha版升级到了webpack5,可以使用react-app-rewired start --scripts-version react-scripts来指定react-scripts版本。实测升级到alpha版后速度更慢了,在优化了配置后速度也没有明显提升。
猜测原因是webpack5的缓存策略主要是dev的时候有用,本质上和hardsource没有太大区别,但首次生成缓存的速度比hardsource稍快,生产环境中一般会禁用或重新构建缓存。

esbuild官网上也可见webpack5比webpack4的速度更慢了

替换babel
  • babel负责将js、ts、jsx等格式代码转换为js。相似功能的还有:tsc、esbuild、swc。

  • tsc在转换时默认会检查ts类型,插件环境没有babel好,一般都会使用babel。

  • babel不会检测类型,只负责把ts转换到js,速度会比tsc要快,插件生态支持的也好一些(react热更新、vue3jsx语法转换官方都只提供了babel的插件)注:swc与nextjs合作,内建了react热更新支持。esbuild调研后未发现支持,vite的react热更新插件还是使用了babel

  • esbuild、swc也不会检测类型,他们会用go和rust生成的二进制文件处理js或ts,速度比babel更快。如果没有使用babel插件,可以在webpack中直接用esbuild-loader或swc-loader替换babel,但插件生态几乎为零(减少了暴露给插件的API以提升速度,esbuild transformer无法使用插件)ps:Vercel团队最近吸纳了swc的作者,并在新版nextjs里提供了替换babel的选项,未来可期

  • 用babel、esbuild和swc时,需要使用fork-ts-checker-webpack-plugin校验ts类型(cra默认启用)

  • 一般在使用tsc打包的时候,也会关闭类型检查并使用fork-ts-checker-webpack-plugin检测类型以优化速度。

  • fork-ts-checker-webpack-plugin是一个webpack插件,它会在打包时fork出一个进程并行进行检查,可以更好的利用多核cpu的能力,过程中几乎不影响webpack主进程,故可以优化速度。

替换terser

terser负责压缩babel和webpack生成的产物,去掉无效代码,去掉日志输出代码,缩短变量名,生成source-map等,可以有效压缩体积

  • terser是js写的,压缩时的内存占用、cpu占用都很高,虽然有cache、多进程等选项,但提升并不理想

  • esbuild带有压缩的功能,使用esbuild替换terser做压缩,可以带来比较大的速度提升,但生成产物比terser压缩的大10%,在中后台项目且会拆分文件的场景下,文件尺寸不是痛点。

  • 目前团队有测试和预上线环境,目前测试和沙箱环境也会压缩代码,可以在测试和沙箱构建时禁用压缩,经测试可以带来40%左右的速度提升

source-map

webpack提供了如下的source-map选项,
不同选项的构建速度和性能以及适用场景都有很大差别,在这里不详细叙述


source-map的速度以及适用场景比较
  • 团队测试和沙箱环境构建时可以不分离sourcemap(或者用cheap-eval-source-map映射到行)以提升速度。

  • 用替代品生成sourcemap(用wasm-webpack-sources替换webpack-sources)


    wasm-webpack-sources
  • 更新webpack-sources的版本,ps: nextjs团队在博客中提到,升级webpack-sources版本后,构建source-map的速度比不开启仅多花了11%左右的时间


    nextjs博客
  • 延迟构建source-map,由于线上环境不会暴露source-map,可以先关闭source-map构建出一份产物到线上,然后打开source-map再在后台打包一次并将source-map上传到Sentry等监控平台和线上文件映射上。

使用多进程打包

happypack和thread-loader


image.png
  • happypack已经很多年没有维护了,核心原理是启用多进程,多个loader并行处理文件,happypack开发人员建议,如果使用webpack4及以上更推荐使用thread-loader,thread-loader做的事情和happypack一样
  • thread-loader是webpack官方推荐的,原理是将loader放在单独的一个worker进程内处理,但实测下来babel-loader前放置thread-loader后的速度更慢了


    无thread-loader

    有thread-loader

    开启thread-loader时,监测到有一瞬间fork出了很多node进程,但接下来就消失了。可能是因为以下限制(用了babel-plugin),实际thread-loader并没有启用成功


    thread-loader的限制
  • 在看到thread-loader官方的用例后,推测也可能是由于项目是ts的,webpack需要递归的调用babel-loader来解析语法和生成依赖关系,进程间通信会消耗大量时间


    thread-loader官方测试用例
使用缓存

cache-loader、开启loader自带的cache选项、dll、hardsource、webpack5

  • cache-loader也是webpack官方推荐使用的,加在耗时较长的loader前面,在heavy loader执行前,对比要处理的文件和缓存文件的mtime,mtime没变的话直接取缓存文件,实测构建时间会变长。
    loader: ['cache-loader', 'other-loader']
    简单看了一下,cache-loader分两个阶段:pitch和loader
    pitch阶段的处理流程是:cache-loader -> other-loader
    loader阶段的处理流程是:other-loader -> cache-loader

pitch阶段根据当前正在处理的文件,读取.cache目录中对应的cache文件,对比mtime判断是否可以复用
loader阶段依赖pitch阶段的判断,如果pitch阶段判断当前文件的缓存失效了,loader阶段就要重新生成缓存。

  • babel-loader自带了cache选项,但babel-loader的cache必须经过一次编译,才会将索引的文件与文件编译结果缓存在内存中。在后续的编译过程中,如果发现索引的文件已经缓存过了,才会直接引用已经编译缓存的结果。(还是会有编译的过程)
  • dll动态链接方案


    DLL和缓存的区别

    可以将共用不经常改变的依赖(如react、react-dom、vue、antd、moment)打包成dll
    webpack打包引入库时入口会被动态指向dll文件里,实测是有用的,但dll方案在18年左右被社区的脚手架抛弃,大概意思是使用dll会大量增加维护的成本,(我在使用时也遇到有些插件打成dll后报错),webpack4相比webpack3带来的打包速度提升使得dll有些得不偿失


    cra

    vue-cli

hardsource和webpack5
hardsource和webpack5持久化缓存的方案类似,webpack5持久化缓存结果至硬盘上,第一次编译文件的时候,计算文件的hash。将编译结果与hash关联起来。第二次编译文件时,首先加载本地缓存结果,进入正常编译环节时,对编译的文件再次求hash,如果此hash在缓存库中已经存在了,那么将直接跳过编译环节,直接输出编译结果。
这两种方案都是dev的时候才会有用(记得官方有个issue说实验基于缓存build可能会有5%的概率出错),升级到cra5之后发现复用缓存的条件极其严格,每次重新build时都会重新构建缓存,hardsource首次构建的消耗时间比较大,webpack5由于的缓存是基于webpack4构建时的内存改造得来,首次构建带来的额外时间消耗并不大,二次构建hardsource和webpack5的速度相当。

external
  • external之后会把webpack会把import语法转成访问全局变量,直接忽略语法解析,也不会调用loader。
    external的主要问题是,有些库之间相互依赖,比如antd依赖moment和react,mobx-react-lite依赖react,也依赖了mobx,引入顺序和cdn质量需要额外花精力维护,很多库官方也没有提供umd的包,使用第三方的umd包可能会有问题。
  • react中比较稳定的umd库有react react-dom antd moment,可以放心external掉

external可以配合Systemjs使用,systemjs支持直接引入esm而不必去找umd,webpack4也内建支持把libraryTarget改成SystemJs的形式,直接使用esm格式包时不需要type=module,也可以进行动态加载改造,微前端框架single-spa也使用这种方式进行依赖动态加载



                    
                    

你可能感兴趣的:(Webpack打包速度优化实践)