目前,比较常见的浏览器内核(渲染引擎)有: WebKit 、 Blink 、 Gecko 、 Trident 、 EdgeHTML ,更多 请看 。
以下是两个主流浏览器内核 WebKit、Gecko 合成 DOM 的过程:
两者整体流程基本相似,在术语方面也有所不同,比如 WebKit 中的 Layout 过程,Gecko 称为 Reflow 。
比如,React Hooks 中处理副作用时,某些场景下应选择 useLayoutEffect ,而不是 useEffect 的原因正是为了减少回流重绘的过程。
「回流」也有称作「重排」的。
在很长一段时间里,显示器的刷新率多数为 60Hz,即使到现在仍然是占多数。
刷新率(RefreshRate),表示单位时间内能够绘制新图像的次数。举个例子,60Hz 的刷新率,表示显示器要在一秒内刷新 60 次图像,换句话说,一次图像的更新要在 16.67ms 内完成。这样才不会造成卡顿。如果超出这个时间,在视角上就会产生卡顿感。
附一个来自网上的图:
还是用 React 举例吧。我们知道 React 16 起采用了全新的 Fiber 架构,就是为了解决大型应用卡顿的问题。
我们知道,浏览器是多进程的,JavaScript 是单线程的。浏览器会为每个标签(Tab)分配一个进程,每个进程由 GUI 渲染线程、JS 线程、定时器线程、网络线程、事件线程多个线程组成。最重要的一点是: GUI 渲染线程与 JS 线程是互斥的 。换句话说,某个时刻它只能执行其中一个线程,等待该线程执行完毕之后,才将执行权交由另一个线程。
那么为什么 React 15 在处理大型应用的时候会卡顿呢?
首先 React 15 架构,由 协调器 (Reconciler)和 渲染器 (Render)组成。而 React 16 在原来的基础上新增了 调度器 (Scheduler),用于调度任务的优先级。
在 React 15 的 Reconciler 中,组件的挂载(Mounting)、更新(Updating)会对应调用 mountComponent 、 updateComponent 方法,它们内部会执行递归操作,以更新子组件。 但是递归执行的缺点是「无法中断」。 假设 JS 线程执行递归耗时超过了 16.67ms ,由于互斥期间 GUI 线程并不能执行任何的操作,等递归完并生成新的虚拟 DOM 之后,触发 DOM 等更新,此时由 GUI 线程进行处理,可能包括回流、重绘的过程,然后才完成一次页面的更新。由于无法满足刷新率的要求,就会产生卡顿感。
在 React 16 新增的 Scheduler 可以使得浏览器有剩余时间的时候通知 React,而且提供了多种调度优先级,使得更高优先级的任务优先进入 Reconciler 阶段。 而 Reconciler 则是利用了 Fiber 这种架构实现了 「可中断的异步更新」 (请注意,这里的异步并不是由 setTimeout 实现的,由于精度问题, setTimeout 实际上最低延迟时间是 4ms ,在这寸土寸金的一帧时间才 16.67ms ,显然是不合理的)。
因此,React 就是采用了这种思路来解决大型应用卡顿问题的。
题外话扯完了,回到本文的主题...
再附上一张源自网上的图:
这几个关键点:
哪些 CSS 属性影响 Layout,哪些影响 Paint 呢?
可以看这个网站 CSS Triggers 。
其中 Change from default 表示从未设置(即 CSS 默认值)到设置为其他值, Subsequent updates 表示属性修改。
解析 HTML 标签, 构建 DOM 树解析 CSS, 构建 CSSOM 树
把 DOM 和 CSSOM 组合成渲染树 (render tree)
在渲染树的基础上进行布局, 计算每个节点的几何结构
把每个节点绘制到屏幕上 (painting)
Reflow:重新计算元素的几何尺寸、位置 + repaint
Repaint:绘制界面发生变化的部分
(回流会引起重绘,所以代价更大)
添加、删除、更新DOM节点(reflow、repaint)
修改元素的magin、padding、border(reflow、repaint)
display: none(reflow、repaint)
visibility: hidden(repaint)
修改颜色、背景色 (repaint)
尽量一次性修改样式
DOM离线后修改
给动画使用绝对定位可以减小 reflow ?
javascript - What's the difference between reflow and repaint? - Stack Overflow
js优化中,离线操作dom中的“离线”怎么理解? - SegmentFault 思否
1.1 重绘
重绘是指页面中某些元素发生了不影响布局的变化时(如颜色改变),浏览器重新绘制的过程。此时由于只需要UI层面的重新像素绘制,因此损耗较少。 仅仅 引发重绘的操作如下所示(注意:回流必定触发重绘,但是重绘不一定触发回流):
1)改变背景色;
2)改变文字颜色;
3)改变边框颜色;
通过visibility:hidden隐藏元素;
……
1.2 回流
回流是指页面中某些元素发生变化而影响了布局时(如尺寸、位置改变),浏览器需要重新布局并绘制的过程。引发回流的操作如下所示:
1)页面初次渲染;
2)浏览器窗口大小改变;
3)元素尺寸、位置、内容发生改变;
4)元素字体大小变化;
5)添加或者删除可见的 dom 元素;
5)激活 CSS 伪类(例如::hover);
6)查询某些属性或调用某些方法:
clientWidth、clientHeight、clientTop、clientLeft
offsetWidth、offsetHeight、offsetTop、offsetLeft
scrollWidth、scrollHeight、scrollTop、scrollLeft
getComputedStyle() :Window.getComputedStyle()方法返回一个对象,该对象在应用活动样式表并解析这些值可能包含的任何基本计算后报告元素的所有CSS属性的值。
getBoundingClientRect()
scrollTo():scrollTo() 方法可把内容滚动到指定的坐标。
1.3 减少回流和重绘
1.3.1 浏览器自身优化策略
由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重排过程。浏览器会将修改操作放入队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列。当你获取布局信息的操作的时候,会强制队列刷新,比如访问以下属性或者使用以下方法:
offsetTop、offsetLeft、offsetWidth、offsetHeight
scrollTop、scrollLeft、scrollWidth、scrollHeight
clientTop、clientLeft、clientWidth、clientHeight
getComputedStyle()
getBoundingClientRect
以上属性和方法都需要返回最新的布局信息,因此浏览器不得不清空队列,触发回流重绘来返回正确的值。因此,在修改样式的时候,最好避免使用上面列出的属性,它们都会刷新渲染队列。如果要使用它们,最好将值缓存起来。
另一优化就是浏览器认为position为absolute或fixed的元素更改只会影响其本身和子元素,而static的元素变化则会影响之后的所有元素。原因在于absolute和fixed认为元素从文档流中清除了,怎么操作是内部的事。例如:对于复杂动画效果,使用绝对定位让其脱离文档流
1.3.2 多次操作变为一次操作
不要一条一条的修改DOM的样式,尽量使用class进行样式修改。
把DOM离线修改(批量修改DOM)
1)使用documentFragment对象在内存里操作DOM
2)先把DOM给display:none,修改完毕再显示出来
3)clone一个DOM节点到内存里,然后想怎么改就怎么改,改完后,和在线的那个的交换一下。
1.3.3 其它
使用css3硬件加速,可以让transform、opacity、filters(滤镜)这些动画不会引起回流重绘(注意:对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能)
不要把DOM结点的属性值放在一个循环里当成循环里的变量。不然这会导致大量地读写这个结点的属性。
千万不要使用table布局。因为可能很小的一个小改动会造成整个table的重新布局。