浏览器回流与重绘

页面渲染

渲染流程

浏览器使用流式布局模型 (Flow Based Layout),浏览器从拿到代码到渲染页面,主要流程

  • 解析 HTML 节点,创建 DOM 树
  • 解析 CSS 创建 CSSOM 树
  • 合并 CSSOM 与 DOM 为 渲染树(renderTree)
    • 由 DOM 树 Root 节点开始遍历获取可视节点
    • 对于 DOM 的可视节点,获取其对应 CSSOM 树规则
    • 根据两棵树上获取的节点与样式,合成 renderTree
  • Layout,计算出 renderTree 每个节点的大小和位置
  • Paint, 绘制 renderTree 到页面上

合并的 renderTree 的过程只获取可视节点,将会忽略不可视节点 (meta, script, link 等节点及 display:none 的节点)。

浏览器回流与重绘20230310142853

渲染原理

浏览器将渲染树上的节点通过渲染类来构建成一个个渲染对象。
每个渲染对象将在页面上绘制其对应的矩形区域,根据 display 的不同,有不同的渲染类。

  • RenderInline
  • RenderBlock
  • RenderListItem

所有渲染类有一个基类 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 即为绘制。
在渲染页面过程中,由根节点开始,递归遍历渲染树(renderTree)上节点,执行每个节点对应渲染对象的 layout 与 paint 方法。

Layout

布局阶段,包括初始的排列 Layout 以及后续出现的回流 Reflow。
为了避免细微更改都进行整体重新布局,浏览器将布局分为全量布局增量布局

全量布局,渲染流程会在整个页面重新来过,非常耗费性能,一般只有视窗变化或影响全局的样式变更会触发全量布局。

增量布局,在渲染树上,将涉及到变更的节点对应渲染对象添加标记位’dirty’,后续布局时只在脏对象进行即可。

布局阶段主要(重新)计算节点的在屏幕的位置与几何形状,可以看做给节点画框架。
通常情况下,当前节点重新布局,会同时触发其关联节点(祖先、后代节点以及兄弟节点)重新布局。
布局本质都会造成页面重新渲染。

Paint

绘制阶段,主要绘制节点的样式,为节点填充具体的像素点,可以看做给节点加个皮肤。
包括初始的绘制 Paint 与后续的重绘 Repaint。
与布局一致,绘制阶段也有全量与增量之分。

回流与重绘触发

回流触发

  • 窗口调整
  • 字体变更
  • 增删样式
    • 操作节点 style
    • 操作节点 class
  • 内容变更
    • 用户输入(input 等表单)
    • 增删 dom 节点
    • 节点尺寸变化
  • 伪类激活
  • 节点动画
  • 读写 offsetWidth

基本上回流都会触发重绘,在DEMO中修改元素宽度,

在Performance中查看,

浏览器回流与重绘20230310155516

可以看到,改变元素宽度,浏览器先回流(Layout)然后进行重绘(Paint)。

重绘触发

如下仅讨论只触发重绘的情况

  • visibility 变化
  • backgroud 背景颜色变化
  • color 字体颜色
  • opacity 调整

这些属性的变化一般只简单影响元素的外观、风格。
不影响节点的位置、形状等会几何属性的改变时,不会触发回流。

DEMO中修改背景色,

在Performance中查看,

浏览器回流与重绘20230310155024

可以看到,只改变背景,浏览器仅重绘(Paint)没有回流。

回流与重绘优化

浏览器优化

  • 队列机制

    在现代浏览器中,由于回流重绘会有较大的性能损耗,多数浏览器会将回流重绘放入队列。
    当一定时间或操作到指定阀值,浏览器在将队列中的一次性进行处理,减少回流重绘次数。

    在上文回流触发中,读取 offsetWidth 等的值也会触发回流重绘,其根本原因就是由于这个优化。
    当代码在读取这些值时,为了获取准确的信息,浏览器不得不将队列清空强制执行回流重绘来得到这些值得最新信息。
    涉及清空回流重绘队列的相关属性和方一般有两类。

    获取盒模型参数:

elem.offsetLeft, elem.offsetTop, elem.offsetWidth, elem.offsetHeight, elem.offsetParent
elem.clientLeft, elem.clientTop, elem.clientWidth, elem.clientHeight
elem.getClientRects(), elem.getBoundingClientRect()

滚动相关属性:

elem.scrollBy(), elem.scrollTo()
elem.scrollIntoView(), elem.scrollIntoViewIfNeeded()
elem.scrollWidth, elem.scrollHeight
elem.scrollLeft, elem.scrollTop
  • 布局区分

如上文,将布局拆分为全量布局与增量布局,避免整棵树回流重绘。

  • CSS3 硬件加速(GPU)

    部分 CSS3 属性的通过设定会触发 GPU 加速,从而不会产生回流重绘。
    常见的属性有 transform、opacity、filters 等。
    在较新的浏览器中使用 will-change 来启用硬件加速。
    will-change 适用于上述属性,使用方式

.skyline {
will-change: transform;
}

在不支持 will-change 的浏览器中,使用 translateZ ,但该属性仅对 transform 生效

