柚木

精读《Compilers are the New Frameworks》

本期精读文章 《Compilers are the New Frameworks》

1 引言

本期文章篇幅短小却言简意骇,文中开头作者就抛出自己的观点 Web 框架正在从运行库转变为优化编译器

作者主要从编译性能方面入手,也提到 WebAssembly 可能将会是下一代 Web 应用的落脚点,因此他也建议 Web 开发者们深入了解学习编译器的工作原理。

2 概述

目前业界流行使用一整套工具来搭建前端项目,如 webpack、webpack-dev-server、babel、scss、react、redux、react-router ...,在项目开发期间需要花费大量时间去进行工程性能优化、编写大量的构建配置项等,从现在前端工程的复杂度以及前端开发的工作量来看,前端框架已经不能再仅仅只是一个单独的视图层或数据处理层,而应该是一套相对完整的框架,它不仅提供如何编写前端页面的方法,同时也应该考虑代码构建编译的性能、页面间路由的跳转、新语法的兼容等一系列问题。

这也正是本期精读文章抛出的观点,Web 框架正在从运行库转变为优化编译器,或者说 Web 框架需要将优化编译性能考虑进去。

PriJs & UmiJs

PriJs & UmiJs 二者正是以上述观点为基础的,基于 react 并包含了工具 & 路由 & 性能优化 & 数据流等强约定弱配置的前端一站式框架,通过约定、自动生成和解析代码等方式来辅助开发,减少开发者在性能&配置&路由&构建上耗费的时间,可以更专注于业务逻辑。

构建工具

webpack 是目前主流的前端代码构建工具,但其复杂的配置一直是前端开发者头疼之处,PriJs & UmiJs 框架内部解决了这一难题,它们将 webpack 复杂的性能优化配置全部内置化,使项目在 0 配置的基础上直接支持 PWA、Automatic code splitting、Tree Shaking、Auto dll、Import on demand、Auto pick shared modules、Scope Hoist、Dynamic import、Service Worker、Sass Loader 等。

页面&路由

PriJs & UmiJs 提供页面生成模版,并自动根据项目页面生成路由,通过单页面或多页面特性决定路由跳转的类型,默认提供 404 页面。

数据流

PriJs & UmiJs 虽然是基于 react 的前端一站式框架,暂不支持 vue、angular 等,但并不局限数据流的使用的方式,可以根据项目需求使用任意数据流方式,如 redux、mobx 等。

插件机制

PriJs & UmiJs 提供了灵活的插件机制,使项目能够拥有强大的定制能力,通过插件机制可以变更 webpack 配置、修改路由规则、修改页面模版、新增命令、使用任意数据流、定制项目规范和约定等。

其它

此外,PriJs 还支持 markdown 格式、支持 Deploy to github pages、支持 Typescript 等。

PriJs & UmiJs 前端一站式框架实际上是提供了一整套的前端开发解决方案,它不仅仅只是单纯的一个运行库,而是将构建性能&工具&路由等一系列问题全部解决,这种做法在一定程度上不正是在说明 Compilers are the New Frameworks。

读者们对此肯定有很多不同的观点和看法,不妨各抒己见。

3 精读

精读文章作者建议 Web 开发者学习编译器工作原理,对于前端开发者来说可以从与前端现在和未来息息相关的 JIT 和 WebAssembly 入手学习编译器相关原理。

JIT

JIT(Just-in-Time)主要是针对 javascript 这一解释型语言所做的性能优化,即浏览器引入编译器来解决解释器性能低效的问题,形成混合的模式。

监视器

浏览器在 js 引擎中增加一个监视器,用于监控通过解释器的代码的运行情况,并将同一行代码运行若干次标记为 warm,将同一行代码运行很多次标记为 hot

基线器

JIT 会将 warm 代码段放到基线编译器中,并将编译结果存储起来。该代码段的每一行都会被编译成一个 stub,并以 行号 + 变量类型 为索引。如果监视器监视到了执行同样的代码和变量类型,就直接将对应的已编译版本提交给浏览器执行,而不用重新通过解释器来翻译,通过这样的做法可以加快执行速度。

优化器

