讲讲浏览器渲染过程

浏览器渲染过程

本文参考:

  1. 浏览器渲染详细过程:重绘、重排和 composite 只是冰山一角
  2. 你不知道的浏览器页面渲染机制

本文面向面试,只罗列些概念

event loop 规范

  1. 任务源产生任务,放入任务队列
  2. eventloop 从不同优先级队列中取出 task 执行(一轮 eventloop 起点)
  3. 清空微任务队列(mircotask)
  4. 渲染工作,如有需要的话(一轮 eventloop 终点)
  5. 返回第 2 步,循环

在执行 2-4 步骤过程中,不同任务源仍旧会产生任务放入任务队列里,等待下一轮 event loop 时进行处理

每一轮 event loop 都是先处理 task,然后清空微任务队列,而微任务队列被清空的时机不仅仅在 task 执行之后,还有其他时机,比如 javascript 执行栈空闲时等,反正见缝插针

渲染工作是在一轮 event loop 的最后一步去处理,但并不是每一轮 event loop 都会进行渲染工作,因为只要画面的更新只需保持一定的频率,如 60Hz 即可,而一轮 event loop 耗时可能非常短,几毫秒之内,所以没必要每次都进行渲染工作

常见任务源有:网络操作、数据库 IO 操作、用户交互事件、setTimeout 等

产生微任务通常是 Promise

渲染过程

渲染的基本流程:

对于网页页面的初次渲染,自然是从解析 HTML 开始:

  1. 解析 HTML
  2. 构建 DOM 树
  3. 构建 CSSOM 规则树
  4. 合成 Render Tree 渲染树
  5. 布局 Layout 计算节点元素的布局信息:位置、大小
  6. 绘制 Paint,计算节点元素显示内容
  7. 呈现

以上是从整体角度上,一个很简略的过程

1-3 过程是一个流水线形式进行的工作,每解析一个元素后,就可以交由下一步

DOM 树根据 HTML 文档构建,与 HTML 文档一一对应,包括注释的节点元素,文本节点元素,通过 css 不显示的节点元素

CSSOM 规则树则是各个节点元素的样式属性

Render Tree 渲染树则是最终需要呈现绘制出来的节点元素结构,它就剔除了 DOM 树中那些不显示的节点元素了

以上过程是理想状态下,也就是纯静态网页时的渲染过程,不涉及外部 css 文件,也不涉及 js 脚本文件

但一个网页通常都缺不了这两种,那么,在进行上面的过程中,如果遇到 js 脚本文件,会先暂停整个解析流程,等待 js 脚本下载并执行结束后再继续,这是因为,js 脚本里可能会通过 DOM API 来生成、删除、修改 DOM 节点元素

css 的下载和解析并不会阻塞 HTML 的解析,但会阻塞 js 的执行(因为 js 里可能通过 API 操作一些 CSS 属性),而 js 的执行又会阻塞 HTML 的解析,所以,css 是会间接的阻塞 HTML 解析

这也是为什么一些优化的文章里会说,将引用外部 css 文件的 link 标签放置在 HTML 文档开头,script 标签放置在底部的原因,就是为了不让 css 阻塞 js 从而阻塞住页面的解析

那么当 js 脚本里修改了 DOM 结构或样式的场景,页面的刷新流程又是怎么样:

盗自开头链接的文章,侵权删

  1. js 修改 DOM 结构或样式
  2. 重新修改 CSSOM 规则树,如有需要
  3. 重新计算布局信息,也叫回流或重排
  4. 重新计算绘制信息,也就重绘
  5. 合成层渲染工作,主要处理平移、缩放、旋转等动画行为的优化

其实,这图还不够全,再看张图:

盗自开头链接文章,侵权删

这图里有很多箭头,箭头指向空中的表示,该操作并不会造成页面刷新,无需进行下个步骤

所以,其实 js 代码的操作,可能并没改动 DOM 或 CSSOM,也无需重新计算布局信息,只需进行重绘即可

常见的会造成回流(重排)的操作:

  • 添加或删除可见 DOM 元素
  • 元素尺寸改变(边距、边框、宽高)
  • 内容变化
  • 计算 offsetWidth、offsetHeigth
  • 计算 style 属性值

常见会引起重绘的操作:

  • 修改字体颜色 color,背景色 background,是否可见 visibility 等

渲染时机

通常来说,我们在 js 里修改的 DOM 结构触发了页面的更新,但其实,渲染工作并不会马上进行,因为 event loop 是按照 task,mircotask,渲染工作的流程在轮询的

也就是 js 里触发的页面更新,通常来说,并不是立马就导致了重排、重绘的工作进行,而是都会等到渲染工作在进行

比如你在 js 里写个几十行改 CSS 的语句,但这些代码只会在下一次渲染里进行一次重排或重绘工作而已,比如:

