HTML、CSS、JavaScript如何渲染页面的?

8/28/2022 渲染流程重排重绘

# 前言

浏览器在提交文档数据后(导航流程),开始进行页面渲染。
我们在开发HTML、CSS、JavaScript后,浏览器打开就可以展示页面。

  • HTML(Hyper Text Markup Language)
    标签(标记) + 文本 => HTML文件内容
    每个标签都有自己的语义,浏览器根据标签的语义正确的展示文本内容。
  • CSS(Cascading Style Sheets)
    选择器 + 属性 => CSS
    改变HTML内容的字体大小、颜色等等,需要CSS来实现,通过选择器定位到HTML标签内容,渲染引擎根据属性正确显示HTML内容。
  • JavaScript
    让网页更加动态,能够操作HTML和CSS

渲染流水线 => 渲染进程执行过程中会被拆分为很多个子阶段,输入HTML文件经过很多个子阶段到最后渲染显示。
渲染流水线经历的子阶段:

  • DOM树构建 => HTML转成DOM树
  • CSS样式计算 => CSS转成styleSheets并且计算DOM节点样式
  • 生成布局树 => DOM + styleSheets计算布局信息生成布局树
  • 分图层 => 对布局树进行分层,生成分层树
  • 图层绘制 => 对每个图层绘制生成绘制指令列表,提交给合成线程
  • 栅格化 => 合成线程将图层分成图块,图块进行栅格化生成位图(栅格化线程池来维护栅格化任务使用GPU加速将图块转成位图并保存在GPU内存中)
  • 合成显示 => 等待图块变成位图(光栅化)后,合成线程发送绘制图块DrawQuad指令通知浏览器进程,浏览器进程使用GPU进程内存中位图生成页面将其显示到显示器 渲染引擎流程

# DOM树构建

为何需要构建DOM树?
浏览器无法直接理解使用HTML文件,所以需要将HTML转换成浏览器能理解的DOM树结构
构建流程:输入HTML文件 => HTML解析器解析 => 输出DOM树。
document对象就是DOM树的根节点,DOM和HTML内容基本一致,但DOM保存在内存中的树状结构,支持JavaScript查询或修改,JavaScript如何影响DOM树构建?
DOM树结构

# 样式计算

CSS样式来源:

  • link引入的外部CSS文件
  • style标签内的CSS
  • 标签元素style属性内嵌的CSS
  • 浏览器内置提供的默认userAgent样式表

# 1. 将CSS转换为styleSheets结构

浏览器无法直接理解纯文本CSS样式,当渲染引擎接受到CSS文件,执行转换操作,将CSS文本转换为浏览器能理解的styleSheets结构,styleSheets结构同样的也具备查询修改的功能。

# 2. 标准化styleSheets(样式表)中的属性值

浏览器能理解样式表后,CSS里面有很多属性值并不是标准的计算值,要统一转成渲染引擎理解的标准值。
常见的:1em、blue、bold => 标准化后 => 16px、rgb(0,0,255)、700;

body { font-size: 1em }
p {color:blue;}
div {font-weight: bold}
1
2
3

# 3.计算DOM树中每个节点的具体样式值

计算方式 => 继承规则 + 层叠规则
CSS继承规则:所有DOM的子节点都会继承其父节点的样式。
CSS样式层叠:定义了如何合并来自多个源的属性值的算法。

# 布局树生成

此时有了DOM树以及标签元素各自的样式,但是还是无法显示页面,因为还不知道DOM标签元素的几何位置信息。所以接下来需要计算DOM树中可见元素的几何位置

# 1.创建布局树

DOM树中有可能包含很多不可见元素(display: none),所以在显示之前,需要额外创建一个只包含可见元素的布局树
DOM树 + 样式表 => 生成只包含可见元素的布局树
浏览器要做的事情:

  • 遍历DOM树可见节点,添加到布局中;
  • 不可见节点被布局树忽略;

# 2.布局计算

有了完整的布局树之后,需要开始计算布局树各个节点的坐标位置

# 分图层

