swc 很好,也还不够好

swc 很好,也还不够好已关闭评论

前情提要

这是一篇总结性文章,先抛出结论:swc 暂时还无法在生成环境使用,swc 很好,也还不够好。

简单介绍下开始实际使用和接触 swc 的背景,我大概是一年多以前开始关注 swc,当时是有学习 Rust 的诉求。

然而真正开始和 swc 打交道是在最近几个月最大的原因是 Rax App 构建实在太慢了,一个 Web MPA 模板就需要 8-10s 左右的时间,如果业务复杂度提升或者开启多端构建,那更是惨不忍睹,其中最大的一块耗时就在 Babel 这块。没错,就是这个在掘金充斥各种“爽文”、使用很广泛的编译工具,从另一方面来说,现在都在说 webpack 的构建速度不如 vite,其实某个角度来说 webpack 是背了黑锅的。

而 Babel 编译慢的原因完全不是自身架构问题,相反 Babel 已经相当健壮和成熟了,真正还是语言的劣势,本身用 js 写的 babel 无法使用多核 CPU 优化编译任务处理,同时相较 swc、esbuild 编译成二进制在 node 执行而言,也有一定的劣势。

为什么选择 swc?

在我选择 swc 之前,团队内已经为 icejs 提供了 esbuild 的插件,主要使用来替换 Terser Plugin 提升代码压缩阶段的速度。最开始依然想看看 swc 的原因是,相比于 Go,我对 Rust 更有好感。后来再一看,swc 相对于 esbuild 最大的优势是它最想做的事情是替换 Babel,这个就和我的目标达成了一致,同时正因为这一点,它提供了 esbuild 所没有面向 JS 生态的插件系统。

简单介绍下这套插件系统,最早的时候 swc 是基于 neon[1] 做的 JS binding,这个库我也在用 Rust 实现 renderToString 的时候有使用过(binding 消耗远大于 native addon 带来的提升而暂时搁置),后来由于 N-API 发布,swc 就切换到了@太狼写的 napi-rs(我现在又去看了眼,neon 也迁移到了 N-API),迁移之后 swc JS plugin 的性能提升了两倍左右,具体直接看太狼的这篇文章[2],swc binding 相关的代码可以看下这里[3]

swc 所带来的构建性能提升是非常显著的,3 月份的时候我尝试将 Rax App 默认的构建能力接入 swc,Dev 阶段的构建速度从 8s 下降到 2s,Build 阶段的构建速度从 12s 下降到 3s,同时我又使用一个复杂的业务项目做了下实验,原项目从 1 分 40s 下降到了 30s 附近(其实很早的时候 webpack 主流程构建就结束了,只是业务有一些定制 assets 的插件阻塞了任务完成)。

迁移到 swc

真正准备让团队的 ice.js 和 Rax App 开始渐进式迁移到 swc 是最近一两个礼拜的事情,具体可以看下这个 RFC[4]。JS Compiler 这样的底层链路改造其实是一件非常谨慎的事情,尤其对于 ice.js 和 Rax App 这样被广泛使用的研发框架而言。

在迁移过程中,需要思考的点比较多,一方面是基础链路的架构设计,另一方面是可能带来的副作用。**基础链路的架构设计:**1. 框架自身的需要能做到和 babel 解耦;2. 自身使用的插件需要使用 swc plugin 再实现一遍;**可能带来的副作用:**1. 对 bundle 体积的影响;2. 转换后的代码是否存在兼容性问题;3. 如何快速的将 babel 插件转换到 swc 插件;4. swc 现有的能力是否满足从 babel 切换到 swc。

迁移这件事本身不是本篇文章的重点,所以接下来我主要围绕可能带来的副作用来讲。

体积问题

就像篇头说的一样,swc 很多行为都是站在 babel 巨人的肩膀上在做,这其中就包括 @babel/helpers,swc 可以通过 swcOptions.jsc.externalHelpers 来将 _typeof 工具方法抽成一个包,从而避免在最终构建的时候打入多份实现。

但是,swc 是自己做的 helpers — @swc/helpers(甚至比 @babel/helpers 做的丰富),这就带来一个问题,JS 社区中的三方包几乎都是通过 babel 编译,也就是说,项目的依赖非常大的概率会依赖 @babel/helpers,所以迁移之后这两个包大概率是会共存的。

基础项目(ice.js basic-mpa)体积对比结果:压缩后产物体积增加 27kb 左右,gizzped 之后体积增加 1kb 左右。

说到压缩,这里还有一个问题,上述实验项目的压缩均采用 esbuild 压缩,原因是:swc 目前的压缩功能相当鸡肋,只是移除换行和空格,当然,Terser Plugin 的作者已经开始参与到 swc 相关能力的建设中,近期就会支持混淆、短字符等更丰富的能力,详见 PR[5]

这块的问题不是很大,后面说到插件这块会再来讲下 swc 后续的规划。

转换后代码是否存在兼容性问题

答案:否。就如同 swc 介绍中所说的那样,swc 大量的配置都是参照 babel 设定的,其中包含了对兼容性产生作用的 preset-env,要说细致的话,swc 做的是非常细致了,大到 targets(设置兼容列表)小到 usebuiltIns(设置是否引入 polyfill)都和 babel 做的一毛一样。

同时,我还在比较排查体积问题的时候,非常无聊的对比了压缩前的产物结构 =。=。。。也几乎没啥区别,我甚至怀疑 swc 是照着 babel 的 test case 做的。

当然,这中间还是发现了些 bug,但都是小毛病,不伤大雅,例如这个[6]

插件存在的问题

