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

581 阅读6分钟

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

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

前面一章已经介绍了一些javascript一些开发时候可以使用的性能优化技巧,这一章介绍一些js编译器内部机制,帮助你能写出更高效的js代码。

Shapes(hidden class)

在js中对象被定义为一个字典的数据结构,string类型的key作为属性key,key对应的值类似这个样子

除了value以外,还定义了另外一些attributes

  • [[Writable]]:属性是否可以被赋值
  • [[Enumerable]]:属性是否可以在for in 循环中出现
  • [[Configurable]]:属性是否可以被删除

你也通过Object.getOwnPropertyDescriptor(object, 'foo');获得这些属性.

array在js中是一个特殊的对象,index是作为一个特殊的属性维护,同时array还维护一个length的属性,只不过length的属性他的Enumerable和Configurable都是false。

ok,我们知道了在js中是怎么定义对象的了。对于对象来说属性的读取和赋值是最常用的功能,js引擎为了让整个操作更高效,引入了一个Shapes的对象。整个Shapes对象的实现方式是每个js内核都有的实现方式,只不过在每个js内核里叫法不一样,可能比较常见的叫法叫做hidden class,因为跟ES6中class的叫法会有些混乱所以我们这里用SpiderMonkey内核中的叫法Shapes。下面就看看Shapes是怎么让属性的存取更高效的。

还是我们之前的对象a = { x:5,y:6 },之前在内存中的储存方法,把他的一个属性的所有值都存在一个JSObject中,假设我们现在还有一个a = { x:7,y:8 },或者多个属性名称都一样的对象,那么这么存储对象是不是有些浪费内存,因为他们都有相同的属性名称和attributes。我们属性的名称和attributes存在一个共同的shape中,并且分别将value存到另外一个对象当中,将这个对象所在的索引也存在shape中。

那有多个相同属性和attributes的对象时好处就很明显了。不论有多少个对象,只要他们是一样的shape,我们只需要存储一次他们shape就可以了。

但是在js中有几种情况是无法共享shapes的

第一种情况就是如果属性的顺序不一样是无法共享shape的,例如:{ x: 4, y: 5 }{ y: 5, x: 4 }他们的shape是不一样的

另外一种情况

const o = {};
o.x = 5;
o.y = 6;

这是一种在js中非常常见的写法,先定义一个空对象,然后在创建对象属性。在js引擎中这种情况是怎么存的呢?

  1. 首先执行第一行代码的时候会先创建一个空的shape,因为初始化的对象没有属性
  2. 当执行第二行代码,增加一个x的属性给对象并且赋值为6,js引擎会创建一个新的shape包含x,并且有一个指针指向前一个shape
  3. 第三行代码又添加了一个新的y属性,js引擎会在创建一个新的shape,这时shape只包含y,并且这个shape有一个指针指向前一个shape。

这个创建新的shape并且连接前一个shape的操作叫做shape变迁。

如果我们要给o.y赋值,js引擎会先查找x所在的shape,通过最后一个shape对前一个shape的引用一直到找到包含x的shape并且赋值。

const a = {};
a.x = 5;
const b = { x: 6 };

js引擎在创建shape时并不是一直都是从空的shape开始创建的,如果你创建的对象一开始就包含某几个属性,没必要从空的shape开始创建.而是可以直接创建一个包含所有属性的shape.所以a对象从shape变迁得到的xshape和直接得到的xshape是两个不一样的shape对象。

能意识到申明对象的方式对创建shape的影响是非常有必要的,因为共享shape除了节省一些内存以外对于js执行效率起着非常大的作用,为什么呢?

Inline Caches

在js引擎中引入shape这个对象的主要原因其实是因为Inline Caches(ICs).ICs是一个js能够快速运行的关键因素。js引擎用ICs缓存对象属性查找的信息,减少查找对象属性带来的开销。

比如我们有这么一个方法

function getX(o) {
	return o.x;
}

这个方法总共有一个参数o,并且访问了参数ox属性.

inline cache会缓存4个字段分别是

  • shape:调用参数他的shape对象
  • prop:方法中需要使用的参数对象的属性
  • offset:shape中属性值对象的索引
  • state:有三个值,会在运行时根据实际情况变化
    • Monomorphic:运行时方法中使用的参数shape是同一个
    • Polymorphic:shape不一致
    • Megamorphic:同时存在Monomorphic和Polymorphic的情况

当方法被第一次调用的时候inline cache中没有值,将第一次调用参数的shape对象放入shape缓存,并将offset赋值成x属性的offset。当后续继续调用这个方法的时候不论什么参数对象,只需要对比shape是否是同一个shape(也就是共享的shape),如果是同一个shape直接使用offset去值对象中取值就可以了.减少了很多的查找开销。所以保持inline cache状态为Monomorphic对性能会有很大的帮助。react团队做了一个实验同一个方法Monomorphic比Polymorphic的性能会高出100倍。

Shapes和ICs在React中的应用

在React中template会用一个FiberNode的对象来表示,FiberNode在我看来是介于Template和Dom的一个对象。

他需要在React各个方法之间非常频繁的被使用,但是在React Template中会有 HtmlElement,Text,Component等等不同类型的Node,React会将不同类型的Node不同的属性字段都合并到一起,这样我们就有一个所有属性都一样的FiberNode,也就是说他们的shape是完全一样的了,因此不论在哪个方法中使用FiberNode inline cache都可以保持Monomorphic的状态,这就大大的减少了方法读取属性值的开销,增加了性能。

结论

我们这里讨论了js引擎是如何存储对象以及数组这个有点特殊的对象,也讨论了shapes和inline cache是如何帮助性能的。基于这些知识,我们知道了在写js哪些写法是有助于提高性能的 用同一种方式来初始化对象,这样他们就能共享shapes。后面我们还有讨论更多的类似的基于js引擎原理的性能技巧。