页面渲染流程
我们看几张的经典流程图
浏览器基础结构
我们一直都在说Javascript是单线程,但浏览器是多线程的,在内核控制下互相配合以保持同步,主要的常驻线程有:
GUI渲染线程
负责渲染界面,解析HTML,CSS,构建DOM和Render树布局绘制等。如果过程中遇到Javascript引擎执行会被挂起线程,GUI更新保存在一个队列中等待Javascript引擎空闲才执行;
Javascript引擎线程
负责解析运行Javascript;执行时间过程会导致页面渲染加载阻塞;
事件触发线程
浏览器用以控制事件循环。当Javascript引擎执行过程中触发的事件(如点击,请求等)会将对应任务添加到事件线程中,而当对应的事件符合触发条件被触发时会把对应任务添加到处理队列的尾部等到Javascript引擎空闲时处理;
定时器触发线程
因为Javascript引擎是单线程容易阻塞,所以需要有单独线程为setTimeout和setInterval计时并触发,同样是符合触发条件(记时完毕)被触发时会把对应任务添加到处理队列的尾部等到Javascript引擎空闲时处理;W3C标准规定时间间隔低于4ms被算为4ms。
异步http请求线程
XMLHttpRequest在连接后浏览器新开线程去请求,检测到状态变化如果有设置回调函数会产生状态变更事件,然后把对应任务添加到处理队列的尾部等到Javascript引擎空闲时处理;
Composite 线程
通过 GPU 执行,真正将页面绘制并输出到屏幕上
页面渲染
渲染流程主要步骤:
解析HTML生成DOM树
整个过程是一个深度遍历的操作,它会从当前节点从上往下逐个构建完之后才会去构建同层级节点及其子节点,最后的结构我网上随便找了一张图,大概如下.
但是这个过程是可以被CSS或者Javascript加载而阻塞,这是浏览器的一个处理机制,因为这两者都可能会改变DOM树结构的变化,所以会等它们执行完之后再继续解析
解析样式生成CSSOM树
这里是跟DOM树同时生成的,这里包含了link, style和内联多份不同的样式整合解析成一份最终的规则,整个结构也是类似DOM树的属性结构,最后的结构我也是网上随便找了一张图,大概如下.
样式的整合计算是个很复杂的过程,存在默认样式,权重,继承,伪元素,重复定义,单位换算等多种不同程度的难题
权重规则大概如下:
- 元素选择符: 1
- class 选择符: 10
- id 选择符: 100
- 内联样式: 1000
- !important 优先级最高
- 如果优先级相同, 则选择最后出现的样式;
- 继承得到的样式的优先级最低;
- 嵌套选择器优先级是相加, 例如: #A .B = 100 + 10 = 110;
实际上浏览器 CSS 选择器的解析规则是从右往左的,每一步都能过滤掉些不符合规则的分支情况, 直到找到根元素或满足条件的匹配规则的选择器就结束这个分支的遍历.所以尽量避免深层嵌套 CSS, 因为寻找选择器和计算最终样式都会受影响的.
两者合并构建Render树
DOM树和CSSOM树是根据代码解析而成的,而代码最终给到浏览器渲染的肯定是视觉上的表现结构,因为它们都带有字体,颜色,尺寸,位置甚至是否存在的样式控制,最后的结构我也是网上随便找了一张图,大概如下.
总的来说,会遍历整个DOM树节点,忽略不可见节点(例如 meta,header 以及指定了属性 display: none等节点),得出各个节点的CSS定义以及他们的从属关系,从而去计算出每个节点在屏幕中的位置
根据Render树开始布局绘制
页面布局以可见区域为画布,从左到右从上往下从根节点开始布局.遍历Render树调用渲染器的API绘制节点内容填充屏幕进行像素级信息计算与绘制
实际上这一步绘制是在多个层上进行绘制
Composite
实际上在paint
之后,display
之前还有一个环节叫渲染层合并
对页面中 DOM 元素的绘制是在多个层上进行的。在每个层上完成绘制过程之后,浏览器会将所有层按照合理的顺序合并成一个图层,然后显示在屏幕上。对于有位置重叠的元素的页面,这个过程尤其重要,因为一旦图层的合并顺序出错,将会导致元素显示异常。(下面会详解)
回流与重绘
这一般是解析过程或者中间样式结构被改变需要重新进行计算布局才会发生,
回流:当浏览器发现变化影响了布局需要重新计算
重绘:只影响当前元素的视觉效果而不会影响其他元素
回流是影响页面性能的重要因素之一,尽量避免
渲染完成
这不是一个循序渐进的过程,浏览器为了尽快渲染界面展示给用户看,从网络请求获取到文档内容的同时就开始局部渲染,而不会等所有HTML解析完才会创建渲染.而且这是基本的流程,实际上现在主流浏览器会有些区别,也会优化整个渲染流程,所以这个过程实际上并没有我们想象中的这么慢
渲染层合成
我们就以Chrome
为例
在 Chrome 中其实有几种不同的层类型:
- RenderLayers 渲染层,这是负责对应 DOM 子树.
- GraphicsLayers 图形层,这是负责对应 RenderLayers 子树。
RenderLayers 渲染层
DOM树每个节点都有一个对应的渲染对象(RenderObject)
,当它们处于一个相同的坐标空间(z轴)就会共同形成一个渲染层.,从上面知道以坐标空间区分不同的渲染层,所以
- 渲染层可以有多个
- DOM元素通过改变层叠上下文即可创建新的渲染层
- 坐标空间决定层叠上下文,但不是唯一因素,下面条件可得
而能够满足条件的情况有:
NormalPaintLayer
- 文档根元素(<html>);
- position 值为 absolute(绝对定位)或 relative(相对定位)且 z-index 值不为 auto 的元素;
- position 值为 fixed(固定定位)或 sticky(粘滞定位)的元素(沾滞定位适配所有移动设备上的浏览器,但老的桌面浏览器不支持);
- flex (flexbox) 容器的子元素,且 z-index 值不为 auto;
- grid (grid) 容器的子元素,且 z-index 值不为 auto;
- opacity 属性值小于 1 的元素(参见 the specification for opacity);
- mix-blend-mode 属性值不为 normal 的元素;
以下任意属性值不为 none 的元素:
- transform
- filter
- perspective
- clip-path
- mask / mask-image / mask-border
- isolation 属性值为 isolate 的元素;
- -webkit-overflow-scrolling 属性值为 touch 的元素;
- will-change 值设定了任一属性而该属性在 non-initial 值时会创建层叠上下文的元素(参考这篇文章);
- contain 属性值为 layout、paint 或包含它们其中之一的合成值(比如 contain: strict、contain: content)的元素。
摘抄自层叠上下文
OverflowClipPaintLayer
- overflow 不为 visible;
NoPaintLayer
- 不需要 paint 的 PaintLayer,比如一个没有视觉属性(背景、颜色、阴影等)的空 div
DOM节点和渲染对象是一一对应的,满足上面条件的渲染对象拥有独立的渲染层,不满足的将与其第一个拥有渲染层的父元素共用同一个.简单来说,RenderObject决定渲染内容,而RenderLayers决定绘制顺序.
GraphicsLayers 图形层
每个 GraphicsLayer
对应着一个渲染层是负责最终呈现的内容图形层,它拥有一个图形上下文(GraphicsContext)
负责输出该层的位图.储存在共享内存的位图将作为纹理上传到GPU,最后由GPU将多个位图进行合成绘制.每个GraphicsLayer可以独立的进行渲染而不会相互影响,那么将一些频繁重绘的元素放到单独的GraphicsLayer中就不会对整个页面造成影响
所以 GraphicsLayer 是一个重要的渲染载体和工具,但它并不直接处理渲染层,而是处理合成层。
CompositingLayer 合成层
某些特殊的渲染层会被提升至合成层(Compositing Layers)
,提升的前提是必须为 SelfPaintingLayer
(可以认为就是上面提到的 NormalPaintLayer
).合成层拥有单独的 GraphicsLayer,而其他不是合成层的渲染层,则和其第一个拥有 GraphicsLayer 父层共用一个。
当满足以下条件的渲染层会被浏览器自动提升为合成层
直接原因(direct reason)
- 硬件加速的 iframe 元素(比如 iframe 嵌入的页面中有合成层)
- video 元素
- 覆盖在 video 元素上的视频控制栏
- 3D 或者 硬件加速的 2D Canvas 元素(普通 2D Canvas 不会提升为合成层)
- 硬件加速的插件,比如 flash 等等
- 在 DPI 较高的屏幕上,fix 定位的元素会自动地被提升到合成层中。但在 DPI 较低的设备上却并非如此,因为这个渲染层的提升会使得字体渲染方式由子像素变为灰阶
- 有 3D transform
- backface-visibility 为 hidden
- 对 opacity、transform、fliter、backdropfilter 应用了 animation 或者 transition(需要是 active 的 animation 或者 transition,当 animation 或者 transition 效果未开始或结束后,提升合成层也会失效)
- will-change 设置为 opacity、transform、top、left、bottom、right(其中 top、left 等需要设置明确的定位属性,如 relative 等)
后代元素原因
- 有合成层后代同时本身有 transform、opactiy(小于 1)、mask、fliter、reflection 属性
- 有合成层后代同时本身 overflow 不为 visible(如果本身是因为明确的定位因素产生的 SelfPaintingLayer,则需要 z-index 不为 auto)
- 有合成层后代同时本身 fixed 定位
- 有 3D transfrom 的合成层后代同时本身有 preserves-3d 属性
- 有 3D transfrom 的合成层后代同时本身有 perspective 属性
overlap 重叠原因
重叠或者说部分重叠在一个合成层之上。(下面隐式合成会讲到)
- 最常见和容易理解的就是元素的 border box(content + padding + border) 和合成层的有重叠
- filter 效果同合成层重叠
- transform 变换后同合成层重叠
- overflow scroll 情况下同合成层重叠。即如果一个 overflow scroll(不管
overflow:auto
还是overflow:scrill
,只要是能 scroll 即可) 的元素同一个合成层重叠,则其可视子元素也同该合成层重叠 - 假设重叠在一个合成层之上(assumedOverlap)
比如一个元素的 CSS 动画效果,动画运行期间,元素是有可能和其他元素有重叠的。针对于这种情况,于是就有了 assumedOverlap 的合成层产生原因,即使动画元素视觉上并没有和其兄弟元素重叠,但因为 assumedOverlap 的原因,其兄弟元素依然提升为了合成层。
需要注意的是该原因下,有一个很特殊的情况:
如果合成层有内联的 transform 属性,会导致其兄弟渲染层 assume overlap,从而提升为合成层。
合成层拥有单独的GraphicsLayers,而其他渲染层则和第一个拥有GraphicsLayers 的父层共用一个
隐式合成
部分渲染层在一些特定场景下,会被默认提升为合成层,举例来说
- 两个绝对定位有不同z-index属性的元素重叠在一起,可以从控制台看出两个元素跟父元素共用同一个GraphicsLayers
- 这时候如果底层元素加上
transform: translateZ(0)
提升至合成层之后,按理说应该会层级更高,但是实际上两个元素都被提升到合成层,这是因为浏览器为了纠正层叠顺序保证视觉效果一致,隐性强行提升了原本高于底层元素层级的元素至合成层
层爆炸和层压缩
层爆炸
一些合成层的条件十分隐蔽,导致产生了不在预期范围内的合成层,达到一定数量之后会严重消耗性能占用 GPU 和大量的内存资源引起页面卡顿,形成层爆炸.我们看下面代码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<style>
@keyframes slide {
from {
transform: none;
}
to {
transform: translateX(100px);
}
}
.animating {
width: 300px;
height: 30px;
background-color: orange;
color: #fff;
animation: slide 5s alternate linear infinite;
}
#ul {
margin-top: -20px;
padding: 5px;
border: 1px solid #000;
}
li {
width: 600px;
height: 30px;
margin-bottom: 5px;
background-color: blue;
color: #fff;
position: relative;
/* 会导致无法压缩:squashingClippingContainerMismatch */
overflow: hidden;
}
p {
position: absolute;
top: 2px;
left: 2px;
font-size: 16px;
line-height: 16px;
padding: 2px;
margin: 0;
background-color: green;
}
</style>
</head>
<body>
<div class="animating">composited animating</div>
<ul id="ul"></ul>
</body>
<script>
window.onload = function() {
var $ul = document.getElementById('ul');
for (var i = 0; i < 10; i++) {
var ulObj = document.createElement("li");
var pObj = document.createElement("p");
pObj.innerText = i
// ulObj.appendChild(pObj);
$ul.appendChild(ulObj);
}
}
</script>
</html>
实验可得10个左右已经开始卡顿,100就基本卡死了.再看控制台现在层的情况
造成层爆炸的原因因为满足了以下情况:
- animating元素应用了transform动画,被提升到合成层
- 动画运行期间,元素是有可能和其他元素有重叠的(assumedOverlap),动画元素视觉上并没有和其兄弟元素重叠,但因为
assumedOverlap
的原因,其兄弟元素依然提升为了合成层。所以li元素都被提升到合成层,如果单单这种情况还是能层压缩的 - li设置了
overflow: hidden
,当渲染层同合成层有不同的裁剪容器(clipping container)时,该渲染层无法压缩(squashingClippingContainerMismatch)
最佳方案是打破 overlap 的条件,也就是说让其他元素不要和合成层元素重叠。
让其他元素不和合成层重叠
.animating { ... position: relative; z-index: 1; }
- 一定需要覆盖动画之上的话,可以调整样式替代裁剪样式,去掉overflow属性
层压缩
浏览器会对多个渲染层同一个合成层重叠时进行优化,这些渲染层会被压缩到一个 GraphicsLayer 中,以防止由于重叠原因导致可能出现的“层爆炸”,继续用上面的例子继续加元素
可以发现在提升到合成层元素之上的多个不同元素会共同提升至同一个合成层,但也有很多无法压缩的情况.但也不是必然,也有很多无法压缩的情况
无法进行会打破渲染顺序的压缩(squashingWouldBreakPaintOrder)
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title></title> <style> #container { position: relative; width: 420px; height: 80px; border: 1px solid black; } #composited { width: 100%; height: 100%; transform: translateZ(0); } #ancestor { -webkit-mask-image: -webkit-linear-gradient(rgba(0, 0, 0, 1), rgba(0, 0, 0, 0)); } #overlap-child { position: absolute; left: 0; top: 10px; bottom: 0px; width: 100%; height: 60px; background-color: orange; } </style> </head> <body> <div id="container"> <div id="composited">Text behind the orange box.</div> <div id="ancestor"> <div id="overlap-child"></div> </div> </div> </body> </html>
在本例中,#overlap-child
同合成层重叠,如果进行压缩,会导致渲染顺序的改变,其父元素 #ancestor
的 mask 属性将失效,因此类似这种情况下,是无法进行层压缩的。目前常见的产生这种原因的情况有两种,一种是上述的祖先元素使用 mask 属性的情况,另一种是祖先元素使用 filter 属性的情况
- video 元素的渲染层无法被压缩同时也无法将别的渲染层压缩到 video 所在的合成层上(squashingVideoIsDisallowed)
- iframe、plugin 的渲染层无法被压缩同时也无法将别的渲染层压缩到其所在的合成层上(squashingLayoutPartIsDisallowed)
- 无法压缩有 reflection 属性的渲染层(squashingReflectionDisallowed)
- 无法压缩有 blend mode 属性的渲染层(squashingBlendingDisallowed)
当渲染层同合成层有不同的裁剪容器(clipping container)时,该渲染层无法压缩(squashingClippingContainerMismatch)
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title></title> <style> .clipping-container { overflow: hidden; height: 10px; background-color: blue; } .composited { transform: translateZ(0); height: 10px; background-color: red; } .target { position: absolute; top: 0px; height: 100px; width: 100px; background-color: green; color: #fff; } </style> </head> <body> <div class="clipping-container"> <div class="composited"></div> </div> <div class="target">不会被压缩到 composited div 上</div> </body> </html>
本例中 .target
同 合成层 .composited
重叠,但是由于 .composited
在一个 overflow: hidden
的容器中,导致 .target
和合成层有不同的裁剪容器,从而 .target
无法被压缩。
相对于合成层滚动的渲染层无法被压缩(scrollsWithRespectToSquashingLayer)
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title></title> <style> body { height: 1500px; overflow-x: hidden; } .composited { width: 50px; height: 50px; background-color: red; position: absolute; left: 50px; top: 400px; transform: translateZ(0); } .overlap { width: 200px; height: 200px; background-color: green; position: fixed; left: 0px; top: 0px; } </style> </head> <body> <div class="composited"></div> <div class="overlap"></div> </body> </html>
红色的 .composited
提升为了合成层,, 当滑动页面,.overlap
重叠到 .composited
上时,.overlap
会因重叠原因提升为合成层,同时,因为相对于合成层滚动,因此无法被压缩(原文一开始说只有composited是合成,重叠之后overlap才会提升,但是我试验是一开始就都提升了)
- 当渲染层同合成层有不同的具有 opacity 的祖先层(一个设置了 opacity 且小于 1,一个没有设置 opacity,也算是不同)时,该渲染层无法压缩(squashingOpacityAncestorMismatch,同 squashingClippingContainerMismatch)
- 当渲染层同合成层有不同的具有 transform 的祖先层时,该渲染层无法压缩(squashingTransformAncestorMismatch,同上)
- 当渲染层同合成层有不同的具有 filter 的祖先层时,该渲染层无法压缩(squashingFilterAncestorMismatch,同上)
- 当覆盖的合成层正在运行动画时,该渲染层无法压缩(squashingLayerIsAnimating),当动画未开始或者运行完毕以后,该渲染层才可以被压缩(这一块我没实验过,直接用原文图片)
梳理流程
再回顾一下流程图
- 文档被解析成对应的DOM树结构和CSSOM树结构
- 再被解析成Render树,每个节点被解析成对应的
RenderObject
,里面包含了关于浏览器怎么渲染节点的信息,如果某些节点不被渲染则不会生成对应的RenderObject
,而RenderObject
的作用就是向GraphicsContext
发出绘制的调用来进行元素的绘制 - Render树再被解析成RenderLayers树, RenderObject根据层叠顺序解析成不同的RenderLayers 渲染层,
- RenderLayers树解析成GraphicsLayers 树,每个GraphicsLayer都有自己的GraphicsContext可以进行独立渲染
- 某些特殊的RenderLayers会也被提升成CompositingLayers,拥有单独的GraphicsLayer
- 开始将一层一层绘制 Layer Tree,一个 Layer 的绘制会被拆分成许多个简单的 绘制指令,然后按顺序将这些指令组成一个绘制列表。绘制列表生成之后,渲染进程的主线程(GUI 渲染线程)会给 Composite 线程发送消息,把绘制列表给 Composite 线程开始绘制页面。
- 拆分图块, 拆分图块的目的是为了优先绘制首屏范围覆盖的内容,而不需要等一个 Layer 绘制完成才显示。同时,Chrome Webkit 考虑到首屏的内容依然可能非常复杂导致绘制的时间较长,为了改善用户体验,绘制图块时会先显示低分辨率的图片,当图块绘制完成时,再把低分辨率的图片绘制成完整的。(这就是上面说过的浏览器优化渲染部分)
- 栅格化,栅格化会将图块(tile)转换成位图(bitmap),如果开启 GPU 硬件加速,则转换过程在 GPU 进程中完成,生成的位图会缓存在 GPU 的内存中;如果没有开启 GPU 硬件加速,则由渲染进程完成,生成的位图缓存在共享内存中。
- 合成和显示, 在上一步的栅格化完成之后,Composite 线程会发送消息通知浏览器主进程从内存中获取位图数据,将位图绘制到屏幕上。
绘制部分摘抄自了解浏览器渲染的过程和原理
CPU 与 GPU 渲染区别
CPU
中央处理器(CPU,central processing unit)作为计算机系统的运算和控制核心,是信息处理、程序运行的最终执行单元。
在计算机体系结构中,CPU 是对计算机的所有硬件资源(如存储器、输入输出单元) 进行控制调配、执行通用运算的核心硬件单元。CPU 是计算机的运算和控制核心。计算机系统中所有软件层的操作,最终都将通过指令集映射为CPU的操作。
GPU
图形处理器(英语:Graphics Processing Unit,缩写:GPU),又称显示核心、视觉处理器、显示芯片,是一种专门在个人电脑、工作站、游戏机和一些移动设备(如平板电脑、智能手机等)上做图像和图形相关运算工作的微处理器。
GPU使显卡减少了对CPU的依赖,并进行部分原本CPU的工作,尤其是在3D图形处理时GPU所采用的核心技术有硬件T&L(几何转换和光照处理)、立方环境材质贴图和顶点混合、纹理压缩和凹凸映射贴图、双重纹理四像素256位渲染引擎等,而硬件T&L技术可以说是GPU的标志。
它不单单存储图形,而且能完成大部分图形功能,这样就大大减轻了CPU的负担,提高了显示能力和显示速度。
从上可得,当CPU处理好位图信息交给GPU渲染能够极大解放CPU负担,也能快速渲染图层.当然过度使用会导致严重的性能下降,内存占用过高
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。