Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JS深入浅出 - 浏览器的重绘(Repaint)与回流(Reflow) #2

Open
jtwang7 opened this issue May 8, 2021 · 0 comments
Open

JS深入浅出 - 浏览器的重绘(Repaint)与回流(Reflow) #2

jtwang7 opened this issue May 8, 2021 · 0 comments

Comments

@jtwang7
Copy link
Owner

jtwang7 commented May 8, 2021

JS深入浅出 - 浏览器的重绘(Repaint)与回流(Reflow)

参考文章:
介绍下重绘和回流(Repaint & Reflow),以及如何进行优化
前端基本功(四):性能优化之你真的懂回流、重绘与合成层吗?
你真的了解回流和重绘吗
浏览器的回流与重绘 (Reflow & Repaint)

1. 浏览器的渲染机制

渲染过程

  1. 构建渲染树
    1.1 浏览器将 HTML 解析成 DOM,把 CSS 解析成 CSSOM
    1.2 将 DOM 和 CSSOM 合并,生成渲染树(Render Tree)
  2. 获取节点绝对像素
    2.1 Layout(布局):根据渲染树,计算所有节点在页面上的大小和位置
    2.2 Painting(绘制):根据渲染树以及回流得到的节点几何信息,结合节点样式,得到节点的绝对像素
  3. Display:将像素发送给GPU,展示在页面上。(该过程又包括了 CSS3 硬件加速创建合成层以及 GPU 合并合成层等)

DOM Tree 与 Render Tree 区别在于:
DOM 树中的各节点对应 HTML 代码中的每个 tag 标签,其根节点是 document 对象。DOM 树里包含了所有 HTML 标签,包括display:none ,还有用JS动态添加的元素等。
Render Tree 能识别样式,Render Tree 中每个 NODE 都有自己的 style,而且 Render Tree 不包含隐藏节点和其他不进行渲染的节点 (比如 display:none的节点,还有 head 节点),因为这些节点不会用于呈现,而且不会影响呈现的,所以就不会包含到 Render Tree 中。

生成渲染树

  1. 解析得到 DOM 树和 CSSOM 树
  2. 从DOM树的根节点开始遍历每个可见节点
  3. 对于每个可见节点,找到 CSSOM 树中对应的规则,并应用它们
  4. 根据每个可见节点以及其对应的样式,组合生成渲染树

注意渲染树中只包含可见节点
不可见节点有:

  • 一些不会渲染输出的节点,比如 script、meta、link 等
  • 一些通过 css 进行隐藏的节点,比如 display:none 等。注意:visibility 和 opacity 隐藏的节点仍会存在于渲染树上,因为它们不显示样式但是仍占据布局空间

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

回流

概念

Render Tree 中部分或全部节点的几何属性发生变化(尺寸、布局、位置、隐藏等),影响到页面排版布局时,触发浏览器重新计算所有受影响节点几何信息并重新排版页面布局的过程。(对应渲染过程流程图的 Layout)

回流在页面布局发生变化时就会触发,由于为了弄清每个节点对象在页面上的确切大小和位置,浏览器会从渲染树的根节点重新开始遍历,调整发生变化的节点位置和其他受影响节点的布局。

触发回流的操作(可大致分为两类)

布局变化触发的回流操作

  • 页面首次渲染
  • 浏览器窗口大小发生改变
  • 元素尺寸或位置发生改变
  • 元素内容变化(文字数量或图片大小等等)
  • 元素字体大小变化
  • 添加或者删除可见的DOM元素
  • 激活CSS伪类(例如::hover)

可能导致页面布局发生变化的操作都会触发回流。

"查询 / 调用节点几何属性"所触发的回流操作

  • clientWidth、clientHeight、clientTop、clientLeft
  • offsetWidth、offsetHeight、offsetTop、offsetLeft
  • scrollWidth、scrollHeight、scrollTop、scrollLeft
  • scrollIntoView()、scrollIntoViewIfNeeded()
  • getComputedStyle()
  • getBoundingClientRect()
  • scrollTo()

浏览器为了返回当前最精确的节点几何信息,会立即执行回流操作,确保返回的几何信息是最新的。

重绘

当页面可见节点样式(此处样式不包括几何属性)发生变化且不影响页面排版布局时,触发浏览器对改变节点重新绘制的过程。(对应渲染过程流程图的 Painting + Display)

回流与重绘的关系

