重绘和回流
概述
当 render tree 中的一部分(或全部)因为元素的尺寸、布局、显示/隐藏等改变而需要重新构建,这个过程称作回流(reflow)。页面第一次加载的时候,至少发生一次回流
当 render tree 中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如 background-color,这个过程叫做重绘(repaint)
在回流的时候,浏览器会使 render tree 中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘。因此回流必将引起重绘,而重绘不一定会引起回流。
Reflow 的成本比 Repaint 高得多的多。DOM Tree 里的每个结点都会有 reflow 方法,一个结点的 reflow 很有可能导致子结点,甚至父点以及同级结点的 reflow。
重绘的主体是节点
回流的主体是DOM 树,既可以是子树也可以是根树,根树的回流又称为页面回流
为什么说 DOM 操作很慢
就是因为会频繁的触发回流和重绘。DOM 本身只是 JS 对象,修改 DOM 树的部分是很快的
在 DOM 查询时,querySelector
和 querySelectorAll
应该是最后的选择,它们功能最强大,但执行效率很差,如果可以的话,尽量用其他方法替代。
在 Chrome 中查看 Repaint
F12 打开控制台 -> DevTools -> more tools -> Rendering -> 勾选 Paint flashing,layer borders
- Paint Flashing 高亮显示网页中需要被重绘的部分。
- Layer Borders 显示 Layer 边界。
- FPS Meter 每一秒的帧细节,帧速率的分布信息和 GPU 的内存使用情况。
- Scrolling Performance Issues 分析鼠标滚动时的性能问题,会显示使屏幕滚动变慢的区域。
- Emulate CSS Media 仿真 CSS 媒体类型,查看不同的设备上 CSS 样式效果,可能的媒体类型选项有 print、screen
查看 tiemline,通过 performance 录制查看
重绘何时发生
当一个元素的外观的可见性 visibility 发生改变的时候,但是不影响布局。 比如:
- outline
- visibility
- background color
- transform
回流何时发生
Document
页面渲染初始化
调整窗口大小
整个页面回流
Css
- 改变字体,比如修改网页默认字体。
影响至少一级的父级元素,默认继承的子元素
- 激活 CSS 伪类,比如 :hover
并没有直接导致回流,是否回流取决于改变的属性
- 操作 class 属性
- 设置 style 属性的值
- 增加或者移除样式表
- 元素尺寸或位置发生改变
如果是独占一行的块级元素,改变宽度不会发生回流
绝对定位的元素,改变 top\left\right\bottom 不会回流,仅重绘
类似的 transform 也是这样, 仅重绘
- 内容变化,比如文本改变或者图片大小改变而引起的计算值宽度和高度改变
影响到父级
Dom
脚本操作 DOM,增加删除或者修改 DOM 节点,元素尺寸改变——边距、填充、边框、宽度和高度。
一些常用且会导致回流的属性和方法:
clientWidth
、clientHeight
、clientTop
、clientLeft
offsetWidth
、offsetHeight
、offsetTop
、offsetLeft
scrollWidth
、scrollHeight
、scrollTop
、scrollLeft
scrollIntoView()
、scrollIntoViewIfNeeded()
getComputedStyle()
getBoundingClientRect()
scrollTo()
var s = document.body.style
s.padding = "2px" // 回流+重绘
s.border = "1px solid red" // 回流+重绘
s.color = "blue" // 重绘
s.backgroundColor = "#ccc" // 重绘
s.fontSize = "14px" // 再一次 回流+重绘
document.body.appendChild(document.createTextNode('abc!')) // 回流+重绘
浏览器的队列优化
如果向上述代码中那样,浏览器不停地回流 + 重绘,很可能性能开销非常大,实际上浏览器会优化这些操作,将所有引起回流和重绘的操作放入一个队列中,等待队列达到一定的数量或者时间间隔,就 flush 这个队列,一次性处理所有的回流和重绘。
虽然有浏览器优化,但是当我们向浏览器请求一些 style 信息的时候,浏览器为了确保我们能拿到精确的值,就会提前 flush 队列。
包括:
width
、height
clientWidth
、clientHeight
、clientTop
、clientLeft
offsetWidth
、offsetHeight
、offsetTop
、offsetLeft
scrollWidth
、scrollHeight
、scrollTop
、scrollLeft
scrollIntoView()
、scrollIntoViewIfNeeded()
getComputedStyle()
getBoundingClientRect()
scrollTo()
减少回流重绘
Css
为 HTML 标签使用 fixed 或 aboslute 的 position,那么修改他们的 CSS 是不会 reflow 的
translateXY 会在自己的图层内重绘,而 left top 会导致整个页面重绘,性能差距,在移动端一般使用 translateXY 实现轮播效果(无缝划屏)
requestAnimationFrame:能保证浏览器在正确的时间进行渲染。
Js
保持 DOM 操作“原子性”:
// bad
var newWidth = ele.offsetWidth + 10
ele.style.width = newWidth + 'px'
var newHeight = ele.offsetHeight + 10
ele.style.height = newHeight + 'px'
// good 读写分离,批量操作
var newWidth = ele.offsetWidth + 10 // read
var newHeight = ele.offsetHeight + 10 // read
ele.style.width = newWidth + 'px' // write
ele.style.height = newHeight + 'px' // write
使用 classList 代替 className:
className 只要赋值,就一定出现一次 rendering 计算;classList 的 add 和 remove,浏览器会进行样式名是否存在的判断,以减少重复的 rendering。
ele.className += 'something'
ele.classList.add('something')
ele.classList.remove('something')
批量操作借助临时变量
// bad
for (let i = 0; i < 10; i++) {
el.style.left = el.offsetLeft + 5 + 'px'
el.style.top = el.offsetTop + 5 + 'px'
}
// good
let left = el.offsetLeft
let top = el.offsetTop
for (let i = 0; i < 10; i++) {
left += 5
top += 5
}
el.style.left = left + 'px'
el.style.top = left + 'px'
JS 离线操作
对元素进行“离线操作”,完成后再一起更新:
- 使用 DocumentFragment 进行缓存操作,引发一次回流和重绘 了解DocumentFragment 给我们带来的性能优化
- 元素操作前使用
display: none
,完成后再将其显示出来,这样只会触发一次回流和重绘。 - 使用 cloneNode + replaceChild 技术,引发一次回流和重绘。
示例
假如需要在下面的 html 中添加两个 li 节点:
<ul id="">
</ul>
let ul = document.getElementByTagName('ul')
let man = document.createElement('li')
man.innerHTML = 'man'
ul.appendChild(li)
let woman = document.createElement('li')
woman.innerHTML = 'woman'
ul.appendChild(woman)
上述代码会发生两次回流,假如使用 display: none
的方案,虽然能够减少回流次数,但是会发生一次闪烁,这时候使用 DocumentFragment 的优势就体现出来了。
DocumentFragment 有两大特点:
- DocumentFragment 节点不属于文档树,继承的 parentNode 属性总是 null。
- 当请求把一个 DocumentFragment 节点插入文档树时,插入的不是 DocumentFragment 自身,而是它的所有子孙节点。这使得 DocumentFragment 成了有用的占位符,暂时存放那些一次插入文档的节点。它还有利于实现文档的剪切、复制和粘贴操作。
let fragment = document.createDocumentFragment()
let man = document.createElement('li')
let woman = document.createElement('li')
man.innerHTML = 'man'
woman.innerHTML = 'woman'
fragment.appendChild(man)
fragment.appendChild(woman)
document.body.appendChild(spanNode)
可见 DocumentFragment 是一个孤儿节点,没爹就能出生,但是在需要它的时候,它又无私地把孩子奉献给文档树,然后自己默默离开。是不是有点像《银翼杀手 2049》?
CSS 硬件加速
参考:https://juejin.im/post/5b6143996fb9a04fd343ae28#heading-4
CSS3 硬件加速又叫做 GPU 加速,是利用 GPU 进行渲染,减少 CPU 操作的一种优化方案。由于 GPU 中的 transform 等 CSS 属性不会触发 repaint,所以能大大提高网页的性能。
动画与帧
之前学习 flash 的时候,就知道动画是由一帧一帧的图片组成,在浏览器中也是如此。我们首先看一下,浏览器每一帧都做了什么。
- JavaScript:JavaScript 实现动画效果,DOM 元素操作等。
- Style(计算样式):确定每个 DOM 元素应该应用什么 CSS 规则。
- Layout(布局):计算每个 DOM 元素在最终屏幕上显示的大小和位置。由于 web 页面的元素布局是相对的,所以其中任意一个元素的位置发生变化,都会联动的引起其他元素发生变化,这个过程叫 reflow。
- Paint(绘制):在多个层上绘制 DOM 元素的的文字、颜色、图像、边框和阴影等。
- Composite(渲染层合并):按照合理的顺序合并图层然后显示到屏幕上。
动画与图层
浏览器在获取 render tree 后,渲染树中包含了大量的渲染元素,每一个渲染元素会被分到一个图层中,每个图层又会被加载到 GPU 形成渲染纹理。GPU 中 transform 是不会触发 repaint 的,这一点非常类似 3D 绘图功能,最终这些使用 transform 的图层都会由独立的合成器进程进行处理。
过程如下:render tree -> 渲染元素 -> 图层 -> GPU 渲染 -> 浏览器复合图层 -> 生成最终的屏幕图像。
注意: chrome devtools 中可以开启 Rendering 中的 Layer borders 查看图层纹理。
其中黄色边框表示该元素有 3d 变换,表示放到一个新的复合层(composited layer)中渲染,蓝色栅格表示正常的 render layer。
在 GPU 渲染的过程中,一些元素会因为符合了某些规则,而被提升为独立的层(黄色边框部分),一旦独立出来,就不会影响其它 DOM 的布局,所以我们可以利用这些规则,将经常变换的 DOM 主动提升到独立的层,那么在浏览器的一帧运行中,就可以减少 Layout 和 Paint 的时间了
合成渲染
合成渲染,听着可能有些陌生,但是你肯定用过。对于 transform/opacity 这两种变换,浏览器不会用 repaint/reflow 处理,而是在已经渲染好的元素基础上进行附加工作。例如一个黑底色的 div,往右飞 100px, 传统 JS 过程是对每次修改 left 值后重新画一个 div。而如果我们用 transform:translate(0,100px) ,transition:2s 浏览器则是把这个绘制好的 div 单独放在一个画面层再平移这个层过去,div 的几何形状,颜色不会再重复计算,而是保留在这个图层中。Google 开发者的一篇文章介绍了合成渲染的好处,其中有图描述了理想动画效果的流程
性能最佳的像素管道版本会避免布局和绘制,只需要合成更改:为了实现此目标,需要坚持更改可以由合成器单独处理的属性。目前只有两个属性符合条件:transforms 和 opacity:
使用 transform
和 opacity
时要注意的是,您更改这些属性所在的元素应处于其自身的合成器层。要做一个层,您必须提升元素,后面我们将介绍方法。
如果没有创建独立的图层,会在动画的过程中先重绘一次,但是依然不会有回流
由于 transform
是位于 Composite Layers
层,而 width
、left
、margin
等则是位于 Layout
层,在 Layout
层发生的改变必定导致 Paint Setup and Paint
-> Composite Layers
,所以相对而言使用 transform
实现的动画效果肯定比 left
这些更加流畅。
而且就算抛开这一角度,在另一方面浏览器也会针对 transform
等开启 GPU 加速。
创建独立图层
Css
- 3D 或者透视变换(perspective,transform) 的 CSS 属性。
如果用的是 2D 转换,并不是 3D 转换。会在动画开始和结束的时候会有两次重绘产生。
3D 转换和 2D 转换的不同在于是否提前生成新的层,如果是 2D 的话是在实行的时候,在动画开始的时候,一个新的层被创建,并且被传入
GPU
处理。当动画结束,独立的层被移除,结果被重新绘制。即使是 3D 转换,也必须在动画前就存在,才会创建独立图层,否则还是在原始图层,等到动画开始再换成新图层
可以 通过 layout border 查看 图层 黄色边框
- 对自己的 opacity 做 CSS 动画或使用一个动画 webkit 变换的元素。
- will-change
- 拥有加速 CSS 过滤器的元素。
- 覆盖在其它元素之上的元素,比如通过
z-index
提升层级 ????
实践中并没有触发
其他
- 使用加速视频解码的 video 元素。
- 拥有 3D(WebGL) 上下文或者加速 2D 上下文的 canvas 元素。
- 混合插件(Flash)。
Dom
- 元素有一个包含复合层的后代节点 (换句话说,就是一个元素拥有一个子元素,该子元素在自己的层里)。
- 元素有一个兄弟元素在复合图层渲染,并且该兄弟元素的 z-index 较小,那这个元素(不管是不是应用了硬件加速样式)也会被应用到复合图层。
浏览器有可能给复合层之后的所有相对或绝对定位的元素都创建一个复合层来渲染,于是就有了上面我厂项目截图的那种效果。
不过也不是所有浏览器都有这个问题,我在 mac 上的 Safari、firefox 都没有明显差异,安卓手机上的 QQ 浏览器好像也正常,猎豹、UC、欧朋、webview 等浏览器差距明显,更多测试就靠大家来发现吧。
关于 z-index 导致的硬件加速的问题,可以查看这篇文章 http://www.th7.cn/web/html-css/201509/121970.shtml
注意
- absolute 布局(fixed 也一样),虽然可以脱离普通文档流,但它仍然属于默认复合层
开启 GPU 加速
如果有一些元素不需要用到上述属性,但是需要触发硬件加速效果,可以使用一些小技巧来诱导浏览器开启硬件加速。
.element {
-webkit-transform: translateZ(0);
-moz-transform: translateZ(0);
-ms-transform: translateZ(0);
-o-transform: translateZ(0);
transform: translateZ(0);
/**或者**/
transform: rotateZ(360deg);
transform: translate3d(0, 0, 0);
}
.example2 {
transform: rotateZ(360deg);
}
注意:我在不同的资料中查到的 transform 是否能触发硬件加速的结果不同,自己测试后,发现结果是可以。
因为 opacity 如果不是动画无法新建图层,而且 filter 属性支持度太差,稳健的开启方法只有一个
有必要使用 transform hack
的地方是提高性能。浏览器自身也提供了优化的功能,这也就是 will-change
属性。这个功能允许你告诉浏览器这个属性会发生变化,因此浏览器会在开始之前对其进行优化。这里有一个例子:
.example {
will-change: transform;
}
属性值必须要与发生动画的 CSS 属性匹配,否则还是会导致重绘
要注意的问题
Memory
过多地开启硬件加速可能会耗费较多的内存,因此什么时候开启硬件加速,给多少元素开启硬件加速,需要用测试结果说话。
Font Rendering
GPU 渲染会影响字体的抗锯齿效果。这是因为 GPU 和 CPU 具有不同的渲染机制,即使最终硬件加速停止了,文本还是会在动画期间显示得很模糊。
因此如果你不在动画结束的时候关闭硬件加速,会产生字体模糊。
Z-index
使用 3D 硬件加速提升动画性能时,最好给元素增加一个 z-index 属性,人为干扰复合层的排序,可以有效减少 chrome 创建不必要的复合层,提升渲染性能,移动端优化效果尤为明显。