紫色的是 Recalculate Style(重新计算 CSS 规则),Layout (重排),Update Layer Tree(更新渲染树)

绿色是 Paint(绘制)

setTimeout 的回调 task 里通过 js 修改了元素的 width,即使多次修改,但也只是会在渲染工作中进行回流

所以,图中应该有两个 task,第一个是 Timer fired 也就是 setTimeout 的回调任务,第二个是渲染工作,包括 Recalculate Style、Layout、Update Layer Tree、Paint

这里之所以说通常,是因为,有一些场景,一些特别的操作,会导致立马进行重排或重绘的工作,比如

当元素 layout 状态为 dirty 时,访问了 offsetTop、scrollHeight 等属性,那么,浏览器会立即重新 layout,以保证读取的是正确的信息

如果元素 layout 状态正常,那么访问 offsetTop 这些属性并不需要重排,因为布局信息是可用的,但如果通过修改元素 width 等属性导致 layout 状态 dirty 之后,再访问 offsetTop 时就需要重新计算,就会立即触发重排工作了

原本正常来说,一轮 event loop 应该是先去任务队列里取一个 task,比如这里的 setTimeout 的回调执行,当该 task 执行结束后,再清空微任务队列,最后再进行渲染工作

但当元素 layout 状态为 dirty 时,此时 js 里访问了 offsetTop,就会立即先去进行 Layout 重排的计算工作了,图中第一个红框里的紫色也就表明确实在当前 task 里就执行去处理渲染工作的计算了

但这个目的仅在于让 js 可以拿到正确的布局信息,所以只需要进行 layout 工作即可,后续的 paint 等还是会继续等待当前 task 执行结束,在该轮 event loop 的最后一个渲染步骤里进行

扩展

其实上面内容都是一些概念性的东西,扫盲用而已

渲染工作实际上并没有这么简单,比如在浏览器上,就包括了渲染进程、GPU 进程

渲染进程(Renderer)是每个 tab 页一个,负责执行 js 和页面渲染,包括 Compositor Thread、Main Thread 等线程

GPU 进程整个浏览器共用一个,主要是将渲染进程中绘制好的位图作为纹理上传给 GPU,调用 GPU 方法显示到屏幕上

Compositor Thread 线程既负责接收浏览器传过来的垂直同步信息 Vsync,也负责接收 OS 系统传来的用户交互、比如滚动、输入、点击、鼠标移动等事件,如果可能,Compositor Thread 会直接处理这些输入事件,并将新的帧直接 commit 给 GPU Thread,从而直接刷新页面。而如果你在这些输入事件上绑定了回调数据,那么 Compositor Thread 就会唤醒 Main Thread,让后者去执行 js、完成 layout,paint 等过程,最后再将页面数据交由 Compositor Thread 来 commit 给 GPU

Main Thread 主要负责 js 的执行,渲染工作的计算(回流、重绘、更新渲染树、合成层工作等)

有时候主线程很卡,导致页面动画很卡时,此时滚动页面,浏览器却仍旧可以做出响应,原因就是因为,渲染的工作其实分多个进程、线程协调合作

再来,Render Tree 上每个节点,其实要么是 Render Layer,要么是 Render Object。后者是 DOM 和 CSSOM 节点合并后的渲染节点对象,而前者则是用来处理一些多层的布局,比如当使用 position 或者 float 或者 z-index 时造成的多层效果时,就会生成一个 Render Layer 来存储当前节点的布局层次信息

还有其他的概念,比如位图、纹理、光栅化等

Main Thread 线程所进行的一系列渲染工作(Layout、Paint)最后得到的是位图数据,也就是页面长什么样的意思

然后需要经过 Compositor Thread 提交给 Raster 线程进程光栅化,光栅化本质就是进行坐标变化、几何离散化、然后填充到 GPU 接收的纹理数据结构

你可以试着用 Performance 抓取一段时间,就可以看到这些线程间的合作进行了

通常界面的刷新过程都是绘制过程,也就是绘制下一帧的界面,但当出现动画,video,canvas 等这些东西时,意味着界面每一帧都在变化,如果每一帧的画面都需要重绘,那么是非常耗性能的,所以针对这种场景,又有一种叫做 Compositing Layer 合成层的来优化

Render Tree 渲染树里的某个节点,当涉及到动画相关时,就会从 Render Layer 或 Render Object 提升为 Graphics Layer,它优化动画的原理在于,动画导致的界面刷新其实原本长什么样基本不变,只是通过一些动画如平移、缩放等来变化界面,所以合成层在处理时,会对原本缓存的纹理使用不同参数重新合成最后的画面,这样就省略了重排、重绘的开销,达到优化的目的了

另外,重绘时,是以合成层作为单位的,也就是每次重绘,并不会重绘整个页面,而是该元素所在的合成层,重绘过程可以在开发者工具里查看

请叫我大苏 wechat
您的支持将鼓励我继续创作!