代码已经关联到github: 链接地址 文章有更新也会优先在这,觉得不错可以顺手点个star,这里会持续分享自己的开发经验(:
概念介绍
- DOM Tree:浏览器将HTML解析成树形的数据结构。
- CSSOM(CSS Object Model):页面的所有css样式的对象模型。
- Render Tree: DOM Tree和CSSOM合并后生成Render Tree。
- layout(布局render树): 有了Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的CSS定义以及他们的从属关系,从而去计算出每个节点在屏幕中的位置。
- painting(绘制render树): 按照算出来的规则,通过GUI绘制页面像素信息。
- reflow(回流,重排,同layout):,当浏览器发现某个部分发生了点变化影响了布局,需要倒回去重新渲染,内行称这个回退的过程叫 reflow。reflow 会从 这个 root frame 开始递归往下,依次计算所有的结点几何尺寸和位置。脱离文档流的布局会减少整体网站的回流和重绘。
- repaint(重绘,同painting):改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性时,屏幕的一部分要重画,但是元素的几何尺寸没有变。
- 强制同步布局:渲染的流程是先执行JavaScript脚本,然后是样式计算,然后是布局。但是,我们还可以强制浏览器在执行JavaScript脚本之前先执行布局过程,这就是所谓的强制同步布局。(后续有详细介绍)
渲染流程
解析html建立DOM Tree
因为浏览器并不认识html,所以这里将html解析一遍。
字节 → 字符 → 令牌 → 节点 → 对象模型。
解析CSS,生成CSSOM 。
字节 → 字符 → 令牌 → 节点 → 对象模型。
会中断页面渲染
- CSS Parser 解析完 CSS 脚本(
link
和style
标签内的css)后,会生成 CSSStyleSheet(document.styleSheets
可获得当前页面所有的) - 将css属性标准化(如将color转化为rgb)
- 通过继承和层叠规则计算具体样式,生成CSSOM** **css大纲
浏览器 CSS 匹配核心算法的规则是以 从右向左
方式匹配节点的,这样做是为了减少无效匹配次数,从而匹配快、性能更优。
同时为了加快样式的解析,选择器层级少也是一个优势,但是基本不会是项目的性能瓶颈。
QA:加快首屏渲染,由于中断渲染存在,所以减少首屏非必要样式。
将CSSOM结合DOM Tree合并成Render Tree。
- 删除所有不可见的元素。例如
<head>
,<script>
,和<meta>
,和具有hidden
属性的HTML元素。 - 通过 CSSOM 找出当前渲染树中的哪些元素与 CSS 选择器匹配。任何匹配的选择器的 CSS 规则都将应用于渲染树的该节点。注意这里
display:none
是特殊的,也会被删除。
布局Render Tree(layout)
布局从上到下进行的,因为每个元素的定位、宽度和高度都是根据其父节点计算的。
回流也会回到这一步
- 从 DOM 树的根节点开始遍历每个可见节点。
- 某些节点不可见(例如脚本标记、元标记等),因为它们不会体现在渲染输出中,所以会被忽略。
- 某些节点通过 CSS 隐藏,因此在渲染树中也会被忽略,例如,上例中的 span 节点—不会出现在渲染树中,—因为有一个显式规则在该节点上设置了“display: none”属性。
- 对于每个可见节点,为其找到适配的 CSSOM 规则并应用它们。
- 设置可见节点,连同其内容和计算的样式。
分层绘制Render Tree(paint)
重绘也会重新从这里重新去渲染。
布局完后,浏览器会把Render Tree分层(composite,详情可看图层介绍)。
浏览器会把一个图层拆分为一个个小的绘制指令,然后将指令按照顺序排成一个列表。(绘制过程和使用canvas进行绘制图类似。)
接着是绘制,浏览器会将各层划分为图块,这么做的目的是因为视口显示的内容有限,如果直接将整个结构进行绘制开销比较大,所以浏览器会优先将视口内的图块转为位图,这个过程叫栅格化。
最后浏览器会GPU会将各层合成,显示在屏幕上。
回流和重绘
回流
当我们对 DOM 的修改引发了 DOM 几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来。这个过程就是回流(也叫重排)。
引发元素位置变化和获取元素实时属性的操作会引起回流:
- DOM 几何属性改变:
width、height、padding、margin、left、top、border
- DOM节点发生改变:比如删除、插入和显示(display控制)节点
- 获取DOM节点的实时属性:比如
getComputedStyle、getBoundingClientRect和offsetTop等
重绘
当我们对 DOM 的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式(跳过了上图所示的回流环节)。这个过程叫做重绘。
仅在当前位置发生变化的操作会引起重绘:
- DOM外观属性改变:
visibility、color、background-color
QA:如何减少回流和重绘?
避免使用触发重绘和回流的CSS属性,将频繁重绘回流的元素创建为一个独立图层。
- 使用transform实现效果:可以避开回流和重绘,直接进入合成阶段(图块-栅格化-合成-显示)
- 用opacity替代visibility:visibility会触发重绘
- 使用class替代DOM频繁操作样式
- DOM离线后修改,如果有频繁修改,可以先把DOM隐藏,修改完成后再显示
- 不要在循环中读取DOM的几何属性值:如offsetHeight
- 尽量不要使用table布局,小改动会造成整个table重新布局
图层介绍
渲染步骤中就提到了composite
概念,浏览器渲染中的图层又两种:
- 渲染图层,又称默认复合层,是页面普通的文档流。我们虽然可以通过绝对定位,相对定位,浮动定位脱离文档流,但它仍然属于默认复合层,共用同一个绘图上下文对象(GraphicsContext)。
- 复合图层,又称图形层。它会单独分配系统资源,每个复合图层都有一个独立的GraphicsContext。(当然也会脱离普通文档流,这样一来,不管这个复合图层中怎么变化,也不会影响默认复合层里的回流重绘)
Chrome源码调试 -> More Tools -> Rendering -> Layer borders
中看到,黄色的就是复合图层信息
复合图层的作用?(为什么硬件加速会使页面流畅)
- 合成层的位图,会交由 GPU 合成,比 CPU 处理要快
- 当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层
- 对于
transform
和opacity
效果(没有发生形变和rgb变化),不会触发 layout 、layer和 paint,直接进入合成线程处理
如何变成复合图层(硬件加速)
- 本身原因
- transform的3D变化,如
translate3d
、translateZ
(最常用) transform
非3D变化 和opacity
,动画执行的过程中才会创建合成层,动画没有开始或结束后元素还会回到之前的状态will-chang
属性,一般值为opacity与translate使用- 3D元素(如video) 或者 硬件加速的 2D Canvas 元素
- 隐藏规则1:如果a是一个复合图层,而且b在a上面,那么b也会被隐式转为一个复合图层
- 隐藏规则2:fixed布局在高分辨率下也会单独一层
- transform的3D变化,如
- 和其他复合图层有重叠
- 后代是复合图层
同时自身有transform、 overflow不为visible、fixed
QA:什么是复合图层,如何变成复合图层
复合图层需谨慎
由于复合图层可以提高性能,但是用户机器不一,有些机器、浏览器资源消耗过度,页面反而会变的更卡。
且使用复合图层时,尽量提升该元素的z-index层级,或者提升父元素的,使其脱离文档流,因为在硬件加速元素的后面其它元素(层级比这个元素高的,或者相同的,并且releative或absolute属性相同的),会默认变为复合层渲染。这就涉及到可能是图层的隐藏规则:如果a是一个复合图层,而且b在a上面,那么b也会被隐式转为一个复合图层
强制布局同步和快速连续布局
强制布局同步
当我们修改了元素的几何属性,导致浏览器触发重排或重绘时。它会把该操作放进渲染队列,等到队列中的操作到了一定的数量或者到了一定的时间间隔时,浏览器就会批量执行这些操作。
强制同步布局,是指 JavaScript 强制将计算样式和布局操作提前到当前的任务中 , 也就是浏览器会立即执行渲染队列的任务。
常见的如先写后读,增加删除元素后读
//先写后读
function logBoxHeight() {
//正确做法是倒过来...
box.classList.add('claaaas');
console.log(box.offsetHeight);
}
//增加删除元素,获取父元素几何信息
let main_div = document.getElementById("mian_div")
let textnode = document.createTextNode("blue")
main_div.appendChild(new_node);
console.log(main_div.offsetHeight)
快速连续布局
在修改布局时,同时去读。如下,将子元素的宽度设为父的宽度,每次设置完毕下次又会去获取,导致不断的重复布局。
const paragraphs = document.getElementById("mian_div").children
function resizeAllParagraphsToMatchBlockWidth() {
for (var i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = box.offsetWidth + 'px'; //w + 'px'
}
}
const w = box.offsetWidth
function resizeAllParagraphsToMatchBlockWidthCorrect() {
for (var i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = w + 'px'
}
}
QA:如何减少强制布局同步、快速连续布局?
避免先写后读,尽量先读后写,更不要不要在循环中读取
参考
探究 CSS 解析原理
CSS 的工作原理:在关键渲染路径中解析和绘制 CSS
关键渲染流程
避免大规模、复杂的布局
带你走近浏览器的渲染流水线
无线性能优化