回流一定触发重绘,而重绘触发时不一定触发回流。
个人理解:
回流本质上是对受影响的(局部/全局)页面布局内所有节点重新计算几何信息的过程,而重绘本质上是浏览器对渲染树重新绘制的过程。
由于回流后必须将重新排版的页面布局展示到浏览器上,因此必然会经过重绘操作,而重绘操作则不会经过回流操作,从渲染过程流程图中我们也可以发现回流和重绘的触发顺序。

回流/重绘代价

浏览器进行“回流/重绘”是有代价的,但是我们又无法避免浏览器的回流和重绘(毕竟人机交互是必需的)。因此我们需要尽量指定合理的方案去触发回流和重绘。

  • 回流比重绘的代价要更高,回流的花销跟 render tree 有多少节点需要重新构建有关系,不恰当的操作可能会导致整个 render tree 回流。而重绘只需要修改样式改变的节点,不用遍历渲染树,不用计算节点几何信息,因此开销要小于回流。

回流/重绘优化

浏览器的优化机制

由于每次回流/重绘都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重排过程。

  • 浏览器会维护1个队列,把所有会引起回流、重绘的操作放入这个队列。
  • 等队列中的操作到了一定的数量或者到了一定的时间间隔(取决于浏览器的重绘频率,一般近似于显示器的刷新频率60Hz),浏览器就会 flush 清空队列,进行一个批处理。

上述优化机制能够让多次的回流、重绘变成一次回流重绘。

但是,某些操作会强制浏览器立即清空回调队列中的回流/重绘操作:

  • clientWidth、clientHeight、clientTop、clientLeft
  • offsetWidth、offsetHeight、offsetTop、offsetLeft
  • scrollWidth、scrollHeight、scrollTop、scrollLeft
  • scrollIntoView()、scrollIntoViewIfNeeded()
  • getComputedStyle()
  • getBoundingClientRect()
  • scrollTo()

浏览器为了给你最精确的值,需要flush队列,因为队列中可能会有影响到这些值的操作。即使你获取元素的布局和样式信息跟最近发生或改变的布局信息无关,浏览器都会强行刷新渲染队列。引擎会重新渲染来确保获取的值是实时的。

减少回流和重绘

CSS

  • 避免使用table布局。(遍历 Render Tree 时可能会多次计算 table 及内部元素)
  • 尽可能在DOM树的最末端改变class。(减少回流影响的范围)
  • 避免设置多层内联样式。(CSS 选择符从右往左匹配查找,我们应尽量减少查找的递归过程,保证层级扁平)
  • 将动画效果应用到position属性为absolute或fixed的元素上。(避免了动画效果对整体页面布局产生的影响)
  • 避免使用CSS表达式(例如:calc(),会强制清空回调队列引发回流)。

JavaScript

  • 避免频繁操作样式,最好一次性重写 style 属性(通过 elem.style.cssText 属性),或者将样式列表定义为 class 并一次性更改class属性。
  • 避免频繁操作DOM,创建一个 documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。
  • 也可以先为元素设置 display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。
  • 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
  • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

总结:减少对 render tree 的操作(合并多次多DOM和样式的修改),并减少对一些 style 信息的请求,尽量利用好浏览器的优化策略。

合成层 Composite 性能优化

正常情况下浏览器渲染流程需要经过回流和重绘(对应 Layout 和 Paint),
将节点提升至合成层,有以下几个优点:

  • 合成层的位图,会交由 GPU 合成,比 CPU 处理要快
  • 当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层(因为合成层与其他层处于平行关系)
  • 对于 transform 和 opacity 效果,不会触发 layout 和 paint

提升合成层的方式

使用 CSS 的 will-change 属性:will-change 设置为 opacity、transform、top、left、bottom、right 可以将元素提升为合成层。

CSS3 硬件加速:利用 transform 和 opacity 实现动画效果

从性能方面考虑,最理想的渲染流水线是没有布局和绘制环节的,只需要做合成层的合并即可:
浏览器一般的渲染流程(简化版):

理想渲染流程:

为了实现上述效果,就需要只使用那些仅触发 Composite 的属性。目前,只有两个属性是满足这个条件的:transforms 和 opacity。

!不要盲目提升合成层

每创建一个新的渲染层,就意味着新的内存分配和更复杂的层的管理,我们通常将包含动画效果的元素(或会频繁触发回流的元素)单独提升为合成层,这样就可以减少对其它元素的影响。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant