干货总结!浏览器渲染原理

130 阅读7分钟

先从一道经典的面试题说起:从浏览器地址栏输入 url 到请求返回发生了什么?

  1. 浏览器根据 DNS 服务器得到域名的 IP 地址
  2. 向这个 IP 的机器发送 HTTP 请求
  3. 服务器收到、处理并返回 HTTP 请求
  4. 浏览器接收到服务器返回的内容

返回的内容如下:其实就是一堆 HMTL 格式的字符串,因为只有 HTML 格式浏览器才能正确解析,这是 W3C 标准的要求。接下来就是浏览器的渲染过程。

  1. 解析HTML,生成DOM树,解析CSS,生成CSSOM树
  2. 将DOM树和CSSOM树结合,生成渲染树(Render Tree)
  3. Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)
  4. Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
  5. Display: 将像素发送给GPU,最后通过调用操作系统Native GUI的API绘制,展示在页面上。

如果觉得抽象可以借助以下例子理解:

  • 浏览器渲染过程 = 盖房子的整个流程
  • 解析 HTML(打地基):从 HTML 中构建 DOM 树。
  • 解析 CSS(装饰蓝图):从 CSS 中构建 CSSOM 树。
  • 合成 Render Tree(房间设计图):将 DOM 和 CSSOM 结合,生成 Render Tree。
  • 布局(安排家具):确定每个元素的具体位置和大小。
  • 绘制(粉刷墙壁):根据 Render Tree 绘制页面的像素。
  • 合成和显示(最终交付):将图层合成并渲染到屏幕。

note:

1、无论通过什么方式影响了元素的几何信息(元素在视口内的位置和尺寸大小),浏览器需要重新计算元素在视口内的几何属性,这个过程叫做回流,也叫重排。

2、所谓重绘就是:通过构造渲染树和重排(回流)阶段,我们知道了哪些节点是可见的,以及可见节点的样式和具体的几何信息(元素在视口内的位置和尺寸大小),接下来就可以将渲染树的每个节点都转换为屏幕上的实际像素,这个阶段就叫做重绘。

3、如何减少重排和重绘?

  • 最小化重绘和重排,比如样式集中改变,使用添加新样式类名 .class 或 cssText 。
  • 批量操作 DOM,比如读取某元素 offsetWidth 属性存到一个临时变量,再去使用,而不是频繁使用这个计算属性;又比如利用 document.createDocumentFragment() 来添加要被添加的节点,处理完之后再插入到实际 DOM 中。
  • 使用 absolute 或 fixed 使元素脱离文档流,这在制作复杂的动画时对性能的影响比较明显。
  • 开启 GPU 加速,利用 css 属性 transform 、will-change 等,比如改变元素位置,我们使用 translate 会比使用绝对定位改变其 left 、top 等来的高效,因为它不会触发重排或重绘,transform 使浏览器为元素创建⼀个 GPU 图层,这使得动画元素在一个独立的层中进行渲染。当元素的内容没有发生改变,就没有必要进行重绘。
  • 使用 CSS 动画代替 JavaScript 动画
  • Lazy Load(懒加载)和 Virtual List(虚拟列表):对于大数据渲染,按需加载数据或只渲染可见部分。

在渲染的过程中,遇到JS怎么办?

渲染过程中,如果遇到script就停止渲染,执行 JS 代码。因为浏览器有GUI渲染线程与JS引擎线程,为了防止渲染出现不可预期的结果,这两个线程是互斥的关系。JavaScript的加载、解析与执行会阻塞DOM的构建,也就是说,在构建DOM时,HTML解析器若遇到了JavaScript,那么它会暂停构建DOM,将控制权移交给JavaScript引擎,等JavaScript引擎运行完毕,浏览器再从中断的地方恢复DOM构建。

为什么呢?原因在于JS代码可能会改变DOM的结构(比如执行document.write()等API)

也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer(延迟) 或者 async(异步) 属性。

浏览器架构

不同的浏览器使用不同的架构,下面主要以Chrome为例,介绍浏览器的多进程架构。

