前导知识

网页生成过程

  1. HTML被HTML解析器解析成DOM 树,HTML中的每个tag都是DOM树中的1个节点,根节点就是常用的document对象DOM树里包含了所有HTML标签,包括display:none隐藏,还有用JS动态添加的元素等。
  2. CSS被CSS解析器解析成CSSOM 树
  3. 结合DOM树和CSSOM树,生成一棵渲染树(Render Tree),渲染树中每个节点都有自己的样式,而且渲染树不包含隐藏的节点 (比如display:none的节点,还有head节点),因为这些节点不会用于呈现,所以就不会包含到 render tree中。
  4. 生成布局(flow),将渲染树的所有节点进行平面合成
  5. 将布局绘制(paint)在屏幕上

第四步和第五步是最耗时的部分,这两步合起来,就是通常所说的渲染

渲染

网页生成的时候,至少会渲染一次

在用户访问的过程中,还会不断重新渲染

重新渲染需要重复之前的第四步(重新生成布局)+第五步(重新绘制)或者只有第五个步(重新绘制)。

重排与重绘的关系

  • 重绘:某些元素的外观被改变,例如:元素的填充颜色
  • 重排:重新生成布局,重新排列元素。

就如上面的概念一样,单单改变元素的外观,肯定不会引起网页重新生成布局,但当浏览器完成重排之后,将会重新绘制受到此次重排影响的部分

比如改变元素高度,这个元素乃至周边dom都需要重新绘制。

也就是说:"重绘"不一定会出现"重排","重排"必然会出现"重绘"

注意:

  • 由于浏览器使用流式布局,对Render Tree的计算通常只需要遍历一次就可以完成,但<table/>及其内部元素除外,他们可能需要多次计算,通常要花3倍于同等元素的时间,这也是为什么要避免使用<table/>布局的原因之一。

回流必将引起重绘,重绘不一定会引起回流。

重排/回流 (Reflow)

当DOM的变化(大小、结构、或某些属性)影响了元素的几何信息(DOM对象的位置和尺寸大小),浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排。

重排也叫回流,重排的过程以下面这种理解方式更清晰一些:

回流就好比向河里(文档流)扔了一块石头(dom变化),激起涟漪,然后引起周边水流受到波及,所以叫做回流

