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)与优化[浏览器机制](优先)

    你真的了解回流和重绘吗
  • 相关阅读:
    FJNU 1151 Fat Brother And Geometry(胖哥与几何)
    FJNU 1157 Fat Brother’s ruozhi magic(胖哥的弱智术)
    FJNU 1159 Fat Brother’s new way(胖哥的新姿势)
    HDU 3549 Flow Problem(最大流)
    HDU 1005 Number Sequence(数列)
    Tickets(基础DP)
    免费馅饼(基础DP)
    Super Jumping! Jumping! Jumping!(基础DP)
    Ignatius and the Princess IV(基础DP)
    Keywords Search(AC自动机)
  • 原文地址:https://www.cnblogs.com/kunmomo/p/13674572.html
Copyright © 2011-2022 走看看