.skyline {
transform: translateZ(0);
}

不要过分使用硬件加速,会有额外的性能消耗与内存占用。
同时,对于 z-index 指定不同层级,低层使用硬件加速,高层也会被强制硬件加速。

代码优化

在实际业务中,页面变化不可避免。
回流与重绘优化的主要思路与浏览器优化思路基本一致,减少回流重绘次数。
技术层面上,优化从 HTML、 CSS 与 JS 大方向入手。

  • HTML

    • 精简节点嵌套层级
      避免出现无意义层级
  • CSS

    • 使用浏览器硬件优化相关属性。
    • 将动画应用于文档流外的节点。
      可能的话,将动画添加到 absolute 或 fixed 定位的节点上,脱离文档流的节点合成层不会影响其他合成层。

    • 避免表达式属性。
      类似于 calc 的 css 属性设置方法会在其他节点或回流触发时进行重新计算,从而使得对应节点也触发回流。

  • JS

    • class 变更选用更底层节点
      使用样式变更来改变节点而不是使用类
      变更类属性时,变更的节点尽可能往后而不是在其包裹或祖先节点上。

    • 调整类而不是样式
      对于多条样式调整的节点,集中更改样式或添加修改类而不是分散修改样式

    • 捆绑 DOM 调整
      对于 DOM 节点的调整,涉及到多次调整,可以捆绑最后再进行一次插入或替换 DOM 操作,具体方法有

      • documentFragment
        通过 documentFragment 新建文档流外的子树,将所有变更应用到子树,最后一次插入或替换到 DOM 中

      • display:none
        隐藏节点(display:none)脱离文档流,多次修改该节点,显示节点

    • 集中读写样式代码

    • 缓存引起强制重绘的属性或函数结果
      对于 clientWith 等引起强制重绘的属性或函数,多次使用时,使用变量缓存其值。

调整类而不是样式

// bad
var left = 10,
top = 10
el.style.left = left + 'px'
el.style.top = top + 'px'

// better
el.className += ' theclassname'

// better
el.style.cssText += '; left: ' + left + 'px; top: ' + top + 'px;'

捆绑 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')
//bad
appendDataToElement(ul, data)

// better
ul.style.display = 'none'
appendDataToElement(ul, data)
ul.style.display = 'block'

// better
const fragment = document.createDocumentFragment()
appendDataToElement(fragment, data)
ul.appendChild(fragment)

集中读写样式代码

// bad
var rh = document.getElementById('rect').clientHeight
document.getElementById('rect').style.height = rh + 10 + 'px'

var ch = document.getElementById('circle').clientHeight
document.getElementById('circle').style.height = ch + 10 + 'px'

//better
var rh = document.getElementById('rect').clientHeight
var ch = document.getElementById('circle').clientHeight

document.getElementById('rect').style.height = rh + 10 + 'px'
document.getElementById('circle').style.height = ch + 10 + 'px'

缓存引起强制重绘的属性或函数结果

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

//better
const width = box.offsetWidth

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

回流基本都会会引起重绘,重绘不一定回流。
一般来说,引起位置变化的都会回流并重绘,不引起位置变化的重绘即可。
不管是浏览器底层还是业务代码层面,优化回流和重绘的基本思路都是减少回流重绘次数

像素管道

像素管道组成

浏览器绘制每一帧画面的流程像是一个流水线管道,这个管道由五部分组成,
被称为像素管道。

浏览器回流与重绘20220715181720

图源

由上文可知,这个管道上的Layout不是必须的。

  • Javascript

Javascript部分一般包含Javascript或CSS对页面样式的变更。
包括显示调整、动画加入、渐变以及部分 Web Animation API 来实现

  • Style

这部分为样式计算,将DOM与对应CSS样式结合。

  • Layout

布局阶段,包括初始的排列 Layout 以及后续出现的回流 Reflow。

  • Paint

绘制阶段,主要绘制节点的样式,为节点填充具体的像素点,可以看做给节点加个皮肤。

  • Composite

浏览器也有图层的概念,最简单的体现就是z-index,合成将绘制的图层正确合成并展示到屏幕。

BMW WARNING

  • Bulletin

本文首发于 skyline.show 欢迎访问,
文章实时更新,如果有什么错误或不严谨之处望请指出,十分感谢。
如果你觉得有用,欢迎到Github 仓库点亮 ⭐️。

I am a bucolic migant worker but I never walk backwards.

  • Material

参考资料如下列出,部分引用可能遗漏或不可考,侵删。

https://dev.to/gopal1996/understanding-reflow-and-repaint-in-the-browser-1jbg

What forces layout / reflow

你真的了解回流和重绘吗

浏览器渲染之回流重绘

  • Warrant

本文作者: Skyline(lty)

文章链接:http://www.skyline.show/浏览器回流与重绘.html

授权声明: 本博客所有文章除特别声明外, 均采用 CC BY - NC - SA 3.0 协议。 转载请注明出处!

Copyright © 2017 - 2024 鹧鸪天 All Rights Reserved.

skyline 保留所有权利