页面的渲染
浏览器的渲染
浏览器的渲染,就是将(字符串形式)HTML代码变成页面上的像素信息
1 | function render(html: string) { |
何时开始渲染
网络进程中有一个线程,会通过网络通信得到HTML(CSS和JS已通过<link>
和<script>
包含在内),再将其作为任务推入消息队列,随后渲染主线程将其渲染
渲染流水线
渲染需经过多步,它们有明确的输入输出,每一步的输出都会成为下一步的输入,俨然一套流水线
渲染流程
这是一整个渲染流程
解析HTML ParseHTML
字符串难以操作,而对象易于操作,所以要将HTML字符串转化成两棵树:DOM树和CSSOM树,以便后续流程进行
使用JS直接修改CSS样式表而非
element.style
是可能的
渲染主线程是很忙的,为了提高解析效率,浏览器会启动一个预解析器(一个线程)预先下载和解析CSS
预解析线程也可以分担一点下载JS的任务,但渲染主线程遇到JS时必须暂停一切行为,等待JS下载执行完后才能继续解析,因为JS可能会修改当前DOM树
渲染这些元素时会以从父到子/从根到叶的顺序渲染
样式计算Recalculate Style
根据上一步生成的DOM树和CSSOM树,计算样式,需要得到带有样式的DOM树,其中每个节点都具有最终样式(Computed Style,计算后的样式)信息
最终样式并非是所有样式表给某一元素设置的所有规则,而是该元素所有样式的取值,在JS中可以使用
getCoumputedStyle()
获取
层叠和继承就是在此处计算的,(部分)相对单位、预设值等也会被转化成绝对单位
最终CSS样式的计算顺序:
- 根据样式声明(样式表)赋予无冲突的样式值
- 对于存在冲突的规则,根据层叠计算(权重)的结果赋予样式值
- 比较重要性(自定义样式表>浏览器默认样式表)
- 比较特殊性(根据选择器权重值判断更特殊的规则)
- 比较源次序(后面定义的规则>之前定义的规则)
- 对于尚未被赋值的样式,如果该样式能继承,则从父元素那里继承样式
- 如果都没有,则赋予默认值
布局Layout
根据上一步生成的带样式的DOM树,计算各个元素的几何信息,例如位置和尺寸
元素间的位置和尺寸会相互影响,再加上浏览器窗口大小、元素采用的CSS视觉格式化模型规则、等等等影响,这内部的计算过程很复杂,很耗时,但它的计算结果很简单,是一个记录元素尺寸和位置的布局(Layout)树
尺寸就是元素的宽高,有一些长度单位无法在上一步计算宽高(例如百分比、auto),就会在这一步计算;而位置是元素相对包含块的位置
DOM树和Layout树不一定是对应的,例如:
Layout树中的节点不是DOM对象,而是底层C++中的对象,JS无法直接获取,但是可以通过一些API间接获取,例如clientWidth
分层Layer
根据上一步生成的Layout树,将整个页面分出一些层次,这样某一层的改动不会影响到其他层次,只需要重新绘制该层即可。
浏览器不会为页面分太多层次,因为每一个层次都需要占用内存,所以浏览器在分层时,需要在页面规模、节省内存和方便重绘中权衡,不同浏览器对此的策略不一样
跟层叠上下文有关的CSS属性也会影响(不是决定)浏览器的分层决策
可以使用
will-change: transform
标记某一元素可能会经常发生变化,但分层的最终决定权仍然在浏览器,用户无法直接操控分层
绘制Paint
根据上一步生成的分层信息为每一层单独生成绘制指令集
绘制指令集,记录的是如何画,这并不是真的画了出来,例如:
将画笔移动到(0,0)
画一个200x200的矩形
用绿色填充矩形
会发现canvas与这些绘制指令相似,其实它就是在调用浏览器内核的绘制功能
渲染主线程的工作到此为止,剩余步骤会交给其他线程
分块Tiling
根据上一步生成的绘制指令集,将每一层分为多个小区域
一层中需要绘制的内容有很多,分块便于决定绘制优先级,例如优先绘制视口区域内的块
渲染主线程会把上一步的绘制指令集交给合成线程,而合成线程会将分块工作交给多个线程,提高效率
光栅化Raster
根据上一步生成的分块信息,将每个块变成位图,靠近视口的块会被优先处理
位图是由像素点组成的图像,每个像素点存储着颜色信息,Raster graphics - Wikipedia
这个过程会用到GPU加速,合成线程会把工作交给浏览器的GPU进程
画Draw
合成线程在上一步生成的位图信息基础上,进一步计算出了每个位图在屏幕上的位置,并交给GPU进行最终呈现
GPU(图形处理单元,简称显卡)擅长图像操作,常用于执行绘图运算Graphics processing unit - Wikipedia
渲染主线程和合成线程所属的渲染进程位于沙盒里,沙盒与操作系统硬件隔离,这么做是为了安全,防止恶意浏览器插件,但这样一来它也无法进行系统调用(调用操作系统的接口)绘制位图,所以需要浏览器的GPU进程中转
概念
重新布局Reflow
当JS修改CSSOM中一些与几何信息相关的属性,或者改变DOM结构时,页面的布局信息需要改变,需要一个新的Layout树,这就会启动一系列样式重新计算
这其中涉及这么多步骤,尤其是布局layout比较复杂,对性能的影响比较大,所以编码时应该尽量少进行影响布局的操作
作为优化,浏览器会避免连续多次操作导致的Layout树反复计算:将这些操作合并成任务推入队列,当JS代码全部完成后再进行统一计算
所以改动属性造成的reflow是异步完成的,那是不是说明,JS可能无法获取最新的布局信息?
1 | // 修改了这么多,但布局树只会重新计算一次 |
为了解决这一问题,当JS代码读取布局信息时,会产生一个同步任务:先reflow完成再执行接下来的JS代码
总结:设置不会立即导致reflow,读取会立即导致reflow
如果需要强制进行重新布局,就可以当场抓个幸运元素读取一下它的布局信息
重绘Repaint
当JS修改一些与几何信息无关的可见属性,例如颜色时,就会进行重绘,样式计算结束后可以直接跳到绘制paint步骤
不需要执行布局layout步骤,大部分情况下也不需要执行分层layer步骤
reflow一定会引发repaint,因为元素的布局信息也属于可见样式
transform
CSS transform属性效率高,本质是因为它在渲染的最后一个步骤,画draw这一步实现
合成线程给GPU进程的指引信息quad不仅指引绘制位置,还会考虑transform变形。它不在主线程进行,且不涉及前面复杂的步骤,所以效率才会这么高
无论渲染主线程如何忙碌,都不会影响transform
1 | <!DOCTYPE html> |
1 | <!DOCTYPE html> |