放到前端研发框架这样的复杂业务场景,JS 插件的能力是至关重要的。架构团队另说,我们几乎不可能要求业务方去用 JS 以外的语言去写插件。可是在复杂的场景中,业务几乎离不开对 JS Compiler 有定制化的诉求,比如用的比较多的 babel-plugin-import,或者是 Rax App 场景下移除非投放端代码的能力等等。

当然,值得注意的是,在 swc 的场景下,并不是所有的插件都用 JS 写合适,前文有提到性能问题,swc 的作者也在尽力将 babel 社区主流的插件改用 Rust 来写。但是基于前面的原因,可扩展的 JS 插件是不可或缺的。

swc 的插件存在哪些问题呢?在看下面的总结之前,可以看下当初 swc 支持 JS Plugin 的 PR[7]

  1. 没有 presets 的概念
  2. plugin 自身能获取的信息过少:缺失 stat 信息,其中包含了当前正在处理的文件路径;无法给插件传递参数
  • 一般我们在 babel 里都是这么搞:plugins: ['babel-plugin-first', ['babel-plugin-second'], { id: 2 }] ,swc 的插件在 JS 这一层其实做的很简单,插件必须继承 Visitor 的基类,然后实现例如 visitProgram 这样的方法,众所周知 AST 本身是树状结构,@swc/core 的 Compiler 会主动调用插件提供的 visitProgram 然后一层层往里修改完 AST 节点,然后把最终的 JSObject 再丢给 Rust 处理
  • 插件的 visit 方法只能拿到 Node Object,无法拿到 Node Path,也就是说,这里没有 nodePath.replaceWith/nodePath.replaceMutipleWith 这样的方法来替换节点,你能做的只能是返回一个 New Node,这就非常受限了鸭。。如果你有一定使用 babel 的经验,你就会知道很多场景中 replace 的重要性
  • 由于它是由顶层的 visitProgram 往内递归执行节点访问操作,所以没有 enter/exit 这样的钩子,这对插件使用场景又是一个很大很大的限制
  • JS Plugin 无法很好的处理 TS Interface 声明,简单来说如果插件遍历到 TS Interface 声明就会报错(因为压根没有实现对应的处理方法)
  • 缺少 @babel/types 这样的生态工具,当我起手改这样一句话的时候,我顿时语塞了:t.isStringLiteral(node.source, { value: 'ice' }) swc 可没提供这样的能力鸭,简单猜想下这个库背后的实现就不难知道,如果树结构一致,@babel/types 也可以给 swc 用来做类型判断,我试了下,果然可以 =。=,可是后面又遇到需要创建节点的场景,这个时候就凉凉了,核心还是节点树的结构对不上,t.importSpecifier 所返回的结构 swc 压根就用不了,这个时候,就只能手拼 AST 节点了 =。=
  • 没有 AST playground,承接上面节点树结构不一样所带来的问题就是无法复用 babel 的 ast playground
  • 所以,我为啥要大家先看眼那个 PR 呢,简单来说就是作者当时快速做的一个功能,而且完全是按照 Rust 这一侧的 AST API 来实现的 JS Plugin API(这涉及到工作量的问题),没考虑太多真实迁移、使用场景。可这个能力呢,在复杂场景下又是相当重要的。

    当然,上述说到的一些问题,我自己在踩坑过程中,也实现了一些[8],比如 presets、plugin 获取当前处理的文件路径、plugin 传参等等,这里另外想要吐槽下官方的 swc-loader 做的东西太少了。

    当然,这里我和作者简单交流了一下[9],包括看了一些之前 issue 的讨论,甚至提到了有没有人愿意贡献个 adapter 来完成 swc-to-babel ,当下,作者决定做 swc2.0 版本,同时在 2.0 版本里好好设计插件系统,JS 侧这边能够做到完全复用 babel 插件的能力(这个还是相当让人兴奋的,因为当下太多和前端相关的新技术方案设计者太欠缺前端视角了)。

    结论和感受

    构建时间说到底了是一种体验优化,所以在对研发框架进行改造的过程中,一定是不能捡了桃子丢了西瓜的(为什么是桃子?因为开发体验可比芝麻要重要的多)。评价一个研发框架是否好用,除了构建时间,还有开发效率、可扩展能力、稳定性等等更多维度的东西。

    babel 确确实实已经经历过太多的考验和迭代,即使 swc 已经快三年了,并且一定程度上站在巨人的肩膀上,但依然有很多事情要做,尤其它的定位是取代 babel。

    目前的结论是,研发框架暂不适合将 swc 投入到生产环境使用,短期内可能会以实验性属性的形式透出,未来会看 swc 的发展来决定怎么处理。

    我自己也会尝试投入一部分时间到这个项目里去,希望可以用真实复杂的业务场景让它更快些变成我们想要的样子吧。

    目前我已经参与到 swc 的社区建设当中,未来会将 swc 的能力应用至 Rax 中,用于多端构建时的编译提速。

    点击阅读原文,为 Rax 点上一个小星星,跟踪更多社区热门技术。

    参考资料

    [1]

    neon: https://github.com/neon-bindings/neon

    [2]

    这篇文章: https://zhuanlan.zhihu.com/p/234914336

    [3]

    看下这里: https://github.com/swc-project/swc/blob/ff440d47a402bf5273217f6995269a918886d322/node/binding/src/lib.rs

    [4]

    这个 RFC: https://github.com/alibaba/ice/issues/4387

    [5]

    详见 PR: https://github.com/swc-project/swc/pull/1302

    [6]

    例如这个: https://github.com/swc-project/swc/issues/1843

    [7]

    JS Plugin 的 PR: https://github.com/swc-project/swc/issues/471

    [8]

    也实现了一些: https://github.com/swc-project/swc/issues/1863

    [9]

    简单交流了一下: https://github.com/swc-project/swc/issues/1863

    来源: Hello FE