JIT 会将 hot 代码段放到优化编译器中进行代码优化,不过需要遵循优化规则:即如果代码循环中每次迭代的对象都有相同的形状,那么就认为它以后迭代的对象的形状也是相同的。但 javascript 是没有类型定义的,就无法确保每次代码迭代的对象都会具有相同类型,因此在代码运行前会检查其规则是否合理,如果合理则执行优化代码,如果不合理则丢弃优化代码,重新回到解释器或基线器。大多数浏览器为了防止引起 优化 - 丢弃优化 的无限循环,一般会对优化次数做限制,比如 JIT 做了超过 10 次 优化 - 丢弃优化 的操作,那么就不再执行优化编译。

JIT 在优化提升 javascript 性能的同时也会增加多余的其它开销,主要是对代码的监视和编译时间的开销,具体包括:

  • 优化和丢弃优化的开销
  • 监视器存储的内存开销
  • 丢弃优化时恢复存储的内存开销
  • 基线版本和优化后版本的内存开销

而 WebAssembly 从更底层去解决这部分多余开销,进一步提升 Web 应用的性能。

WebAssembly

为什么说 WebAssembly 更为高效,性能更好?

在 JS 引擎中性能消耗的分布大致为:将源码转为解释器可运行代码 -> 基线&优化编译器的运行 -> 优化-丢弃优化的过程 -> 执行代码 -> 垃圾回收&内存清理,这个过程是交叉进行的。

而 WebAssembly 却只要简单的三个步骤即可完成 JS 引擎的整个交叉执行过程。

Parse

当到达浏览器时,JS 源码需要被解析成 AST(抽象语法树)变成字节码提供给引擎编译,而 WebAssembly 却不需要这种转换,因为其本身就是字节码,因此它只需对代码进行 decode 并检查其正确性即可。

Compile + Optimize

这是执行代码编译和优化的阶段,在这个阶段 WebAssembly 的性能优于 JS 的主要原因为:

  • WebAssembly 是有类型定义的代码,不需要在编译前运行代码来获取变量类型
  • WebAssembly 不需要像 JS 那样当变量类型改变时需要将代码编译成不同版本
  • WebAssembly 不需要在编译阶段做太多的优化工作

Re-optimize

当 JIT 在执行 JS 阶段发现变量类型不合理,就会丢弃优化代码重新进行 优化 - 丢弃优化 的循环,而 WebAssembly 中的变量类型都是确定的,JIT 不需要检查变量类型的合理性,因此并没有重优化阶段。

Execute

如果开发者了解 JIT 的内部实现机制,当然是可以针对性的写出符合 JIT 标准的代码,使之具有更高的执行效率,但通常开发者为了代码可读性更好而使用的编码模式往往却不适合编译器对代码的优化,而且不同浏览器的优化规则也不尽相同,导致 JS 的执行效率并不高。

WebAssembly 正是为了编译器而设计的,很多 JIT 为 JS 所做的优化 WebAssembly 并不需要,使得 WebAssembly 专注于提供执行效率更高的指令。

Garbage collection

JS 不支持开发者手动清理内存,而是由 JS 引擎自动做垃圾回收,因此垃圾回收的时机并不可控,有可能会在一个不合适的时机执行,而且也会增加代码执行的开销。而对于 WebAssembly 而言,其内存操作是由开发者手动控制的,虽然会增加一些开发成本,不过这也使的代码执行效率更高。

4 总结

本文从 Web 框架正在从运行库转变为优化编译器 这一观点切入,讨论了 PriJs & UmiJs 前端框架的思路转变,简洁的描述了 JIT 的工作原理以及 WebAssembly 相比于 JS 的性能优势。

本文主要希望读者可以积极参与讨论此观点,因此并没有长篇剖析 JIT & WebAssembly 的深刻原理,但我相信深入学习编译器的工作原理对 Web 开发者来说绝对是受益匪浅的事情,后续文章将对 WebAssembly 进行深入探讨和剖析。

5 更多讨论

讨论地址是:精读《Compilers are the New Frameworks》 · Issue #69 · dt-fe/weekly

如果你想参与讨论,请点击这里,每周都有新的主题,每周末发布。