前端性能优化四:现代浏览器javascript性能优化-js引擎是如何对原型链进行性能优化

704 阅读4分钟

前端性能优化一:性能指标

前端性能优化二:现代浏览器javascript性能优化(1)

前端性能优化三:现代浏览器javascript性能优化-ICs

前言

前面一章我们已经介绍过js引擎是如何通过shape和ICs来优化js对象。如果没看过的同学可以先看前面一章。这一章介绍一下js引擎是如何对原型链进行优化的。

js引擎对原型链性能优化

我们都知道js是基于原型链的编程语言,所有的方法都是通过原型链进行共享的。

function Bar(x) {
	this.x = x;
}

Bar.prototype.getX = function getX() {
	return this.x;
};

const foo = new Bar(true);

在js中原型对象就是一个普通的js对象,因为原型对象就是一个普通的js对象,所以原型对象有一个属于自己的shape对象,这个shape比较特殊,原型对象的shape对象无法共享.

原型链存取

当我们创建一个对象的实例的时候在内存中会发生什么我们已经知道了,那当我们需要使用getX方法的时候会发生什么呢

  1. 首先检查foo对象的shape看是否有getX方法,没有包含getX方法。
  2. 找到Bar.prototype,检查他的的shape,有getX方法。 也就是一个简单的读取需要进行1+2N(N表示找到方法需要的原型链的个数)次查找,1次自己本身的shape,不存在找到原型,然后在检查原型的shape。
    3次检查在这里看起来似乎可以接受,但是当我们操作DOM元素的时候const anchor = document.createElement('a'); DOM有6层的原型链。

一个最简单的getAttribute方法就需要通过3层原型链在Element.prototype上才能找到,也就是说根据前面的1+2N公式,需要7次的查找。因为DOM操作在web上是非常常见的操作,那么这样的查找是非常的不高效的。

回到前面的例子foo.getX,如果我们能把原型的查找的和方法的查找包裹在一起做就好了。js引擎通过把原型链的指针从实例本身转移到他的shape上实现了这一操作

将prototype的指针移到shape上意味着原来的查找操作从1+2N变成了1+N,但是只要你换了foo的原型,shape就要对应的进行shape变迁。 即使是减少了开销,但是这个仍然是一个线性的增加,如果查找的原型链太深还是会有不小的性能开销。

ValidityCell

之前提到,prototype的shape无法和别的对象共享,就是因为每个prototype的shape有一个特殊的ValidityCell字段。这个ValidityCell字段标识了是否有人修改了原型对象。同时为了让查找更快js引擎也加入了inline cache。

当第一次运行代码的时候,inline cache会分别给这几个字段缓存上数据

  1. prototype:会记录在哪个原型对象中找到了需要的字段,这里是getX方法在Bar.prototype
  2. offset:会记录找到这个字段的原型中字段的索引
  3. shape:会用来记录对象实例的shape
  4. ValidityCell:用来记录当前实例shape指向的原型对象,这里也是Bar.prototype 在下一次运行这个方法的时候,js引擎会先对比shape是不是同一个和检查ValidityCell是否为true,也就是原型对象有没有更修改过,如果验证通过,js引擎就可以直接使用offset,省去很多查找的性能消耗。

但是如果你修改了原型对象,比如delete Bar.prototype.getX在原型对象上删掉了getX方法。这个时候原型的shape对象会被重新分配,ValidityCell也会变成false,当下一次使用inline cache的时候也通不过检查从而得不到offset的缓存了,这会影响性能。

回到之前那个DOM元素的例子,任何对原型链的修改都会将ValidityCell设置为false,如果你修改了Object.prototype,不只有Object.prototype的shape的ValidityCell变成了false,所有“继承”他的原型对象的ValidityCell全部都变成了false。也就是说修改Object.prototype了以后会对程序的性能造成很大的影响,因为所有的对象最终都"继承"Object

总结

之前一章我们介绍了shape和ICs,这一张我们又介绍了prototype的shape对象,基于这些知识我们知道了一个技巧可以提高我们的性能:不要修改原型对象,如果你真的需要修改原型对象,请在其他代码执行之前就将原型对象修改好。