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也使用这种方式进行依赖动态加载

<script type="systemjs-importmap">
        {
               imports": {
                   "react": "//gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js",
                   "react-dom": "//gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js",
                   "moment": "//cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.0/moment.min.js",
                   "antd": "//unpkg.com/antd@4.6.6/dist/antd.min.js",
                   "handsontable": "//cdn.jsdelivr.net/npm/handsontable@8.4.0/dist/handsontable.full.min.js",
                   "braft-editor": "//cdn.jsdelivr.net/npm/braft-editor@2.3.9/dist/index.min.js",
                   "lodash": "//cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js",
                   "mobx": "//cdnjs.cloudflare.com/ajax/libs/mobx/5.15.7/mobx.min.js",
                   "webpackBundle":"./output.bundle.js"
               }
           }
       </script>
<script>
System.import('xxx').then((res) => {
   System.import('webpackBundle');
 })
</scirpt>
webpack5 Module Federation预编译node_modules

module federation 是 webpack5 提出的新特性,含义为模块联邦。主要是使用于微前端场景,可以在运行时动态引入子应用。
优化思路:利用 webpack5 的 module federation 特性,构建一个虚拟的 federation 应用,项目直接仅使用编译好的依赖,这样就可以直接减去热更新和启动时对依赖的重新编译。实际和dll的原理类似?但速度比dll要快很多

module federation

umi使用module federation优化的思路

业内脚手架umi已在这方面做出了实践,但没有提供通用的webpack插件,可以期待未来社区的产出

硬件更换

由于JS的多线程能力不佳,webpack在打包时更吃CPU单核性能,多核性能几乎(在多核服务器上测试还没有笔记本块)没有用。
截止目前,苹果还没有发布第二代自研桌面处理器,(APPLE M1x在制程不变的情况下无法大幅提高主频,大概率会通过堆核心、增加GPU、增加总线带宽、提高内存频率等方式做优化,可以预见单核性能不会有类似Intel -> M1的巨大提升)基于的硬件选购建议是,M1 16g配置的Mac电脑在未来几年内都会是非常适合前端开发者使用的(传统x86芯片的电脑在不改变封装逻辑的前提下预计相同价位提升至M1的单核性能水平需要很长时间,基于JS的打包生态迁移到rust和go也需要很久)。

使用nice命令提高本机webpack进程优先级后速度也略有提升(需要root权限)

总结

基于上述调研和项目的业务场景,最终决定应用以下优化

  1. 用esbuild替换terser作压缩
  2. 测试和预上线环境禁用压缩并启用cheap-source-map
  3. 应用dll
    以下是在本机的一些实验结果


    external和dll

    esbuild压缩测试

    优化后vs优化前生产环境构建提升了大约43%的时间,测试环境构建提升了大约58%的时间


    项目测试

一些困惑

  1. Module Federation为什么比dll快
  2. 能不能使用JS实现多线程打包

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • 最近给项目进行webpack优化,尝试过几乎所有方法,一共26条,列举在此。 优化webpack,首先明确优化目标...
    seaasun阅读 2,102评论 0 4
  • 速度分析webpack有时侯打包很慢,而我们在项目中可能用了很多的plugin和 loader,想知道那个环节慢,...
    鱼儿灬天上飞阅读 427评论 0 0
  • 一、入坑初探 1. 设置项目为私有 我们只需要在package.json文件中配置,因为是私有项目不需要向外部暴露...
    zxhnext阅读 1,982评论 0 15
  • 前言 笔者最近在整理关于 webpack 相关的知识点,一方面是因为自己掌握的知识点比较零碎、不够系统,有时候碰到...
    darrell阅读 1,015评论 0 4
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,471评论 28 53