重新认识浏览器是如何渲染一个页面,细节满满

当网络线程接收到一个 HTML 文件后,会产生一个渲染任务,放到消息队列中。

渲染主线程会在事件循环机制下,开启渲染流程。

整个渲染流程分为 八大阶段,分别是:

  • parse - HTML 解析
  • style - 样式计算
  • layout - 布局
  • compositor - 分层
  • paint - 绘制
  • tiling - 分块
  • raster - 光栅化
  • draw - 画

这八大步骤中,涉及到了 渲染主线程合成线程光栅化线程GPU进程 等多个线程和进程分工合作,每个阶段都有明确的输入和输出,上一个阶段的输出会成为下一个阶段的输入。

这样整个渲染流程就形成了一套组织严密的生产流水线。

见下图:

接下来,咱们就一步一步地剖析,每一步浏览器是怎么做的。

1_Parse - HTML 解析

在解析之前,浏览器会开启一个预解析线程,将 HTML 文件中的外部资源(外部 CSS、外部 JS)先下载下来。

解析 HTML 文件时,遇到 CSS 解析 CSS,遇到 JS 执行 JS。

解析 CSS 时,如果外部 CSS 没有下载完成,渲染主线程不会等待,会继续向后解析。因为下载和解析 CSS 会在预解析线程中完成,完成后会将 CSSOM 树交给渲染主线程,这就是 CSS 不会阻塞 HTML 解析和渲染页面的根本原因

解析 JS 时,渲染主线程会停止解析 HTML 和渲染页面,将 JS 文件下载完成并执行完后,才会继续解析和渲染。这是因为在执行 JS 的过程中可能会修改现有的 DOM 结构,所以解析生成 DOM 树必须暂停,否则会导致最后渲染出来的结构不可控。这是 JS 会阻塞 HTML 解析和渲染页面的根本原因

解决这个问题的方法就是使用异步加载,deferasync,它们的区别是:

  • defer 会等 JS 文件下载完成,并且 HTML 解析完成后,才开始执行 JS 代码;
  • async 会等 JS 文件下载完成,立即执行 JS 代码
    第一步完成后,渲染主线程会得到 DOM 和 CSSOM

2_calculate style - 样式计算

得到了 DOM 和 CSSOM ,渲染主线程会进行样式计算。

主线程会遍历 DOM,为每个节点计算出最终的样式,称为 Computed Style。这个过程中,很多预设值会变成绝对值(如 red 会变成 rgb(255,0,0)),相对单位会变成绝对单位(如 em 会变成 px)。

第二步完成后,渲染主线程会得到带有样式的 DOM

3_layout - 布局

得到了带有样式的 DOM 后,渲染主线程会进行布局。

主线程会遍历 DOM,计算每个节点的几何信息(节点的宽高、相对包含块的位置)。

大部分时候,DOM 树和 layout 树不是一一对应的。因为,有些节点的样式是 display: none;visibility: hidden;时,就没有几何信息,不会在 layout 树中;有些节点有伪元素选择器,虽然 DOM 中不存在这些节点,但是这些伪元素在页面上显示是有几何信息的,所以最后会生成到 layout 树中;还有匿名行盒、匿名块盒等等都会导致 DOM 树和 layout 树无法一一对应。

layout 树是一个对象,但不是一个 DOM 对象,而是 C++ 对象。JS 是获取不到的,我们获取的元素的 clientHeight、clientWidth 这些都是从 layout 树当中获取的。
第三步完成后,渲染主线程会得到 layout 树(包含了节点的几何信息)

4_compositor - 分层

得到了 layout 树后,渲染主线程会进行分层。

将多个元素合为一个层,而不是将所有元素全部作为一个图层。分层的好处是,当元素变化时,只会影响该元素所在的当前图层重新处理,而不会影响其他图层,从而提升效率。

分层的结果会受滚动条的样式和堆叠上下文 相关的属性(z-indexopacitytransform)的影响,浏览器会结合内存条件去进行合适的分层。因为分层数多了会影响内存,很卡;分层数少了会影响重绘时的性能。

此外 will-change 属性会更大程度地影响分层结果,设置了该属性的元素,浏览器会单独为该元素生成一个图层。通常在页面渲染卡顿时,用作于性能优化,加快渲染速率。但是慎用,不能一个页面用于多个元素,这也会造成内存问题。

第四步完成后,渲染主线程会得到分层信息

5_paint - 绘制

渲染主线程会为每个层产生绘制指令集,用于描述这一层的内容该如何画出来。

第五步完成后,渲染主线程的工作就完成了,会将每个分层的指令集交给合成线程

6_tiling - 分块

合成线程会将每个图层再进行划分,划分成更多的小块。好处是,当图层中的某个元素发生变化时,不必重绘整个层,只需绘制当前图层的所在小块。

然后合成线程会从线程池中取出多个线程来完成分块工作。

第六步完成后,合成线程会得到分成许多小块的分层

7_raster - 光栅化

