巨石应用解决方案 ❌ 📦
发布于 1 个月前 作者 xiaoxiaojx 1500 次浏览 最后一次编辑是 6 天前 来自 分享

hi all, 分享一个大型系统打包加速的文章,原文链接: https://github.com/xiaoxiaojx/blog/issues/20

image

背景

最近一段时间陆续有同学吐槽, 现有的开发环境打包太慢了, 原话如下

  • 同学 a: xxx 项目冷启动,刚刚计时是 3分45秒 左右,有空看看是否有优化空间哈。 视频是 xxx 项目 f-xxx-6196 分支(yarn dev回车后开始计时)
  • 同学 b: 本地运行太慢了,想砍人
  • 同学 c: 我们项目启动很慢,咋整
  • 同学 d: xxx 项目编译时间太长了,改个东西容易电脑卡,不排除我的电脑问题,体验很差

0cbf92807d10b552fccc77993449a821c97ecb14

截图为该项目的 gitlab 信息

也确实, 3分45秒 🐢🐢🐢 的等待时间谁又受得了 😣 ! 那么我们现在的脚手架的问题出在哪里 ?

现有脚手架是基于 webpack 打包,其实做了 如 babel, ts 等缓存优化,甚至 hard-source 这样的持久缓存。但是由于需求的快速迭代,一切换分支导致了大量 node_modules 依赖变化需要重新生成新的缓存,使得每次打包缓存等优化等都失效了, 此时我也埋下了 nopack (不打包 ❌📦 ) 的仇恨种子!

所以当现有的优化手段都命中的时候,时间才能勉强减少到 40s 🐢 左右。

image

webpack5 也尝试去解决慢的这个问题, 比如新增了一个比较重量级的持久缓存的功能, 不过从 esbuild 官方给大家的数据来看, webpack5 却是最慢的 ! 😢

这可能是 webpack5 被黑得最惨的一次… 因为第一次打包有一部分时间是在生成缓存了, 怎么不拿第二次缓存生效后的时间来遛一遛比一比了?在这种情况下, 我们升级 webpack5 可能还是解决不了痛点, 分支一来回切换又回到了解放前!

当项目逐渐膨胀时, 似乎是已经到了 webpack 的瓶颈, 近期特别火的 vite 与 snowpack 或许才是真正的解决方案 ?

vite 与 snowpack

image 最终我想到的是接入 vite 或者 snowpack 解决现有的问题, 关于提到的这些工具的原理, 有不少文章都讲得很好了, 这里就不做过多的介绍。

snowpack 刚出来不久就认真充满好奇的读了他的代码, 发现里面有不少写死的地方, 比如 define 是通过字符串的替换去实现, 这明显就会误伤无辜, css 的 import 也没处理, 最近作者也说会交给社区去维护, 自己有些力不从心了。

Real-world testing is super important. I’m sure that sounds cliche, but its true. We had a few starter projects that we could test Snowpack against, but they were all small and simple. This created a huge experience gap between our internal projects and our actual users.

文章出自 6 More Things I Learned Building Snowpack to 20,000 Stars (Part 2), 而我们更接近业务, 近 100 个大型项目, 包含各类系统与 h5, 几乎能踩完所有的坑。

To be honest, I’m not sure where Snowpack goes from here. I burnt out on it at the end of last year, and haven’t found the energy to return. Usage and downloads began to trend down and the community has gotten quieter.

At the same time, Vite (that Snowpack alternative that now powers SvelteKit) is taking off. To their credit, they do a lot of things really well. The good news is that two tools are very similar and easy to switch out. Even Astro is experimenting with moving from Snowpack to Vite in a future release.

虽如此 snowpack 依然是个可敬的产品 🔥 ~

所以综合体验下来还是 vite 值得信赖, 如果此时是新项目毫无疑问我推荐大家使用 vite, 但是我们这是老项目…

何为老项目? 就是之前升级了一个 ts-loader 的大版本, 有俩项目测试环境白屏了。在试图接入 vite 时遇见了 cssModules 解析报错、js scss import 解析路径失败等问题。

