有经验的专家写的代码,和无经验的新手写的代码,在运行时性能上大概会有多少差异?
个人感觉,常规业务逻辑代码通常可以差 1 倍;如果算上框架的影响,可以差 2~4 倍。
仅考虑业务代码的话,新手容易搞不清楚 Control Flow 进而绕了一大圈去完成原本可以直接完成的事情。我在优化或者重构过程中需要花费很大的精力去梳理原先的 Control Flow,这部分优化已经可以带来将近成倍的提升,例如:
- 将串行网络请求修改为并行
- 添加一些缓存(如
React.memo
) - 把会导致 UI 重渲染的代码整合在一起,做批处理
细化到核心算法层面,我的思路是:算法剪枝 + 围绕引擎的优化(主要是 V8)。剪枝就得根据具体算法来看了,剪得好也能得到将近成倍的提升,也不属于本问讨论范围。
以各位专家(我不是)的经验,这种编译器最多可以提升多少% 运行时性能?可以推荐一下该款编译器的主要优化方向吗?
围绕引擎的优化我认为是有可以做的空间的。例如:
Map<string, T>
重构为Record<string, T | undefined>
,读取速度可以快 30% 左右,修改删除速度不清楚forEach(fn)
重构为for (let i = 0; i < arr.length; i++)
循环自身开销可以减少 20% 左右function foo(...args: T[])
重构为function foo(args: T[])
,可变参数改固定参数,会有一定的提升- class 属性
[[Define]]
语义改为[[Assignment]]
语义,现有情况下可以提升 50% 左右的实例化性能 - class 中
foo?: T
重构为foo: T | undefined
,提前初始化所有属性(即使是没用到的属性,也初始化为 undefined)会有一些函数字节码缓存相关的调用性能提升 [1] - 为了传参创建一次性对象,改为固定使用一个对象不断改变其值,可以减少 GC 稳帧率
- 强制整型运算,例如
1 + 1
重构为(1 | 0) + (1 | 0)
- ……肯定还有很多我不知道的
注:以上数字偷个懒没考证,可以再用 https://jsbench.me/ 跑一下
值得一提的是,若干年前,emscripten 和 asm.js 应该使用了非常多的奇技淫巧,来把 C++ 代码变成高性能的 JS 代码,到现在应该都还有参考价值。
但是按我的经验来看,业务代码可能绝大多数都是 IO 密集型的,引擎级别的优化能让业务代码提升多少,真的不好回答。如果是计算密集型的库代码,综合提升 10%~20% 应该是有希望的。
说完了可能的方向,我来说一个我认为通过编译器优化 TS 代码的一些潜在困难
- 大部分 JS 开发者没有类型意识,甚至有一小部分开发者认为类型是写代码过程中的累赘。由此产生的问题有:代码中经常出现 any 或者跟 any 极为相似的类型,相当于是完全关闭了类型系统,难以优化;空值问题显著,例如很多开发者并不知道
foo?: T
、foo: T | undefined
、foo: T | null
、foo: T | null | undefined
到底有什么区别。 - TS 无任何运行时束缚,而且 TS 就是这么设计的。因此,万一编译器把类型搞错了,不会有任何提示,只会有线上 Bug。
- TS 在一些情况下 unsound,这是为了妥协 JS 社区中一些常见用法而设计的,降低了类型系统的门槛,但可能对编译器不友好。
当然相信在巨佬的努力下是可以解决这些难题的~
最后夹带一个性能优化万金油,没试过的甚至可能直接完成你下半年的性能优化 OKR
在项目中用了各种转码器(babel、esbuild、swc)的同学,转码器虽好,但他们都是有性能代价的。把向下兼容的版本调的越低,兼容代码给业务代码带来的性能负担就会越重,包括运行效率和包体积。
用 babel-preset-env 的,开启 loose 选项,整个项目可以加速 10%~30%。如果写的是库,可以配合 esbuild/swc + buble(不是 typo,真的叫 buble,但已经不维护了,慎用),几乎可以做到 0 编译开销。
Reference
[1] https://stackoverflow.com/questions/44466931/is-it-an-optimization-to-explicitly-initialize-undefined-object-members-in-javas