Prepack详细介绍及微信小程序优化的新思路

1,689 阅读10分钟
作者介绍:雪婧,美团点评点餐团队成员。


前言

Prepack前几个月刚出来的时候已经得到了前端界的大范围关注,而在不久之后又逐渐退出了人们的视线。此时这篇文章出来可能显得有些滞后,个人还是比较看好它未来对于前端代码预编译优化所带来的收益。所以再详细地介绍一下Prepack和它给我带来的思考。
在前端技术迭代更新速度较快、前端人力宝贵的情况下,面对新技术的不断涌现我们需要保持冷静和严谨的态度去接受这些新技术,所以一般在一个新技术涌现时,我都会先弄清楚这几个问题再考虑是否要推动和更迭现有的技术栈:
  • 是什么?
  • 解决了什么问题?
  • 带来了什么新的问题?
  • 新的问题和解决的问题在目前场景下权重是怎么样的?
  • 投入产出比如何?
带着这几个问题进入正题。

一、什么是Prepack

官网的第一句是:A tool for making JavaScript code run faster. —— 一个让JavaScript代码运行更快的工具。
实际上Prepack 就是一个部分求值器(Partial Evaluator),代码打包编译时提前将计算结果放到编译后的代码中,从而去除冗余的代码(相对运行时来说,对应的代码逻辑本身并不是冗余的),而不是在代码真正运行时才去求值。消除那些本可以在编译阶段完成的运行时计算,一定程度上减少了javascript资源包大小以及浏览器运行js所耗费的时间。详情移步prepack.io/
另外要说明的是,Prepack虽然能一定程度上减少代码量的大小,但是Prepack开发团队开发它的初衷并不是为了减少代码包大小,主要还是减少运行时的时间消耗,代码包的大小减少算是一个附加收益。

二、工作原理

根据官网列的几个点依次进行详细解释。
  • Abstract Syntax Tree (AST) 抽象语法树 (粒度) Prepack在抽象语法树的级别运行,使用Babel解析并生成JavaScript源代码。 那什么是“在抽象语法树的级别运行”? 将一种结构化语言编译成另一个结构化语言的代码过程如下:

1、Parse,将代码解析成抽象语法树(AST);

2、对抽象语法树(AST)进行遍历(traverse)和替换(replace) 生成新的 抽象语法树(AST);

3、Generator—— 根据新的 AST 生成编译后的代码; “在抽象语法树的级别运行” 说明了Prepack的编译流程,也说明了编译粒度。 比抽象语法树粒度更细的还有一个分析树。 例如以下一个3+4 例子的对比: 分析树:

抽象语法树:


概括来说,分析树包含了记号序列和推导的各个步骤,拆分粒度更细;抽象语法树则只包含了关键的信息,不知道具体的文法和运算符细节。 在抽象语法树的级别编译,也就是说不用依赖于具体的文法,不依赖于语言的细节。也进一步说明,原code经过语法分析后总是构造出相同的抽象语法树,再通过Prepack进行转换处理成新的抽象语法树,Prepack不会对原有的代码文法和处理逻辑带来影响,对编译器的接口的统一性也不会造成影响。 对编译更多细节感兴趣的可读 两周自制脚本语言 (图灵程序设计丛书)
  • Concrete Execution 具体执行 Prepack的核心是一个JavaScript解释器,它与ECMAScript 5几乎完全兼容,而且紧密地保持与ECMAScript 2016语言规范的一致性,你可以将Prepack中的解释器视为完全参照JavaScript实现的。
解释器能够跟踪并撤销包括所有对象Mutation在内的结果,从而能够进行推测优化(Speculative Optimization)。

  • Symbolic Execution 符号执行 除了对具体值进行计算外,Prepack的解释器还可以操作受环境相互作用影响的抽象值。例如Date.now可以返回一个抽象值,你可以通过helper辅助函数(如__abstract())手动注入抽象值。Prepack会跟踪所有在抽象值上执行的操作,在遇到分支时,Prepack会执行并探索所有可能性。所以,Prepack实现了一套JavaScript的符号执行引擎。 以上是官网的翻译,就是符号可以代表一个根据环境不同而可变的一个方法执行结果,例如以下例子中的_0就是这样的一个抽象值,它无法提前计算,因为在不同的浏览器或运行时机下结果是不同的。左边编译前代码,x 存在if 判断的分支情况,Prepack会汇总所有的分支情况进行优化编译,如例子中将所有分支汇总(2个分支)替换成三目运算符表达。


  • Abstract Interpretation 抽象释义 符号执行在遇到抽象值的分支时会分叉(fork),Prepack会在控制流合并点加入分歧执行(Diverged Execution)来实现抽象释义的形式。连接变量和堆属性可能会得到条件抽象值,Prepack会跟踪有关抽象值和型域(Type Domain)的信息。 还是之前的例子,就是抽象值“_0 ” 存在2个分支,Prepack底层会汇总分支情况,生成新的抽象语法树,在优化解析时(Generator)通过连接变量和堆属性,得到各分支下的抽象值“_0 ”可能的对应情况,并跟踪相关情况的信息计算出结果形成新的代码。


  • Heap Serialization堆序列化
