高频dom操作和页面性能优化探索

  重排是指元素的位置或尺寸发生了变化,浏览器需要重新计算渲染树,而新的渲染树建立后,浏览器会重新绘制受影响的元素。

  页面重绘的速度要比页面重排的速度快,在页面交互中要尽量避免页面的重排操作。浏览器不会在js执行的时候更新DOM,而是会把这些DOM操作存放在一个队列中,在js执行完之后按顺序一次性执行完毕,因此在js执行过程中用户一直在被阻塞。

  Style: 计算样式,如果元素的样式有改变,在这一步重新计算样式,并匹配到对应的DOM上

  在网页生成的时候,至少会进行一次布局和渲染,在后面用户的操作时,不断的进行重绘或重排,因此如果在js中存在很多DOM操作,就会不断地出发重绘或重排,影响页面性能。

  如前面所说,DOM操作影响页面性能的核心问题主要在于DOM操作导致了页面的重绘或重排,为了减少由于重绘和重排对网页性能的影响,我们要知道都有哪些操作会导致页面的重绘或者重排。

  例如改变DOM元素的宽高值时,原渲染树中的相关节点会失效,浏览器会根据变化后的DOM重新排建渲染树中的相关节点。如果父节点的几何属性变化时,还会使其子节点及后续兄弟节点重新计算位置等,造成一系列的重排。

  添加DOM节点、修改DOM节点位置及删除某个节点都是对DOM树的更改,会造成页面的重排。浏览器布局是从上到下的过程,修改当前元素不会对其前边已经遍历过的元素造成影响,但是如果在所有的节点前添加一个新的元素,则后续的所有元素都要进行重排。

  窗口尺寸的改变会影响整个网页内元素的尺寸的改变,即DOM元素的集合属性变化,因此会造成重排。

  在最近做的抽奖项目中,就遇到了这样的由于高频操作DOM,导致页面性能变差的问题。在经历几轮抽奖后,文字滚动速度越来越慢,肉眼能感受到与第一次抽奖时文字滚动速度的明显差别,如持续时间过长或轮次过多,还会造成浏览器假死现象。

  FPS:最上面一栏为绿色柱形为帧率(FPS),顶点值为60fps,上方红色方块表示长帧,这些长帧被Chrome称为jank(卡顿)。

  CPU:第二栏为CPU,蓝色表示loading(网络通信和HTML解析),黄色表示ing(js执行时间),紫色表示rendering(样式计算和布局,即重排), 绿色为painting(即重绘)。

  由上图可以看出,在文字滚动过程中红色方块出现频繁,页面中存在的卡顿过多。帧率的值越低,人眼感受到的效果越差。

  在这段区域内最大一帧达到了49.7ms,帧率只有20fps,接下来看看这一帧里是什么因素耗时过长

  由上图可以看出,耗时最大的在ing,js的执行时间达到了44.9ms,占总时间的93.2%,因为主要靠js计算控制DOM的显示内容,所以js运行时间过长。

  由此可知,主要是rolling这个函数执行时间过长,对该部分失帧影响较大。而这个函数的主要作用就是实现文字的滚动效果,也可以从代码中看出,这个函数利用的setTimeout来反复执行,并且在这个函数中存在着循环以及大量的DOM操作,造成了页面的失帧等问题。

  一次性生成全部li,并且隐藏这些li,随机生成一组随机数数组,只有index与数组里面的随机数相等时,才显示该位置的li,虽然也会触发重排和重绘,但是性能要远远高于直接操作DOM的添加和删除。

  setTimeout():在特定的时间后执行函数,而且只执行一次,如果在特定时间前想取消执行函数,可以用clearTimeout立即取消执行。但是并不是每次执行setTimeout都会在特定的时间后执行,页面加载后js会按照主线程中的顺序按序执行那个,如果在延迟时间内主线程不空闲,setTimeout里面的函数是不会执行的,它会延迟到主线程空闲时才执行。

  setInterval():在特定的时间间隔内重复执行函数,除非主动清除它,不然会一直执行下去,清除函数可以使用clearInterval。setInterval也会等到主线程空闲了再执行,但是setInterval去排队时,如果发现自己还在队列中未执行,就会被drop掉,所以可能会造成某段时间的函数未被执行。

  requestAnimationFrame():它不需要设置时间间隔,它会在浏览器每次刷新之前执行回调函数的任务。这样我们动画的更新就能和浏览器的刷新频率保持一致。requestAnimationFrame在运行时,浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了CPU开销。

  在采用上面的方法进行优化后,在经历多轮抽奖后,文字滚动速度依旧正常,网页性能良好,不会出现文字滚动速度越来越慢,最后导致浏览器假死的现象。

  优化前的代码对DOM操作很频繁,因此FPS值普遍偏低,而优化后可以看出红色方块明显减少,FPS值一直处于高值。

  优化前js的CPU占用率较高,而优化后占用CPU的主要为渲染时间,因为优化后的代码只是控制了节点的显示和隐藏,所以在js上消耗较少,在渲染上消耗较大。

  吸顶导航条要求当页面滚动到某个区域时,对应该区域的导航条在设置的显示范围内保持吸顶显示。涉及到的操作:

  由于scroll事件被触发的频率高、间隔近,如果此时进行DOM操作或计算并且这些DOM操作和计算无法在下一次scroll事件发生前完成,就会造成掉帧、页面卡顿,影响用户体验。

  页面加载时根据当前页面中吸顶导航的数量复制对应的DOM,并且隐藏这些导航。当页面滚动到指定区域后,显示对应的导航。

  将复制的DOM存储到数组中,将该数组append到对应的父节点下,而不是根据复制得到DOM的数量依次循环插入到父节点下。

  如果某个节点将在后续进行多次操作,可以将该节点利用变量存储起来,而不是每次进行操作时都去查找一遍该节点。

  为了减少DOM操作对页面性能产生的影响,在实现页面的交互效果时一定要注意一下几点:

  优化前的代码中,每进行一次循环,都会读取一次div的innerHtml属性,并且对这个属性进行了重新赋值,即每循环一次就会操作两次DOM,因此执行时间很长,页面性能差。

  在优化后的代码中,将要更新的DOM内容进行缓存,在循环时只操作字符串,循环结束后字符串的值写入到div中,只进行了一次查找innerHtml属性和一次对该属性重新赋值的操作,因此同样的循环次数先,优化后的方法执行时间远远少于优化前。

  在抽奖项目中频繁操作DOM来控制文字滚动的方法(demo:导致页面性能很差,最后修改为如下代码。

  上面代码的优化原理即先生成所有DOM节点,但是所有节点均不显示出来,利用vue.js中的v-show,根据计算的随机数来控制显示某个li,来达到文字滚动效果。

  如果采用jquery,则需要将生成的所有li全部存放在ul下,并且隐藏它们,在根据生成的随机数组,利用jquery查找index与生成的随机数对应的li并显示,达到文字滚动效果。

  display属性值为none的元素不在渲染树中,因此对隐藏的元素操作不会引发其他元素的重排。如果要对一个元素进行多次DOM操作,可以先将其隐藏,操作完成后再显示。这样只在隐藏和显示时触发2次重排,而不会是在每次进行操作时都出发一次重排。

  下图为同样的循环次数下未隐藏节点直接进行DOM操作的rendering时间(图一)和隐藏节点再进行DOM操作的rendering时间(图二)

  由对比图可以看出,总时间、js执行时间以及rendering时间都明显减少,并且避免了painting以及其他的一些操作。

  在上面的代码中,每对element进行一次样式更改都会影响该元素的集合结构,最糟糕情况下会触发三次重排。

  优化方式:利用js或jquery对该元素的class重新赋值,获得新的样式,这样减少了多次的DOM操作。

  到此本文结束,如果对于问题分析存在不正确的地方,还请及时指出,多多交流。

相关阅读