到这里我认为 vite 是没有任何问题的, 有些稀奇古怪, 百花齐放的问题的代码就得你自己去改, 当然实际继续挣扎下去可能会发现更多的奇葩问题, 其解决沟通成本和时间成本是无法估计的。

nopack

最终我决定自己开发, 并且过渡阶段希望可以随意切换新的 ES Module 开发模式与现有的 webpack 模式。

nopack 将以全局安装的形式存在, 即对现有项目 0 侵入,对线上环境 0 危险,对接入的项目源代码可以做到 0 修改就可以运行起来。

原理图

image

时序图

image

Q: 如何做到项目 0 改动接入 ?

  • vite 的大部分零件 jsResolver, sass 等的处理是偏向于 rollup 体系。要想无缝兼容现在项目中配置的 _moduleAliases 与 sass 中各种 import, 于是这部分我采用的 webpack 的零件, 主要是 enhanced-resolve 与 sass-loader 的处理逻辑。
  • 另一点不太重要的是发现大家都喜欢用 websocket 通信, vite、snowpack、webpack-dev-server 都是如此, 而 nopack 这里使用的是 sse。

Q: nopack 这个名字是啥意思 ?

  • 本意是只想做一个转译服务, 竭尽全力的不打包 ❌📦 。最初是想仅对 ts 与 jsx 文件请求进行一个转译后返回, 即总共只进行一次 AST 的操作。想法还是天真了, 因为 cjs to ejs 是一个不成立的事情, require 是运行时的, import 是静态的, 可能你想到了 import() 函数不也是运行时的吗 ? 不过 import() 返回的却是 Promise !

  • 事实证明初期只想做转译的思想不成立 👇 image

Q: 如何彻底消除 cjs 这些语法了 ?

  • 那就只有预构建打包了, 合成一个文件, require 的代码直接塞在对应的位置了, 哪还会有 require 这些东西了

Q: nopack 不是坚决不打包吗 ?

  • 为了这些不合法模块, 只能忍气吞声了, 这里 nopack 与 vite 这里有些区别, nopack 会试图判断这个 npm 包是不是 esm 的包, 比如 package.json 中有 module, browser 字段, 或者 import 的是 es, esm 目录的文件如 “packageA/es/a.js” 这种或许也是 esm 就不进行预构建了。这样下来项目 xxx 🔍 扫描到了 336 个包, 最后只会对 199 个包进行预构建 📦 。

Q: 这样看来比 vite 更快 ?

  • 快是快了, 但是具有迷惑性行为的包也不少, 比如某个包也声明了 module 字段, 但我其实是 cjs 的代码。更有甚者一个包中大部分文件是 esm 的, 有1 ~ 2 文件个是 cjs 的🤯! 没办法, 你不是 nopack 吗, 这些包我忍你了, 我 hardcode 到一个数组中把你们加入黑名单。所以进行全量预构建的 vite 作为一个通用的工具可能也是无奈之举。

Q: 最终接入的效果如何 ?

  • 号称 3分45秒 的项目通过 esbuild 预构建大概只花 8s 左右, 然后页面刷新时会对 src 下的 ts 与 jsx 通过 esbuid 进行转译, 大概 4s 的时间, 所以最终开始预构建到页面完全展示出来只花了 12s, esbuild 远比我想象中的更快 ⚡️⚡️⚡️。

Q: esm 开发的劣势?

  1. 刷新页面的等待时间会慢一点
    • esm 模式强调转译, 浏览器运行阶段代码从入口文件一直往下运行下去的时候, 会不断有 import 语法发起新的文件请求, 对于 jsx 与 ts 文件的请求还需要通过 esbuild 进行转译, 对于 scss 请求需要 dart-sass 转译, 直到所有的代码运行完成。
    • webpack 模式强调打包, 打包阶段会把入口文件开始依赖分析, 最后打包出一个 main.js 交给浏览器运行, 所以浏览器运行阶段就会快很多。