有了布局树,每个元素的具体位置也算出来了,接下来需要进行分层,为何还需要分层呢?
因为页面上有很多复杂效果:3D变换、页面滚动、z-index(z轴排序),这些效果的实现,需要渲染引擎为这些特定的节点生成专用的图层,并且最终生成一颗图层树(LayerTree),最后将图层进行叠加构成了最终的页面图。
一般情况下,布局树并非每个节点都包含一个图层,如果某个节点没有对应的图层,那么会继承父节点所在的图层,每个节点都会直接或间接属于一个图层。
什么情况下,渲染引擎会为特定节点创建新的图层?

  • 拥有层叠上下文属性的元素,会被提升为单独一个图层。
    • 明确定位属性的元素(position: fixed)
    • 定义透明属性属性的元素(opacity: 0.5)
    • css滤镜
    • z-index
  • 页面裁剪(clip)的地方也会创建为图层
    • 常见的div内容文本过多被裁剪显示
    • 出现滚动条,会被单独提升为一层

# 图层绘制

图层树构建完成后,渲染引擎会对图层树中每个图层进行绘制。
一个图层的绘制会被拆分成很多个绘制指令,这些小指令按照顺序组成一个待绘制表(用来记录绘制顺序和绘制指令的列表)。
绘制指令:绘制边框、背景、颜色more...

# 栅格化raster(GPU生成位图)

绘制表准备完毕后,主线程通知(commit)合成线程开始对图层进行处理。
前置知识:

  • 屏幕上页面的可见区域称为视口(ViewProt)。一般一个页面很大,但是用户只能看到其中一部分,这部分就是ViewProt。
  • 有时候图层很大,页面需要滚动才能看完,但是对于用户的视口只能看到一小部分,为了避免不必要的开销,没必要绘制图层的所有内容。
  • 栅格化、光栅化 => 图块生成位图。

合成线程如何工作?

  • 合成线程将图层进行划分为图块(tile),一般256 * 256 or 512 * 512。
  • 然后合成线程按照ViewProt附近的图块优先去生成位图(栅格化 or 光栅化),图块是栅格化执行的最小单位。
  • 渲染进程维护了一个栅格化的线程池,所有图块的栅格化任务都在线程池内执行。
  • 渲染进程把栅格化任务发送给GPU(GPU加速),在GPU中执行生成图块的位图(GPU栅格化),保存在GPU内存中。

# 合成显示

当所有的图块被栅格化,合成线程生成一个绘制图块的指令(DrawQuad),提交命令给浏览器进程。
浏览器进程里面有一个叫viz的组件,用来接收合成线程发过来的DrawQuad命令,然后配合GPU内存中的位图,将页面内容进行绘制合成,最后将页面内容显示到屏幕上。

# 渲染流程相关的概念

# 重排 -- 更改元素的几何属性

通过JavaScript或者CSS修改了元素的几何位置(布局)、尺寸属性,比如元素宽高(div.style.height = xxx),浏览器会重新触发布局、分图层等之后的一系列子阶段,这个过程称之为重排。重排需要再走一遍完整的渲染流水线,开销最大

  • DOM元素删除、添加
  • 改变位置
  • 改变尺寸(宽高、内外边距...)
  • 改变浏览器窗口尺寸(resize)
  • 激活CSS伪类
  • 设置style属性改变结点样式的

# 重绘 -- 更改元素的绘制属性

JavaScript更改元素的背景色(div.style.background = red),因为没有引起几何位置的变换,所以布局阶段不会执行,直接进入绘制阶段,然后执行之后的一系列子阶段,这个过程称之为重绘。重绘省去了布局和分图层阶段,执行效率比重排更高。

  • color
  • border-style
  • visibility
  • background
  • text-decoration
  • box-shadow

# 直接合成

更改一个不需要布局也不需要绘制的属性,渲染引擎直接跳过布局和绘制,只执行后续合成操作,比如CSS的transform实现动画效果(transform:translate(0, 0)),可以避开重排和重绘,直接在合成线程上(非主线程)上执行合成操作,效率最高。

  • 在非主线程上合成,并没有占用主线程资源
  • 同时避开了布局、分层、绘制两个阶段

# 总结

  • 触发重排reflow、重绘repaint的操作尽量放一起,比如改变DOM高度和设置margin分开写的,可能会触发两次重排。
  • 框架的虚拟DOM层计算出操作DOM前后总的差异,一起提交给浏览器,减少触发重排重绘的次数。
Last Updated: 11/2/2022, 5:11:53 PM