Javascript是一种解释器逐行翻译执行的语言,遵循JIT范式
- 源代码通过词法分析和语法分析解析为抽象语法树(AST)
- 抽象语法树通过解释器生成字节码
- 字节码通过编译器将部分常用的字节码替换为高度优化的机器码
解释器与编译器
解释器
简单,启动快,执行慢,原因在于解释器不能识别重复的工作,同一行代码,会重复的翻译执行
编译器
复杂,启动慢,执行快,他会在执行前翻译所有代码,并做优化,比如找出重复的代码,进行共享
即时编译 JIT
他结合了解释器与编译器的优点,使得代码的转换和执行都变得更快
在执行代码期间,监视器(探查器)会标记热代码片段和运行很多次的热代码片段,随着次数增加,会标记代码块的热度,并根据等级分别发送给基线编译器或优化编译器
- Warm 将解释器执行代码发送给jit引擎,将其编译为机器码,但此处不会
- hot 解释执行代码被替换为warm编译出的机器码
- Very hot 将解释器执行的代码发送给优化编译器,创建和编译出更高效的机器码执行代码并进行替换
基线编译器
warm部分的代码将被编译成字节码,并尝试通过创建存根来优化代码,以下面代码为例子(真实环境下,这段代码过于简单会被直接编译成机器码)
javascript
function concat(arr) {
let res = '';
for(let i = 0; i < arr.length; i++) {
res+=arr[i]
}
return res
}
console.log(concat(['a', 1,'b', true, 'c']))
基线编译器会将res += arr[i]
转换为存根
,但由于此指令是多态的(没有什么能保证i每次都是一个整数或arr[i]每次都为一个字符串),它将为每个可能的组合创建存根(可以理解为)。
- i是一个整数?
- res是一个数组?
- arr是一个数组?
- arr[i]是一个字符串?
优化编译器
优化编译器会将基线编译器创建的存根变成一个组(可以简单的理解为针对数组内不同的值创建了对应的机器码方法称之为存根组,而存根则是每一步代码的匹配对应的机器码),并将上面的一些例子固化,匹配到了就直接使用优化后的高度优化的机器码,跳过判断的流程。
arr
是一个数组?是i
是一个整数?是res
是一个字符串?是
编译器去优化
以上面代码为例子如果这里的数组中出现了一个object类型,会导致优化编译器的存根组失效,执行去优化操作,优化和去优化的过程是昂贵的。由此产生了一类JavaScript的优化方法,下面将详细描述。
如何优化Js代码
在构造函数中声明对象属性
更改对象属性会产生新的隐藏类
ini
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
var p1 = new Point(11, 22); // hidden class Point created
var p2 = new Point(33, 44);
p1.z = 55; // another hidden class Point created
当p1中添加了z属性时,v8会单独为p1创建一个新的隐藏类,此时,任何接受Point对象的方法都将发生去优化
保持对象属性不变
更改对象属性的顺序会导致新的隐藏类,因为对象形状中是包含顺序的。
ini
const a1 = { a: 1 }; # hidden class a1 created
a1.b = 3;
const a2 = { b: 3 }; # different hidden class a2 created
a2.a = 1;
上面的代码中,a1和a2有不同的隐藏类。修复顺序允许编译器重用同一个隐藏类。因为添加的字段(包括顺序)用于生成隐藏类的id
修复函数参数类型
函数根据特定参数位置的值类型更改对象形状。如果此类型发生更改,则函数将去优化并重新优化。
scss
function add(x, y) {
return x + y
}
add(1, 2);
add("a", "b");
add(true, false);
add([], []);
add({}, {});
第9行过后,V8将不会再优化add这个函数。
在脚本 作用域 中声明类
不要在函数作用域中声明类。以下面这个例子为例:
javascript
function createPoint(x, y) {
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
return new Point(x, y);
}
function length(point) {
...
}
每一次createPoint这个函数被调用的时候,一个新的Point原型会被创建。
每一个新的原型都对应着一个新的对象形状,所以每一次length函数都会看到一个新的point的对象形状。
跟之前一样,当看到4个不同的对象形状的时候,函数会变得megamorphic,TurboFan将不会再尝试优化length函数。
在脚本作用域中声明class Point,我们可以避免每一次调用createPoint的时候,生成不同的对象形状。
使用for in
for...in
循环比函数迭代、带箭头函数的函数迭代和for循环中的object.keys快4-6倍。
无关字符不影响性能
无关的字符,比如空白,注释,变量名长度,函数签名等,不会影响函数的性能
Try/catch/finally 不是毁灭性的
Try代码块以前容易出现高昂的优化-去优化的周期
总结
优化的方法在于减少对已有对象的修改,以及函数参数的改变
参考
- How JavaScript works: Optimizing the V8 compiler for efficiency: blog.logrocket.com/how-javascr...