重排与重绘

重排与重绘

前言

此处需要首先说明一下网页从HTML文件变成屏幕上的画面所经历的过程:

  1. HTML内容被HTML解析器解析生成DOM树
  2. CSS内容被CSS解析器解析生产CSSOM树
  3. DOM树+CSSOM树会生产Render Tree(渲染树)
  4. 生成布局,浏览器根据渲染树来布局,以计算每个节点的几何信息
  5. 将各个节点绘制到屏幕上

其中第4步为布局排列(flow),第5步为绘制(paint),这两部加起来也就是我们通常所说的渲染

本次主题所说的重排与重绘便是指网页重新布局排列和重新绘制,英文概念分别为:Reflow , Repaint,也有些文章中称之为回流与重绘

目的

重绘与重排与我们的前端性能有什么关系呢?

在Web前端页面的生命周期中,在网页生成时,浏览器会至少渲染一次页面,并且,在用户访问页面过程中,还可能会不断触发重排和重绘,不管页面发生了重绘还是重排,都会影响到网页的性能,尤其是其中的重排,会使我们付出高额的性能代价,因此我们应尽量避免。

那么哪些操作会引发重排与重绘呢?

  • 重绘:元素的外观被改变,例如:元素的背景颜色发生变化

  • 重排:重新生成布局,重新排列元素,例如:元素的尺寸、位置发生变化

重排(Reflow):

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

重排也叫回流,简单的说就是重新生成布局,重新排列元素。

以下情况会引发重排:

  • 页面初始渲染(无法避免)
  • 添加或删除可见的DOM元素
  • 元素位置的改变,或者使用动画
  • 改变元素尺寸,比如边距、填充、边框、宽度和高度等
  • 填充内容的改变,比如文本的改变或图片大小改变而引起的计算值宽度和高度的改变
  • 浏览器窗口尺寸的变化(resize事件发生时)
  • 设置 style 属性的值,因为通过设置style属性改变结点样式的话,每一次设置都会触发一次reflow
  • 读取某些元素属性:offsetLeft/Top/Height/Width, clientTop/Left/Width/Height, scrollTop/Left/Width/Height, width/height, getComputedStyle(), currentStyle(IE)

重排影响的范围:

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

  • 全局范围:从根节点html开始对整个渲染树进行重新布局
  • 局部范围:对渲染树的某部分或某一个渲染对象进行重新布局

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

重绘(Repaints):

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

重排必定会引发重绘,但重绘不一定会引发重排。

代价

重排的代价是高昂的,会破坏用户体验,并且让UI展示非常迟缓,重排重绘非常耗费资源,是导致网页性能低下的根本原因。

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

// div元素有两个样式变动,但是浏览器只会触发一次重排和重绘

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

优化

我们应尽量减少重排,最简单的方式是:1.减少重排次数;2.缩小重排范围

  1. 减少重排范围

    • 应该尽量以局部布局的形式组织HTML结构,使各个结构间相互独立,当某个结构发生重排时,不会影响到页面上的其它结构
    • 应该尽可能在底层级的元素上设置样式,削弱修改样式时,对页面其它元素带来影响
    • 不要使用table布局,可能很小的一个小改动会造成整个table的重新布局
  2. 减少重排次数

    • 不要频繁的操作样式,对于一个静态页面来说,明智且可维护的做法是更改类名而不是修改样式,对于动态改变的样式来说,相较每次微小修改都直接触及元素,更好的办法是统一在 cssText 变量中编辑。

      // bad
      var left = 10;
      var top = 10;
      el.style.left = left + "px";
      el.style.top = top + "px";
      
      // good 
      el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
      
    • 离线操作元素:

      • 使用display:none

        一旦我们给元素设置 display:none 时(只有一次重排重绘),元素便不会再存在在渲染树中,相当于将其从页面上“拿掉”,我们之后的操作将不会触发重排和重绘,添加足够多的变更后,通过 display属性显示(另一次重排重绘)。通过这种方式即使大量变更也只触发两次重排。另外,visibility : hidden 的元素只对重绘有影响,不影响重排

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

      • 复制节点,在副本上工作,然后替换它

        dom.display = 'none'
        // 修改dom样式
        dom.display = 'block'
        
        
        var fragment = document.createDocumentFragment()
        // 操作dom
        document.body.appendChild(fragment);
        
        var newUL = oUl.cloneNode(true);
        // 操作dom
        parentElement.replaceChild(newUl, oUl);
        
  • 使用 absolute 或 fixed 脱离文档流:使用 absolutefixed 脱离文档流使用绝对定位会使的该元素单独成为渲染树中 body 的一个子元素,重排开销比较小,不会对其它节点造成太多影响

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

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

观察

我们可以通过Chrome观察重排与重绘

打开开发者工具 => 点击Performance => 点击左侧小圆点 => 点击刷新页面 => 录制=>查看数据

  • Loading: 网络通信和HTML解析
  • Scripting: JavaScript执行
  • Rendering: 样式计算和布局,即重排
  • Painting: 重绘

你可能感兴趣的:(重排与重绘)