用Rust锈化Vue Compiler

lxf2023-12-14 14:20:01

wasm的playground做好了!

Rusty Vue Playground herringtondarkholme.github.io/vue-compile…

一切都是从这条回复开始 github.com/vuejs/rfcs/…

用Rust锈化Vue Compiler

那么就来试着搞一下

github.com/HerringtonD…

正好练习一下高性能软件的编写技巧。毕竟前端社区里用原生语言写的项目也越来越多了,比如 Relay, SWC,和已经吹出牛的 Rome。

尝鲜指南

目前还没有时间做napi binding和wasm,需要从源码编译。 编译工具链没有额外依赖,直接使用rustup.rs/ 安装即可。


# uncomment the lilne below if rust is not installed 
# curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 
git clone git@github.com:HerringtonDarkholme/vue-compiler.git 
cd vue-compiler 
cargo run path-to-vue-file 

示例输出:


cargo run tests/fragment_key_flag.vue 
    Finished dev [unoptimized + debuginfo] target(s) in 0.05s 
     Running `target/debug/vue-compiler-cli tests/fragment_key_flag.vue` 
 
const _Vue = Vue 
 
return function render(_ctx, _cache) { 
  with (_ctx) { 
    const { 
      Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, createElementVNode: _createElementVNode, createCommentVNode: _createCommentVNode, createTextVNode: _createTextVNode, renderList: _renderList,  
    } = _Vue 
    return _createElementVNode(_Fragment, null, [ 
      (_openBlock(), _createElementBlock(_Fragment, null, _renderList(bb, (a) => { 
        return (_openBlock(), _createElementBlock(_Fragment, { 
          key: a, 
        }, "\n    text\n", 2112 /*STABLE_FRAGMENT | DEV_ROOT_FRAGMENT*/)) 
      }), 128 /*KEYED_FRAGMENT*/)), _createTextVNode("\n\n"), (test) 
        ? (_openBlock(), _createElementBlock(_Fragment, { 
          key: a, 
        }, "\n    text\n", 2112 /*STABLE_FRAGMENT | DEV_ROOT_FRAGMENT*/)) 
        : _createCommentVNode('v-if', true), (_openBlock(), _createElementBlock(_Fragment, null, _renderList(bb, (a) => { 
        return (_openBlock(), _createElementBlock(_Fragment, { 
          key: a, 
        }, [ 
          _createElementVNode("p"),  
        ], 2112 /*STABLE_FRAGMENT | DEV_ROOT_FRAGMENT*/)) 
      }), 128 /*KEYED_FRAGMENT*/)), _createTextVNode("\n\n"), (test) 
        ? (_openBlock(), _createElementBlock(_Fragment, { 
          key: a, 
        }, [ 
          _createElementVNode("p"),  
        ], 2112 /*STABLE_FRAGMENT | DEV_ROOT_FRAGMENT*/)) 
        : _createCommentVNode('v-if', true),  
    ]) 
  } 
} 

在这个输出里可以看到目前除了v-if/v-for/v-slot等基本功能外,也完成了Patch Flag/ Open Block/ Helper Import等Vue编译器的优化。 目前项目的完成了对标 @vue/compiler-core 中的功能。dom, ssr, sfc中的功能还未实现。

锈化目标

  • 广泛用: 提供一个可以广泛使用的Vue编译器库,可以作为Rust库使用,nodeJS使用(基于napi.rs),以及 wasm 库。
  • 高性能:快是肯定快的,就看比nodejs能快多少。事实上V8的性能相当可以,Vue本身的实现也没有大量deopt的地方,如果原生语言实现不当心,也是有可能快不了多少的。
  • 可扩展:除了能编译为SSR/DOM等编译成JS场景,希望还能提供足够的架构弹性,将Vue编译为其他平台的目标文件。如编译至Hybrid框架模板文件(可以是JS也可以是二进制的Bytecode),或者是编译成小程序的WXML文件。