所以这里可以对 node_modules 的文件进行一个强缓存的优化, 以及通过 react-refresh 去做热更新, 避免页面刷新操作

  1. 对性能有一定的要求
    • 在其中一个项目接入时, 使用 windows 开发的同学说快是快了, 但是页面刷新的那一刻有点卡 🤔, 排查时发现这个项目 network 中发了 3000 多个 js 的请求, chrome 把 cpu 拉满了。所以 nopack 不得不又退步 😢, 只能把 node_modules 的包进行全量的预构建来达到合并更多的文件, 一下请求数降低到了 700, chrome 即使拉满了 cpu 也是短暂就下来了。所以最终 nopack 又进行了妥协, 采用了全量预构建的方式。事实证明文件请求减少页面刷新时白屏时间减少了, 预构建也只增加了 2s 左右的时间, 开发体验会更好一点。

如果全量预构建 node_modules 还没解决该同学的问题, 我也开发了一个基于 esbuild ⚡️ 打包的开发环境开发的版本, 即像 webpack 一样, 把 src/index.js 为入口打包最后产出为一个 main.js 文件的模式, 对于这个 3分45s 的项目采用 esbuild ⚡️ 打包也仅花了 15s 左右!

  1. esbuild 不会对 ts 类型进行检查
    • 可以参考 fork-ts-checker-webpack-plugin 实现一个插件配合 esbuild 检查 image
  2. 开发环境与生产环境不一致
    • esm 由于 js 文件请求过多, 页面展示的速度慢。生产环境还是得走现有的打包逻辑(文件合并, 丑化等), 开发环境与生产环境不一致可能会出现本地没有问题, 测试生产有问题的情况。

Q: 生产环境能用上了吗 ?

  • nopack 也支持生产环境构建, 但是核心的老项目是不会使用 ⚠️⚠️⚠️ (不会为了这三分钟丢了饭碗 😨)。不过新的内部项目将会默认采用 nopack (其实这里可以用 vite 了, 为了不存在重复的工具就用 nopack 吧)。另外 nopack 生产环境打包是采用的 esbuild ⚡️ , vite 采用的是 rollup 📦 。

Q: esm 开发的时代是不是已经来了 ?

  • 还在观望的同学也可以上了, 截止 2021-10-29 已经有 9 个项目接入了 nopack🚀🚀🚀, 基本都是 0 改动接入运行。虽然期间暴露了各种各样的问题, 但是先驱者 vite 基本都有了各种解决方案可以进行参考。

Q: vite、snowpack、unbundled 这些推荐用哪个 ?

  • 个人建议有 v 选 v。

swc 与 esbuild

image 选择编译器的时候, 调研了 swc 与 esbuild

swc

nextjs 已经实验性使用 swc。

image

esbuild

vite 与 snowpack 都是使用的 esbuild。

目前仅发现范型函数赋值给变量编译会有问题。

image

测试数据

下面是 webpack 模式下测试 esbuild 与 swc 相关的测试数据

  • 项目: xxx
  • 版本: 91a69d4e
  • 编译速度: 首次无缓存状态下的构建时间

image

  • 编译兼容:
    • swc 编译 componets/ModalForFirstTime.js 等文件存在问题,其他路由页面表现正常。
    • esbuild 编译 js 的装饰器不支持,故换成 ts 模式编译 js 文件,其他路由页面表现正常。

@evanw decorator spec unfortunately still seems like it’s a long way off, both based on low activity and based on how many things it looks like they still have to figure out.

  • 小结:
    • esbuild 与 swc 速度相差无几,但是 esbuild 对老项目的编译兼容性高于 swc。

彩蛋

10月27日看到了字节大佬开源了基于 esbuild 的工具 unbundled, 当时还在想要是自己英语学好一点, 不用中式英语, 还指不一定也叫 unbundled 了 😄

最后的话

站在巨人的肩膀总是能看得更远, 首先感谢 vite 与 snowpack 从 CommonJs 到 ES Module 过渡时期提供的各种解决方案, 最后感谢背后的 esbuild 与 swc 提供的强大的心脏 ⚡️(🤔 看样子 js 的工具链的巨大变革会被 rust / go 拉起帷幕)。

image

谢谢阅读 ~

回到顶部