网页性能管理:重绘和重排

网页生成的过程

要理解网页性能为什么不好,就要了解网页是怎么生成的。

网页的生成过程,大致可以分成五步:
1.HTML代码转化成DOM
2.CSS代码转化成CSSOMCSS Object Model)。
3.结合DOMCSSOM,生成一棵渲染树(包含每个节点的视觉信息)。
4.生成布局(layout),即将所有渲染树的所有节点进行平面合成。
5.将布局绘制(paint)在屏幕上。

"生成布局"(flow)和"绘制"(paint)这两步,合称为"渲染"(render)。

重排和重绘

网页生成的时候,至少会渲染一次。用户访问的过程中,还会不断重新渲染。

重新渲染,就需要重新生成布局和重新绘制。前者叫做重排(reflow),后者叫做重绘(repaint)。

需要注意的是,重绘不一定需要重排重排必然导致重绘

对于性能的影响

提高网页性能,就是要降低"重排"和"重绘"的频率和成本,尽量少触发重新渲染。

DOM变动和样式变动,都会触发重新渲染。但是,浏览器已经很智能了,会尽量把所有的变动集中在一起,排成一个队列,然后一次性执行,尽量避免多次重新渲染。

div.style.color = 'blue';
div.style.marginTop = '30px';

上面代码中,div元素有两个样式变动,但是浏览器只会触发一次重排和重绘。

如果写得不好,就会触发两次重排和重绘。

div.style.color = 'blue';
var margin = parseInt(div.style.marginTop);
div.style.marginTop = (margin + 10) + 'px';

上面代码对div元素设置背景色以后,第二行要求浏览器给出该元素的位置,所以浏览器不得不立即重排。

一般来说,样式的写操作之后,如果有下面这些属性的读操作,都会引发浏览器立即重新渲染。

offsetTop/offsetLeft/offsetWidth/offsetHeight
scrollTop/scrollLeft/scrollWidth/scrollHeight
clientTop/clientLeft/clientWidth/clientHeight
getComputedStyle()

所以,从性能角度考虑,尽量不要把读操作和写操作,放在一个语句里面。

// bad
div.style.left = div.offsetLeft + 10 + "px";
div.style.top = div.offsetTop + 10 + "px";

// good
var left = div.offsetLeft;
var top  = div.offsetTop;
div.style.left = left + 10 + "px";
div.style.top = top + 10 + "px";

一般的规则是:

  • 样式表越简单,重排和重绘就越快。
  • 重排和重绘的DOM元素层级越高,成本就越高。
  • table元素的重排和重绘成本,要高于div元素。

重排何时发生

  • 添加或者删除可见的DOM元素。
  • 元素位置改变。
  • 元素尺寸改变。
  • 元素内容改变(例如:一个文本被另一个不同尺寸的图片替代)。
  • 页面渲染初始化(这个无法避免)。
  • 浏览器窗口尺寸改变。

最小化重绘和重排

  • DOM的多个读操作(或多个写操作),应该放在一起。不要两个读操作之间,加入一个写操作。

  • 如果某个样式是通过重排得到的,那么最好缓存结果。避免下一次用到的时候,浏览器又要重排。

  • 不要一条条地改变样式,而要通过改变class,或者csstext属性,一次性地改变样式。

// bad
var left = 10;
var top = 10;
el.style.left = left + "px";
el.style.top  = top  + "px";

// good 
el.className += " theclassname";

// good
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
  • 尽量使用离线DOM,而不是真实的网面DOM,来改变元素样式。比如,操作Document Fragment对象,完成后再把这个对象加入DOM。再比如,使用 cloneNode()方法,在克隆的节点上进行操作,然后再用克隆的节点替换原始节点。
  • apple
  • orange

如果代码中要添加内容为peachwatermelon两个选项,你会怎么做?

let lis = document.getElementById('fruit');
let li=document.createElement('li');
li.innerHTML='apple';
lis.appendChild(li);

let li = document.createElement('li');
li.innerHTML = 'watermelon';
lis.appendChild(li);

很容易想到如上代码,但是很显然,重排了两次,怎么破?
前面我们说了,隐藏的元素不在渲染树中,太棒了,我们可以先把idfruitul元素隐藏(display=none),然后添加li元素,最后再显示,但是实际操作中可能会出现闪动,原因这也很容易理解。

这时,fragment元素就有了用武之地了。

let fragment = document.createDocumentFragment();

let li = document.createElement('li');
li.innerHTML = 'apple';
fragment.appendChild(li);

let li = document.createElement('li');
li.innerHTML = 'watermelon';
fragment.appendChild(li);

document.getElementById('fruit').appendChild(fragment);

文档片段是个轻量级的document对象,它的设计初衷就是为了完成这类任务——更新和移动节点。文档片段的一个便利的语法特性是当你附加一个片断到节点时,实际上被添加的是该片断的子节点,而不是片断本身。只触发了一次重排,而且只访问了一次实时的DOM

  • 先将元素设为display: none(需要1次重排和重绘),然后对这个节点进行100次操作,最后再恢复显示(需要1次重排和重绘)。这样一来,你就用两次重新渲染,取代了可能高达100次的重新渲染。

  • position属性为absolutefixed的元素,重排的开销会比较小,因为不用考虑它对其他元素的影响。

  • 只在必要的时候,才将元素的display属性为可见,因为不可见的元素不影响重排和重绘。另外,visibility : hidden的元素只对重绘有影响,不影响重排。

  • 使用虚拟DOM的脚本库,比如React等。

  • 使用window.requestAnimationFrame()window.requestIdleCallback()这两个方法调节重新渲染.

参考文档:
高性能JavaScript 重排与重绘
网页性能管理详解

你可能感兴趣的:(网页性能管理:重绘和重排)