目标受众

那么哪些读者可以看这篇文章呢?

  • 大厂架构组

如上所述,这次的设计能支持自定义的输出,比较适合大厂做原生开发的时候,生成自己平台的代码。比如在某有许多Vue Contributor写React的大厂中就可以用这套编译器直接生成二进制模板字节码。

  • Build工具开发者

在未来Rusty Vue Compiler成熟之后可以对接其他Rust写的前端工具链,比如 github.com/Brooooookly… 或者 github.com/web-infra-d…

学习Vue编译器到底干了什么,准备面试材料。比如背书Vue面经的时候,市面上的源码解析基本没几个有说Vue3的优化到底干了什么的。但是你可以在这个项目里找到整理好的优化Pass。github.com/HerringtonD…。可以吹Patch Flag的意义或者Hoist Static的优化条件。

当然这些东西在通常工作里根本不会用到,面试的时候问这些问题或者准备面试的时候背这些答案都是浪费生命。Anyway,培训机构的讲师依然可以拿这个项目的整理进行源码解析当内卷材料。(手动狗头

架构

vue-next 中的经典实现里,AST和代码生成的数据结构是混合在一起的。比如,AST中的template node有codegen 和ssrcodgen两个字段。大部分的转化逻辑和优化逻辑都是用transform pass完成,对每一个AST节点进行就地修改。

这样的架构一般没法达成“锈化目标”中可扩展的部分。因为Vue的AST是面向终端开发者的,它的改变只和Vue的模板语法挂钩。而编译产物则会因为不同目标平台的特色需要处理不同的信息,比如DOM和SSR需要的信息就有不同,而如果是小程序或者Hybrid App的Bytecode就更有可能会需要其他信息。在这次重写中多加了一个中间表达的转换,解除Vue框架的AST和编译产物直接的耦合。

目前Rusty Vue Compiler用的架构比较简单,分成五个部分

  • Scanner: 输出Token,Scanner是手工写死在Compiler里,贴合HTML规范的实现。
  • Parser: 输出AST,也是手写在Compiler中,不需要扩展。
  • Converter: 这一步输出中间表达IRNode(Itermediate Representation Node),可以由平台开发定义。
  • Transformer: 这一步处理IRNode,进行信息收集或者代码优化。(比如prefix identifier和patch flag的优化都可以做在这里)
  • Code Generator: 输入IR输出目标代码。Compiler内置了JS输出。也可以为不同平台定制其他输出比如有人希望把Vue编译到Flutter平台,那么就可以产出Dart(真的有人在想github.com/vuejs/rfcs/…,真的有人在做Flutter但是不是一回事儿www.bilibili.com/video/BV1L4…)。

实现细节

以下部分需要有一定Rust经验才能看明白,各位看官可以选择性阅读。

memchr

Scanner的实现中使用了Rust内置的memchr。是一个利用SIMD进行高速找字符串的routine。 github.com/BurntSushi/…

HTML Parser State

在HTML规范中定义了Tokenizer所在的Namespace是由Parser决定的。这个看似简单的需求在Rust里其实比较难做,简单的写法会导致Tokenizer里需要依赖Parser的引用,最后编译不过。

所以特意花了一点心思抽离了HTML规范中的这个诡异行为。这里用一个trait来表达规范中的随心所欲转换成Mercy。

String Allocation

编译阶段有许多字符串操作的行为,在Rust中操作字符串是长久以来的痛,比如见这个爆栈网的怒吼。简单的办法是直接就地转换成String。但既然要追求性能那自然是越少分配内存越好。观察到Vue中操作字符串的种类有限,于是可以把操作都记录在一个bitflag中等到代码生成的时候再写入。就能减少很多字符串的内存分配了。

Hash Function

  • 可以用rustc中的FxHash (Firefox Hash)来代替原声的Hash函数。虽然FxHash不能抵抗哈希冲突,但是在编译的场景下,并不会有恶意输入。所以可以用。

Rslint vs SWC

Vue模板中的表达式需要打上_ctx的前缀 ,这一步需要依赖JavaScript解析。目前社区里有两套JS Parser的实现: SWC和RSLint。但是实验下来SWC的开发体验实在比较糟糕。具体的糟糕点在这条Issue中有提 。简单来说

  1. SWC的依赖比较脏,加了一些没有必要的依赖或者依赖混乱。比如为了转化camelCase引入了inflector这个库,进而引入了一整套regexp的运行时。
  2. 文档缺乏,不知道怎么写SWC的插件。当然大多数开源项目都需要看源码这个问题也不大,但是:
  3. SWC源代码模块的分割比较迷,用parser得连带好多其他模块,common, ast, atom, visit 等等等等,不太好使。而且一时间看不太懂模块之间的依赖关系的设计(比如,swc_ecmascript_parser依赖了swc_visitor,有点因果倒置。另外swc_visitor和swc_ecmascript_visitor是两个crates)
  4. 核心访问AST的类型 Visitor 有几百个方法,而且都是macro生成的,根本看不了定义。
  5. 以及综合以上的问题,Macro和依赖把SWC的二进制大小提到了33MB,IDE直接卡住补全不出来了。

真正用swc作为依赖的库,emmm,也不喜欢swc。比如deno lint自己多写了个接口库 包装swc的AST。相对而言Rslint就好很多,甚至Rome都在考虑用。

Avoid dyn Trait

说了那么多SWC的坏话也说一下他的优点。dyn trait的避免是从swc中学习来的。

在Transform中需要串联许多不同的pass来实现不同的逻辑。注意到这些pass的类型不一致,在初期使用了dyn Pass这样的胖指针(fat pointer)来写。但是dyn的virtual dispatch有一定开销。当然更麻烦的是需要大量的类型声明来指挥编译器进行dyn的操作,导致函数签名非常可怕。并且lifetime的推断也有影响,使用起来也需要临场召唤所有要用的pass。

通过阅读SWC的代码发现可以用类似HList的写法进行抽象,使用一个叫AndThen<A, B>的结构体把不同pass给合并起来。Rust中的Future,Iterator也是用了类似方法。

这种写法可以绕过dyn trait的virtual dispatch,也不需要Box的运行时开销或是引用的编译期生命周期体操。最重要的是使用的时候可以得到Pass的ownership并来回传递。而且通过 inline可以把不少代码直接在编译时优化掉。

还没有做的性能细节

下面这些优化还来不及做

  • Arena Allocation
  • Alternative Allocator
  • Parallelization

Benchmark

没有Benchmark就没有性能优化。首先引入了Rust的benchmark criterion进行测试。并用了Github Action 进行自动化。 Benchmark的文件出自于VueJS官网的Example, v3.vuejs.org/examples/ma…。 比较@vue/compiler-core 和 Rusty vue core的性能,不包含dom的转化或者JS表达式的解析(因此没有比较babel和Rslint的性能差距)。初步看来性能提升可以在五倍左右,最高可达七倍。 Node版本的Benchmark (collected by Benchmark.js)

用Rust锈化Vue Compiler

Rust版本的Benchmark (collected by cargo-criterion)

用Rust锈化Vue Compiler

注意,JS和RS的benchmakr单位不同,前者是us,后者是ns。而且Rust版本还有改进的 Benchmark的结果可以参考在GitHub Action上集成的GH Pages查看。

总结

总体而言用Rust开发的效率在没有碰到生命周期的时候都是比较高的。写出的软件性能也相对较好。 但是也有在和编译器搏斗的时候一天什么事情都不干只在改对象归属权的事情发生,也并非银弹。 总体而言,未来会看到更多原生语言写的工具链。他们性能更好,速度更快,更加节能省电,是更环保的选项。

本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!