摘要
文章从 TypeScript 源码中的 checker.ts 文件入手,阐述了该文件因将 5.2 万行代码写在一个文件里达 2.92MB 的情况,接着分析诸多如低配版 named parameters、ESM/CJS 性能问题等方面的原因,最后指出由于 JavaScript 特性的限制,TypeScript 源码实现拧巴但整体架构设计不错。
一、checker.ts 文件概况
checker.ts 文件很特别,它将 TS 完整类型系统 5.2 万行逻辑都写在一个 ts 文件里,文件大小达到 2.92MB。这让人疑惑,难道是 ts 源码维护者不会写代码吗?其实不是。
二、具体的性能相关做法及原因
(一)低配版 named parameters
在 js 里通常用对象解构传参,但在 ts compiler 里这样会有大量内存开销。在高频调用场景下,这种方式会导致 type checking 时内存峰值,频繁 gc 和 mem_copy。而且字面量 key 顺序还影响 v8 的 inline caches 优化,可能影响 TurboFan 优化,造成性能损失。所以 ts 用了低配版用注释表示 named parameters 的方式,像其他语言(如 moonbit 或 swift)中的标签函数一样。
// 示例的标签函数
fn add(~ left: Int, ~ right: Int) -> Int {
return left + right;
}
add(left: 1, right: 44);
add(right: 44, left: 1);
add(1, 2);
(二)能用 number 尽量 number
像 switch、const enum 等设计,尽量使用 number 的原因是 object 和 string 开销大,而小整数在 v8 里甚至无开销(如果不算 SMI tagged pointer 指针自身数值的话)。
(三)无限制使用 const enum
const enum 可将枚举值 inline 到函数里变成立即数,能享受极致优化。虽然社区不推荐使用,ts 的部分维护者也认为这是个错误,但 ts 源码里有 800 +个 const enum,没这个特性 tsc 可能会慢不少。
(四)ESM/CJS 的性能问题
当 export 导出太多成员时,V8 处理这类对象会变成 Slow Properties 字典模式。在高频模块内常量被引用大几百万次时,像const constant = require('./constant'); module.export = function getXXConfig() { return constant.xxx + constant.bbb; }
这种点读查询开销不能忽视。而 checker.ts 将所有东西放在一起,在函数作用域内,查询时间是 O(1)。
(五)ESM 没有 private 导出
esm 没有 private export 特性,但 ts 需要。ts 通过/** @internal */
注解实现,标记为@internal
的在生成 d.ts 时会被抹去,实现内部可 import 外部不可 import。
(六)ts 大量使用 var
部分函数为了性能全用 var,不用 let 和 const。在 ts 场景下,v8 这类 js runtime 的 TDZ 检查会影响运行性能,production build 比 dev build 快的原因之一就在于此。
(七)往 String.prototype.xxx 上注入东西
在普通 js/ts 项目里这种操作不被认可,但 ts 作为静态类型语言,为了拓展基础类型来使用会这么做,这在其他语言(如 swift/go)里是常见操作。
(八)无类编程,推崇组合编程
checker.ts 几万行核心逻辑几乎没有 class 和继承,用函数组合架构代码。这样做可能是考虑到 class 继承存在潜在性能问题,如在 V8 引擎下 A extends B 场景中,可能会影响引擎 ICs 的优化效果,导致性能下降。
(九)没有使用「表驱动」模式
源码里根据 ast node kind 走不同逻辑时,没有用 Record<Kind, Fn>的表驱动方式,是因为表驱动无法被 v8 这类 runtime 静态分析优化,而且会慢几十倍,对于基础设施不可接受。
(十)基本没有 try - catch
checker.ts 里通过返回值和往 context.xxx 上写东西指示异常,一方面是为了性能,另一方面可能是没有 checked exception 导致只能这样 type checked。
三、文件数量与模块系统相关
(一)文件多的问题
在大型 js/ts 项目里文件多会导致找东西困难,import 很麻烦。ts 曾经尝试做满血版 namespace 特性,但由于确定不做运行时特性而放弃,现在 ts 源码里还大量使用 namespace 或者用 ESM 模拟 namespace 特性。
(二)js 对 ts 演进的影响
由于 js 自身语言特性少,已经严重限制了 ts 自身实现。虽然 ts 类型系统已经很完善,但因为承诺不再做新的 runtime 特性,只做类型系统,导致源码实现很拧巴。不过 ts 的整体 compiler pipeline 架构设计相当漂亮和简洁。