这一步,合成线程会将上一步得到的块信息交给 GPU 进程,快速地进行光栅化。

GPU 进程会开启多个线程来完成光栅化,并且优先处理靠近视口区域的块。

光栅化的结果就是一块一块的位图。

这个过程也会用到 GPU 加速

第七步完成后,合成线程会得到一块一块的位图

8_draw - 画

合成线程拿到了每个层、每个块的位图后,会生成一个个 指引(quad)信息。

指引会标识出每个位图应该画到屏幕的哪个位置,以及会考虑到旋转、缩放等变形。

是的,transform 属性在最后一步才会处理,与主线程无关,这就是为什么使用 transform 效率高的本质原因。

合成线程会把 quad 提交给 GPU 进程,由 GPU 进程产生系统调用,提交给 GPU 硬件(显卡)完成最终的屏幕成像。

这里补充一个细节

真正去做光栅化的是显卡,合成线程交给 GPU 进程,GPU 进程再交给显卡

请问,中间为什么需要 GPU 进程转一下,为什么合成线程不直接交给显卡呢?

这是因为我们的我们每个渲染进程都是独立运行的。为了保障操作系统的安全性,每个渲染进程都有个沙盒环境,和外界隔离开。这样当我们的某个线程遭到攻击,有了病毒,不至于影响整个操作系统。

所以,当合成线程想直接交给显卡时,是必须要通过 GPU 进程去做系统调用的。
至此,整个渲染工作完成。

Q&A

解析和渲染是一回事吗?

不是,解析是将 HTML 文件解析为 DOM 树和 CSSOM 树,渲染是指页面渲染出画面的整个过程。

且浏览器不是解析完整个 HTML 文件才开始渲染,而是解析完一部分,渲染一部分。

什么是 reflow(回流)、repaint(重绘)?

reflow 的本质的就是重新计算 layout 树,当进行了会影响 layout 树的操作后,需要重新计算 layout 树,就会引发 reflow。

为了连续多次的操作导致 layout 树的反复计算,浏览器会合并这些操作。当 JS 代码全部执行完成后再进行同意计算。所以,属性的修改造成的 reflow 是异步完成的。

也因为如此,当 JS 获取布局属性时,就可能无法获取到最新的布局信息。

在浏览器的权衡下,最终决定获取属性立即 reflow。


repaint 的本质就是重新根据分层信息计算了绘制指令。

当改动了可见样式后,就需要重新计算,引发 repaint。

由于元素的布局信息也属于可见样式,所有 reflow 一定会引发 repaint。

什么是硬件加速?

当触发生成了一个合成层,浏览器会尝试优化这段动画。

浏览器会将所有的纹理 (texture) 传输到 GPU,而不是对每一帧上的像素进行光栅化。GPU 非常擅长执行此类基于纹理的转换。

因此,我们就会得到非常流畅,高性能的动画,这称为 硬件加速

有趣的是,GPU 和 CPU对页面的渲染效果略有不同。当 CPU 将渲染任务交给 GPU 时,有时会看到页面动画有一些轻微的抖动。

此时,will-change 就非常适合称为 GPU 和 CPU 之间的桥梁。通过 will-change 属性,提前告知浏览器,让浏览器做好准备,设置 will-change 也是一种强制开启硬件加速的方式。

为什么 transform 效率高,对性能好?

因为 transform 既不会造成 reflow 也不会造成 repaint,它影响的是渲染流程的最后一个阶段 draw

由于 draw 在合成线程中,所以 transform 的变化几乎不会影响渲染主线程。反之,渲染主线程无论再忙,也不会影响 transform 的变化。即使在 JS 执行错误或卡死的情况,样式的更新或动画也不会受影响。

相关推荐
护国神蛙1 天前
给你一个页面如何定时刷新
前端·javascript·浏览器
kite01218 天前
浏览器工作原理05 [#] 渲染流程(上):HTML、CSS和JavaScript是如何变成页面的
浏览器
mpr0xy8 天前
React Router 中 navigate 后浏览器返回按钮不起作用的问题记录
javascript·react.js·浏览器·路由
北京_宏哥9 天前
🔥《刚刚问世》系列初窥篇-Java+Playwright自动化测试-17- 如何优雅地切换浏览器多窗口(详细教程)
java·前端·浏览器
musashi10 天前
学会这招,可以录制所有 web 端的视频(附完整实现代码)
前端·javascript·浏览器
不一样的少年_10 天前
🚨 别再乱用will-change了!前端翻车的"性能优化"陷阱
前端·浏览器
codeAlwaysPass14 天前
《探索浏览器底层并实现简易浏览器 -- 第一章:请求和响应》
浏览器
魔云连洲21 天前
浏览器强缓存还未过期,但服务器资源已经变了怎么办?
前端·缓存·浏览器
打小就很皮...1 个月前
浏览器存储 Cookie,Local Storage和Session Storage
前端·缓存·浏览器
小妖6661 个月前
chrome 浏览器怎么不自动提示是否翻译网站
浏览器