在Chrome中,主要的进程有4个:

  • 浏览器主进程 (Browser Process):负责浏览器的TAB的前进、后退、地址栏、书签栏的工作和处理浏览器的一些不可见的底层操作,比如网络请求和文件访问。
  • 渲染进程 (Renderer Process):负责一个Tab内的显示相关的工作,也称渲染引擎。
  • 插件进程 (Plugin Process):负责控制网页使用到的插件
  • GPU进程 (GPU Process):负责处理整个应用程序的GPU任务

这4个进程之间的关系是什么呢?它们是如何分工协作的呢?

  1. 首先,当我们是要浏览一个网页,我们会在浏览器的地址栏里输入URL,这个时候Browser Process会向这个URL发送请求,获取这个URL的HTML内容,
  2. 然后将HTML交给Renderer ProcessRenderer Process解析HTML内容,解析遇到需要请求网络的资源又返回来交给Browser Process进行加载,同时通知Browser Process,需要Plugin Process加载插件资源,执行插件代码
  3. 解析完成后,Renderer Process计算得到图像帧,并将这些图像帧交给GPU ProcessGPU Process将其转化为图像显示屏幕。(将像素发送给GPU,最后通过调用操作系统Native GUI的API绘制,展示在页面上)

为什么需要 deferasync

为了解决传统 <script> 的阻塞问题,提升页面加载性能,HTML 引入了 deferasync 属性。

异步加载和直接加载有何区别

  1. script :会阻碍 HTML 解析,只有下载好并执行完脚本才会继续解析 HTML。
  2. async script :解析 HTML 过程中进行脚本的异步下载,下载成功立马执行,有可能会阻断 HTML 的解析。
  3. defer script:完全不会阻碍 HTML 的解析,解析完成之后再按照顺序执行脚本

共同点:都是去异步加载外部的JS脚本文件,它们都不会阻塞页面的解析

属性加载方式HTML 解析执行时间执行顺序
无属性阻塞加载暂停,直到脚本加载和执行完成立即执行,阻塞 HTML 解析按照书写顺序执行
defer异步加载(并行加载)与 HTML 解析同时进行HTML 解析完成后执行按照书写顺序执行
async异步加载(并行加载)与 HTML 解析同时进行脚本加载完成后立即执行(可能打断 HTML 解析)加载完成即执行,顺序不确定

defer 的使用场景:

  • 页面主要逻辑的 JavaScript 文件。
  • 脚本需要等待完整的 HTML 结构准备好(如操作 DOM 元素)。
  • 脚本之间有依赖关系,必须按顺序执行
<script src="main.js" defer></script>
<script src="utils.js" defer></script>

async 的使用场景:

  • 独立脚本,比如广告、统计分析工具。
  • 脚本不依赖 HTML 结构或其他脚本。
  • 对执行顺序没有要求。
<script src="main.js" defer></script>
<script src="utils.js" defer></script>

为什么异步加载可以与 HTML 解析同时进行?

要结合浏览器架构来理解。

网络线程的作用

  • 浏览器使用独立的网络线程来加载脚本文件。
  • 当遇到 <script> 带有 deferasync 时:
  • 主线程继续解析 HTML。
  • 网络线程负责异步加载脚本。

任务队列机制

  • 加载完成的脚本会被放入浏览器的任务队列中。对于 async :加载完成的脚本会尽快被主线程执行,无需等待 HTML 解析完成。
  • 对于defer:脚本会在 HTML 解析完成后按照顺序依次执行。

性能优化策略

  • JS优化: <script>标签加上 defer属性 和 async属性 用于在不阻塞页面文档解析的前提下,控制脚本的下载和执行。 defer属性: 用于开启新的线程下载脚本文件,并使脚本在文档解析完成后执行。 async属性: HTML5新增属性,用于异步下载脚本文件,下载完毕立即解释执行代码。
  • CSS优化:<link>标签的 rel属性 中的属性值设置为 preload 能够让你在你的HTML页面中可以指明哪些资源是在页面加载完成后即刻需要的,最优的配置加载顺序,提高渲染性能