zoukankan      html  css  js  c++  java
  • 深入了解浏览器重排和重绘

    浏览器的渲染引擎

    浏览器的主要组件有:用户界面、浏览器引擎、渲染引擎、网络、用户界面后端、JavaScript解释器、数据存储。

    浏览器的主要功能就是向服务器发出请求,在浏览器窗口中展示您选择的网络资源。浏览器在解析HTML文档,将网页内容展示到浏览器上的流程,其实就是渲染引擎完成的。

    浏览器的渲染过程

    我们在这里讨论Gecko和Webkit这两种渲染引擎,其中Firefox 使用的是 Gecko,这是 Mozilla 公司“自制”的呈现引擎。而 Safari 和 Chrome 浏览器使用的都是 WebKit。

    WebKit 渲染引擎的主流程:

    Mozilla 的 Gecko渲染引擎的主流程:

    1. HTML被HTML解析器解析成DOM 树

    2. css则被css解析器解析成CSSOM 树

    3. 结合DOM树和CSSOM树,生成一棵渲染树(Render Tree)

    4. Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)(生成布局(flow),即将所有渲染树的所有节点进行平面合成)

    5. Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素(将布局绘制(paint)在屏幕上)

    6. Display:将像素发送给GPU,展示在页面上。(这一步其实还有很多内容,比如会在GPU将多个合成层合并为同一个层,并展示在页面中。而css3硬件加速的原理则是新建合成层)

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

    渲染树与渲染对象

    生成渲染树

    渲染树(render tree)是由可视化元素按照其显示顺序而组成的树,也是文档的可视化表示。它的作用是让您按照正确的顺序绘制内容。

    为了构建渲染树,浏览器主要完成了以下工作:

    1. 从DOM树的根节点开始遍历每个可见节点。

    2. 对于每个可见的节点,找到CSSOM树中对应的规则,并应用它们。

    3. 根据每个可见节点以及其对应的样式,组合生成渲染树。

    第一步中,既然说到了要遍历可见的节点,那么我们得先知道,什么节点是不可见的。不可见的节点包括:

    • 一些不会渲染输出的节点,比如script、meta、link等。

    • 一些通过css进行隐藏的节点。比如display:none。注意,利用visibility和opacity隐藏的节点,还是会显示在渲染树上的。只有display:none的节点才不会显示在渲染树上。

    注意:渲染树只包含可见的节点

    渲染对象

    Firefox 将渲染树中的元素称为“框架”。WebKit 使用的术语是渲染器(renderer)或渲染对象(render object)。

    每一个渲染对象都代表了一个矩形区域,通常对应相关节点的css框,包含宽度、高度和位置等几何信息。框的类型受“display”样式属性影响,根据不同的 display 属性,使用不同的渲染对象(如 RenderInlineRenderBlockRenderListItem 等对象)。

    WebKits RenderObject 类是所有渲染对象的基类,其定义如下:

    class RenderObject{
      virtual void layout();
      virtual void paint(PaintInfo);
      virtual void rect repaintRect();
      Node* node;  //the DOM node
      RenderStyle* style;  // the computed style
      RenderLayer* containgLayer; //the containing z-index layer}

    我们可以看到,每个渲染对象都有 layout 和 paint方法,分别对应了回流和重绘的方法。

    渲染

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

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

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

    重排比重绘大

    大,在这个语境里的意思是:谁能影响谁?

    • 重绘:某些元素的外观被改变,例如:元素的填充颜色

    • 重排:重新生成布局,重新排列元素。

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

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

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

    重排(reflow)

    概念:

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

    重排也叫回流

    前面我们通过构造渲染树,我们将可见DOM节点以及它对应的样式结合起来,可是我们还需要计算它们在设备视口(viewport)内的确切位置和大小,这个计算的阶段就是回流。

    为了弄清每个对象在网站上的确切大小和位置,浏览器从渲染树的根节点开始遍历,我们可以以下面这个实例来表示:

    <!DOCTYPE html>
    <html>
      <head>
        <meta name="viewport" content="width=device-width,initial-scale=1">
        <title>Critial Path: Hello world!</title>
      </head>
      <body>
        <div style=" 50%">
          <div style=" 50%">Hello world!</div>
        </div>
      </body>
    </html>

    我们可以看到,第一个div将节点的显示尺寸设置为视口宽度的50%,第二个div将其尺寸设置为父节点的50%。而在回流这个阶段,我们就需要根据视口具体的宽度,将其转为实际的像素值。(如下图)

    常见引起重排属性和方法

    任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发重排,下面列一些栗子:

    1. 添加或者删除可见的DOM元素

    2. 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)

    3. 元素的位置发生变化

    4. 内容变化,比如文本变化或图片被另一个不同尺寸的图片所替代

    5. 页面一开始渲染的时候(这肯定避免不了)
    6. 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)

    7. 计算 offsetWidth 和 offsetHeight 属性

    8. 设置 style 属性的值

    改变这些属性会触发回流:

    • 盒模型相关的属性: widthheightmargindisplayborder

    • 定位属性及浮动相关的属性: top,position,float

    • 改变节点内部文字结构也会触发回流:text-alignoverflowfont-sizeline-heightvertival-align

    重排影响的范围:

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

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

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

    全局范围重排:(比如,滚动条出现的时候或者修改了根节点)

    <body>
      <div class="hello">
        <h4>hello</h4>
        <p><strong>Name:</strong>BDing</p>
        <h5>male</h5>
        <ol>
          <li>coding</li>
          <li>loving</li>
        </ol>
      </div>
    </body>

    当p节点上发生reflow时,hello和body也会重新渲染,甚至h5和ol都会收到影响。

    局部范围重排:

    用局部布局来解释这种现象:把一个dom的宽高之类的几何信息定死,然后在dom内部触发重排,就只会重新渲染该dom内部的元素,而不会影响到外界。

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

    重排需要更新渲染树,性能花销非常大:

    它们的代价是高昂的,会破坏用户体验,并且让UI展示非常迟缓,我们需要尽可能的减少触发重排的次数。

    重排的性能花销跟渲染树有多少节点需要重新构建有关系:

    所以我们应该尽量以局部布局的形式组织html结构,尽可能小的影响重排的范围。

    而不是像全局范围的示例代码一样一溜的堆砌标签,随便一个元素触发重排都会导致全局范围的重排。

    "回流"还是"重排"?

    本质上它们是同样的流程,只是在不同浏览器引擎下的“说法”有所差异。

    • Gecko 将视觉格式化元素组成的树称为 "Frame tree框架树。每个元素都是一个框架;

         对于元素的放置,将其称为 "Reflow回流

    • WebKit 使用的术语是 "Render Tree渲染树,它由"Render Objects"组成。对于元素的放置,WebKit 使用的术语是 "Layout布局(或Relayout重排

    重绘(repaint):

    概念:

    当一个元素的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程,叫做重绘。

    最终,我们通过构造渲染树和回流阶段,我们知道了哪些节点是可见的,以及可见节点的样式和具体的几何信息(位置、大小),那么我们就可以将渲染树的每个节点都转换为屏幕上的实际像素,这个阶段就叫做重绘节点。

    重绘是填充像素的过程。它涉及绘出文本、颜色、图像、边框和阴影,基本上包括元素的每个可视部分。在重绘阶段,系统会遍历渲染树,并调用渲染对象的“paint”方法,将渲染对象的内容显示在屏幕上。

    常见的引起重绘的属性:

    绘制顺序

    绘制的顺序其实就是元素进入堆栈样式上下文的顺序。这些堆栈会从后往前绘制,因此这样的顺序会影响绘制。块渲染对象的堆栈顺序如下:

    1、背景颜色

    2、背景图片

    3、边框

    4、子代

    5、轮廓

    浏览器的渲染队列:

    思考以下代码将会触发几次渲染?

    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);
    div.style.top = '10px';
    console.log(div.offsetTop);
    div.style.width = '20px';
    console.log(div.offsetWidth);
    div.style.height = '20px';
    console.log(div.offsetHeight);

    这段代码会触发4次重排+重绘,因为在console中你请求的这几个样式信息,无论何时浏览器都会立即执行渲染队列的任务,即使该值与你操作中修改的值没关联。

    因为队列中,可能会有影响到这些值的操作,为了给我们最精确的值,浏览器会立即重排+重绘

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

    1. offsetTop, offsetLeft, offsetWidth, offsetHeight

    2. scrollTop, scrollLeft, scrollWidth, scrollHeight

    3. clientTop, clientLeft, clientWidth, clientHeight

    4. getComputedStyle(), 或者 IE的 currentStyle

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

    重排优化建议

    就像上文提到的我们要尽可能的减少重排次数、重排范围,这样说很泛,下面是一些行之有效的建议,大家可以参考一下。

    1. 分离读写操作

    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);

    还是上面触发4次重排+重绘的代码,这次只触发了一次重排:

    在第一个console的时候,浏览器把之前上面四个写操作的渲染队列都给清空了。剩下的console,因为渲染队列本来就是空的,所以并没有触发重排,仅仅拿值而已。

    2. 样式集中改变

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

    虽然现在大部分浏览器有渲染队列优化,不排除有些浏览器以及老版本的浏览器效率仍然低下:

    建议通过改变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;";

    3. 缓存布局信息(避免触发同步布局事件)

    当我们访问元素的一些属性的时候,会导致浏览器强制清空队列,进行强制同步布局。举个例子,比如说我们想将一个p标签数组的宽度赋值为一个元素的宽度,我们可能写出这样的代码:

    function initP() {
        for (let i = 0; i < paragraphs.length; i++) {
            paragraphs[i].style.width = box.offsetWidth + 'px';
        }
    }

    这段代码看上去是没有什么问题,可是其实会造成很大的性能问题。在每次循环的时候,都读取了box的一个offsetWidth属性值,然后利用它来更新p标签的width属性。这就导致了每一次循环的时候,浏览器都必须先使上一次循环中的样式更新操作生效,才能响应本次循环的样式读取操作。每一次循环都会强制浏览器刷新队列。我们可以优化为:

    const width = box.offsetWidth;
    function initP() {
        for (let i = 0; i < paragraphs.length; i++) {
            paragraphs[i].style.width = width + 'px';
        }
    }

    避免循环读取offsetLeft等属性,在循环之前把它们存起来

    另外一个例子:

    // bad 强制刷新 触发两次重排
    div.style.left = div.offsetLeft + 1 + 'px';
    div.style.top = div.offsetTop + 1 + 'px';
    
    // good 缓存布局信息 相当于读写分离
    var curLeft = div.offsetLeft;
    var curTop = div.offsetTop;
    div.style.left = curLeft + 1 + 'px';
    div.style.top = curTop + 1 + 'px';

    避免循环读取offsetLeft等属性,在循环之前把它们存起来

    4. 离线改变dom

    批量修改DOM

    当我们需要对DOM进行一系列修改的时候,可以通过以下步骤减少回流重绘次数:

    1. 使元素脱离文档流

    2. 对其进行多次修改

    3. 将元素带回到文档中。

    该过程的第二步和第三步可能会引起回流,但是经过第一步之后,对DOM的所有修改都不会引起回流,因为它已经不在渲染树了。

    有三种方式可以让DOM脱离文档流:

    • 隐藏要修改的元素,应用修改,重新显示

      在要操作dom之前,通过display隐藏dom,当操作完成之后,才将元素的display属性为可见,因为不可见的元素不会触发重排和重绘。   

         dom.display = 'none'   

             // 修改dom样式   

             dom.display = 'block'

    • 通过使用DocumentFragment创建一个dom碎片,在它上面批量操作dom,操作完成之后,再添加到文档中,这样只会触发一次重排。

    • 将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素。

    考虑我们要执行一段批量插入节点的代码:

    function appendDataToElement(appendToElement, data) {
        let li;
        for (let i = 0; i < data.length; i++) {
            li = document.createElement('li');
            li.textContent = 'text';
            appendToElement.appendChild(li);
        }
    }
    
    const ul = document.getElementById('list');
    appendDataToElement(ul, data);

    如果我们直接这样执行的话,由于每次循环都会插入一个新的节点,会导致浏览器回流一次。

    我们可以使用这三种方式进行优化:

    隐藏元素,应用修改,重新显示

    这个会在展示和隐藏节点的时候,产生两次重绘

    function appendDataToElement(appendToElement, data) {
        let li;
        for (let i = 0; i < data.length; i++) {
            li = document.createElement('li');
            li.textContent = 'text';
            appendToElement.appendChild(li);
        }
    }
    const ul = document.getElementById('list');
    ul.style.display = 'none';
    appendDataToElement(ul, data);
    ul.style.display = 'block';

    使用文档片段(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档

    const ul = document.getElementById('list');
    const fragment = document.createDocumentFragment();
    appendDataToElement(fragment, data);
    ul.appendChild(fragment);

    将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素。

    const ul = document.getElementById('list');
    const clone = ul.cloneNode(true);
    appendDataToElement(clone, data);
    ul.parentNode.replaceChild(clone, ul);

    对于上述那种情况,我写了一个demo来测试修改前和修改后的性能。然而实验结果不是很理想。

    原因:原因其实上面也说过了,浏览器会使用队列来储存多次修改,进行优化,所以对这个优化方案,我们其实不用优先考虑。

    5. position属性为absolute或fixed

    position属性为absolute或fixed的元素,重排开销比较小,不用考虑它对其他元素的影响

    6. 优化动画

    • 对于复杂动画效果,使用绝对定位让其脱离文档流

       可以把动画效果应用到position属性为absolute或fixed的元素上,这样对其他元素影响较小

         对于复杂动画效果,使用绝对定位让其脱离文档流,否则会引起父元素及后续元素大量的回流

    • 动画效果还应牺牲一些平滑,来换取速度,这中间的度自己衡量:

      比如实现一个动画,以1个像素为单位移动这样最平滑,但是reflow就会过于频繁,大量消耗CPU资源,如果以3个像素为单位移动则会好很多。

    • css3硬件加速(GPU加速)

         比起考虑如何减少回流重绘,我们更期望的是,根本不要回流重绘。这个时候,css3硬件加速就闪亮登场啦!!(最佳的性能渲染流程,就是直接避开回流和重绘,只运行Composite合成这一操作。)

         

         划重点:使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘 。但是对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。

         GPU(图像加速器):

         GPU 硬件加速是指应用 GPU 的图形性能对浏览器中的一些图形操作交给 GPU 来完成,因为 GPU 是专门为处理图形而设计,所以它在速度和能耗上更有效率。

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

         如何使用

         常见的触发硬件加速的css属性:

        transform

        opacity

        filters

        Will-change 

                css3硬件加速的坑

        如果你为太多元素使用css3硬件加速,会导致内存占用较大,会有性能问题。

          在GPU渲染字体会导致抗锯齿无效。这是因为GPU和CPU的算法不同。因此如果你不在动画结束的时候关闭硬件加速,会产生字体模糊。  

    7. 避免使用table布局

    HTML 采用基于流的布局模型,从根渲染对象(即<html>)开始,递归遍历部分或所有的框架层次结构,为每一个需要计算的渲染对象计算几何信息,大多数情况下只要一次遍历就能计算出几何信息。但是也有例外,比如<table>的计算就需要不止一次的遍历。

    参考

    浏览器重绘(repaint)重排(reflow)与优化[浏览器机制](优先)

    你真的了解回流和重绘吗
  • 相关阅读:
    第一篇博客
    margin 与 padding
    CSS伪类
    CSS定位
    利用css布局在图片插入文字
    CSS选择符
    CSS伪类
    CSS语法顺序
    CSS样式特点及优先级
    frame-框架
  • 原文地址:https://www.cnblogs.com/kunmomo/p/13674572.html
Copyright © 2011-2022 走看看