会导致回流的操作:

  • 页面首次渲染

  • 浏览器窗口大小发生改变——resize事件发生时

  • 添加或者删除可见的DOM元素;

  • 元素内容变化(用户在input框中输入文字或图片大小等等)

  • 元素字体大小变化

  • 添加或者删除可见DOM元素

  • 激活CSS伪类(例如::hover

  • 查询某些属性或调用某些方法

  • 常见引起重排属性和方法
    width height margin padding
    display border position overflow
    clientWidth clientHeight clientTop clientLeft
    offsetWidth offsetHeight offsetTop offsetLeft
    scrollWidth scrollHeight scrollTop scrollLeft
    scrollIntoView() scrollTo() getComputedStyle()
    getBoundingClientRect() scrollIntoViewIfNeeded()

重排影响的范围

由于浏览器渲染界面是基于流失布局模型的,所以触发重排时会对周围DOM重新排列,影响的范围有两种:

  • 全局范围:从根节点html开始对整个渲染树进行重新布局。

    有时即使仅仅回流一个单一的元素,它的父元素以及任何跟随它的元素也会产生重排。

  • 局部范围:对渲染树的某部分或某一个渲染对象进行重新布局

    把一个dom的宽高之类的几何信息定死,然后在dom内部触发重排,就只会重新渲染该dom内部的元素,而不会影响到外界。

重绘 (Repaint)

当页面中元素样式的改变并不影响它在文档流中的位置时,浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。

常见的引起重绘的属性:

color border-style visibility background
text-decoration background-image background-position background-repeat
outline-color outline outline-style border-radius
outline-width box-shadow background-size

合成(Composite)

图片说明

更改一个既不要布局也不要绘制的属性,浏览器会跳过布局和绘制,浏览器会主动将渲染层提至合成层,只执行后续的合成操作,以下是影响 composite 的因素:

  • 3D transforms: translate3d, translateZ 等;
  • video, canvas, iframe 等元素;
  • 通过 Element.animate() 实现的 opacity 动画转换;
  • 通过 СSS 动画实现的 opacity 动画转换;
  • position: fixed;
  • will-change;
  • filter;

提升为合成层简单说来有以下几点好处:

  1. 合成层的位图,会交由 GPU 合成,比 CPU 处理要快
  2. 当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层
  3. 对于 transform 和 opacity 效果,不会触发 layout 和 paint

浏览器的渲染队列

div.style.left = '10px';
div.style.top = '10px';
div.style.width = '20px';
div.style.height = '20px';

这段代码理论上会触发4次重排+重绘,因为每一次都改变了元素的几何属性,实际上最后只触发了一次重排,这都得益于浏览器的渲染队列机制

当修改了元素的几何属性,导致浏览器触发重排或重绘时。它会把该操作放进渲染队列,等到队列中的操作到了一定的数量或者到了一定的时间间隔时,浏览器就会批量执行这些操作。

div.style.left = '10px';
console.log(div.offsetLeft);

当在console中访问节点会导致重排的属性时,浏览器会立即重排+重绘,因为队列中,可能会有影响到这些值的操作,为了给我们最精确的值,浏览器会立即重排+重绘

强制刷新队列的style样式请求

  • clientWidthclientHeightclientTopclientLeft
  • offsetWidthoffsetHeightoffsetTopoffsetLeft
  • scrollWidthscrollHeightscrollTopscrollLeft
  • getComputedStyle()
  • getBoundingClientRect()

在开发中,应该谨慎的使用这些style请求,注意上下文关系,避免一行代码一个重排,这对性能是个巨大的消耗

性能影响

重排比重绘的代价要更高。

尽可能的减少重排的次数、重排范围

  • 重排需要更新渲染树,性能花销非常大:
  • 它们的代价是高昂的,会破坏用户体验,并且让UI展示非常迟缓,需要尽可能的减少触发重排的次数。
  • 重排的性能花销跟渲染树有多少节点需要重新构建有关系:
  • 所以应该尽量以局部布局的形式组织html结构,尽可能小的影响重排的范围。而不是一溜的堆砌标签,随便一个元素触发重排都会导致全局范围的重排。

优化措施

CSS

  • 避免使用table布局。

  • 尽可能在DOM树的最末端改变class

  • 避免设置多层内联样式。

  • 将动画效果应用到position属性为absolutefixed的元素上。

  • 避免使用CSS表达式(例如:calc())。

  • 优化动画,牺牲一些平滑,来换取速度,比如实现一个动画,以1个像素为单位移动这样最平滑,但是reflow就会过于频繁,大量消耗CPU资源,如果以3个像素为单位移动则会好很多

  • 启用GPU加速,GPU 加速通常包括以下几个部分:Canvas2D,布局合成, CSS3转换(transitions),CSS3 3D变换(transforms),WebGL和视频(video)。

    /*
     * 根据上面的结论
     * 将 2d transform 换成 3d
     * 就可以强制开启 GPU 加速
     * 提高动画性能
     */
    div {
      transform: translate3d(10px, 10px, 0);
    }

JavaScript

  • 分离读写操作/样式集中改变,避免频繁操作样式,或者将样式列表定义为class并一次性更改class属性。

    div.style.left = '10px';
    div.style.top = '10px';
    div.style.width = '20px';
    div.style.height = '20px';
    console.log(div.offsetLeft);
    console.log(div.offsetTop);
    console.log(div.offsetWidth);
    console.log(div.offsetHeight);        
  • 避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。

  • 也可以先为元素设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。

  • 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。

  • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。