这里的堆序列化其实指的是当全局代码返回,初始化阶段结束时,也就是在generator 过程中Prepack 会捕获最终的堆,按顺序遍历堆创建、链接堆中的可及对象。堆中的一些值可能是对抽象值进行计算的结果,Prepack 将会根据这些值生成执行计算的代码,最终组成新的代码。


总结一下Prepack的工作过程:


  1. 开发的代码经过解析(Parse)形成抽象语法树
  2. Transform时期预先计算出的可求的值,对存在受环境影响的抽象值进行抽象释义,转换成新的抽象树
  3. Generate时期按顺序遍历堆进行符号执行和跟踪有关抽象值和型域信息处理抽象值等,最终组成新的代码。

三、示例

官网(prepack.io/)有举代表性的例子,这里不再详述。还可以通过线上的Prepackprepack.io/repl.html将自己的代码输入动态查看解析结果。

四、Prepack 引入会带来的问题

Prepack 带来的优势是把 js 的 AST 编译到更加低级的语义,提前计算出值, 减少 js 代码在浏览器端初始化运行消耗的时间,附带收益是减少了代码包的大小,还可以与Closure Compiler 配合进一步缩小代码包.,但同时也带来了新的问题:
  • 没有官方统一、成熟的方案支持打包集成到开发环境——需要开发者另辟蹊径完成集成
  • 不能识别 document 和 window,会识别为 undefined——浏览器环境代码利用Prepack存在较大局限性
  • 没有完全支持浏览器环境和node.js环境——node代码不能全面接入Prepack
  • 生成的代码没有针对引擎做好优化——运行效率会存在变低的情况
  • 存在一些尚未解决的issues——会有更多的“坑”需要踩

五、Prepack 未来规划

目前Prepack仍处于早期开发阶段,尚未准备好在生产环境中使用,官方建议仅尝试使用,并且欢迎提供反馈以帮助修复错误。
Prepack团队对未来的规划如下:
短期
  • 稳定现有功能集,用于预优化(Prepack)React Native代码包
  • 集成React Native工具链
  • 根据React Native所用模块系统的假设来构建优化
中期
  • 进一步优化序列化(Serialization),包括:消除不暴露特性(identity)的对象;消除未使用的导出属性,等等
  • 预优化每个函数、基本代码块、语句、表达式
  • 与ES6保持完全一致
  • 支持广泛的模块系统
  • 假设ES6支持某些功能,延迟完成或直接忽略Polyfill应用
  • 进一步实现Web和Node.js环境中的兼容性目标
  • 深入集成JavaScript虚拟机,改进堆反序列化过程,包括 :暴露“对象懒初始化”的概念 - 以一种JavaScript无感知的方式,在首次使用对象时对其进行初始化;通过专门的字节码提高普通对象创建的编码效率;将代码分为两个阶段:1) 非环境依赖阶段,虚拟机可以安全地捕获并恢复生成的堆;2)环境依赖阶段,通过从环境中获得的值执行所有剩余的计算过程来拼凑具体的堆,等等
  • 总结循环和递归
长期 - 利用Prepack作为一个平台
  • JavaScript Playground - 通过调整JavaScript引擎体验JavaScript特性,这些引擎由JavaScript所编写,托管在浏览器中;你可以把它想象成一个“Babel虚拟机”,实现了不能被编译的JavaScript新特性
  • 捉Bug - 发现异常崩溃、执行问题……
  • 效果分析,例如检测模块工厂函数可能的副作用或强制纯净注释
  • 类型分析
  • 信息流分析
  • 调用图推理,允许内联和代码索引
  • 自动测试生成,利用符号执行的特性与约束求解器(Constraint Solver)结合来计算执行不同执行路径的输入
  • 智能模糊(Smart Fuzzing)
  • JavaScript沙盒 - 以不可观察的方式有效地测试JavaScript代码

六、Prepack给微信小程序优化带来的新思路

从以上Prepack带来的优势和问题来看,在普遍的前端项目中使用的话投入较大于产出,目前必然是不适宜投入到项目中使用。
虽然目前Prepack尚未完善,但是开拓了一个新的js代码优化方向。减少 js 代码在浏览器端初始化运行消耗的时间,这对于提升用户体验来说也是很关键的点。例如 app本身的启动一般也比较占用时间,所以一般app 都会有一个启动时的广告页或静态介绍页,借此消除启动时间较长给用户带来的不良体验。 App可以用广告页和静态介绍页吸引用户的注意力。
最近在做小程序相关的业务,微信小程序本质是app,在业务逻辑较重的小程序也同样存在启动时间消耗带来不良用户体验的问题,但是却不能以普通app的解决方案来解决。因为微信本身比较注重用户体验,是一个擅长做减法且克制的产品,加入广告和启动页不仅会影响用户体验还破坏产品的“克制”性。 所以小程序的启动优化就需要新的解决思路,而Prepack就刚好带来了曙光。
微信小程序不存在 document 和 window 对象,也非在node.js环境,不受它带来的问题所局限。可以通过脚本或其他集成方式将Prepack加入到打包流程中完成优化,也可降级处理部分主要文件发挥他的作用。总之和微信小程序之间存在着可能性和可行性,后续会再出针对小程序和Prepack的实践文章,感兴趣的同学可待续。

以上纯属个人观点,可能存在不足或错误之处,欢迎指正。