1 - 宏观视觉上的浏览器
1.1 - Chrome 架构:仅仅打开了 1 个页面,为什么有 4 个进程
打开浏览器的任务管理器,有时候会很好奇为什么只开了一两个标签页面,但是进程却那么多
我们先看看进程和线程的区别
进程和线程
进程是程序运行的实例,启动程序的时候,操作系统为它分配一块内存,用来存放代码,运行中的数据和一个执行任务的主线程。我们把这套环境称之为进程
线程由进程启动和管理,依附于进程,主要执行任务。
看看一个进程里面单线程和多线程处理问题的示例图
多线程可以并行运算提高效率,是不是感觉开启多线程是不错的选择
总结看看线程和进程关系:
- 进程任一一个线程崩溃的话,整个进程都会崩溃
- 线程共享进程中的数据
- 进程关闭后,所有占用的资源都会回收
- 进程之间相互独立,彼此之间数据是严格独立的,即使挂了也不会影响其他进程
单进程浏览器时代
单进程浏览器:所有功能模块都运行在同一个进程中,包括网络,插件,JavaScript 运行环境,渲染引擎和页面,如下图,是一种单进程多线程的模式
这种模式有很多问题:
- 不稳定:渲染引擎,或者一些第三方的插件不稳定崩溃的话,会导致整个浏览器进程崩溃
- 不流畅:所有页面的渲染模块,JavaScript 运行环境和插件运行在一起,如果一个线程在运行一个高内存的任务,其他线程可能都无法得到执行了,浏览器会失去响应,造成卡顿(这里想到了时间循环系统)。另外我们的页面经常也会有一些内存没有回收的情况,如果只是关闭页面,即关闭了一个线程,不能完全回收内存,那么用的越久内存占用率也越高,浏览器越来越慢
- 不安全:这块感觉是因为进程内所有线程的内存是共享的,第三方插件可能通过一些手段攻击或者窃取信息导致的安全问题
以前经常存在的一个场景就是,一个页面崩溃了,导致所有浏览的页面都崩溃了,非常崩溃
多进程浏览器时代
早期多进程浏览器架构
前面说到的单进程浏览器时代,指的是 07 年前的,看下 08 年 谷歌发布的一个结构图
这里页面运行在单独的进程内,插件也在单独的进程内,他们通过 IPC 进行通信
- 解决不安全问题:现在进程是相互隔离了,现在一个页面或者插件崩溃了,都不会影响到其他插件和页面
- 解决不流畅问题:JavaScript 运行在单独的渲染进程中,如果 JavaScript 阻塞了渲染进程,也只会影响当前页面,不会影响其他页面。而内存泄漏问题也会随着渲染进程的关闭而得到释放
- 解决不安全问题:采用多进程的话我们可以加入安全沙盒,沙盒内的程序运行的时候不能给计算机硬盘写入任何数据,也读取不到敏感位置的信息和数据。现在给页面和插件加上了安全沙盒他们即使执行了恶意的程序,也无法获取到系统的一些敏感权限
目前多进程浏览器架构
现在包括了
- 浏览器主进程(browser):界面显示,用户交互,子进程的管理,存储能力
- GPU 进程:初衷是实现 css3 动画效果,后面页面和 UI 界面都采用 GPU 绘制,这使得它成为一个普遍的需求,所以也引入了一个单独的 GPU 进程
- 网络进程(network):负责网络资源加载
- 多个渲染进程:把 html css js 转化为用户可操作的页面,排版引擎 blink 和 JavaScript 引擎 V8 都运行在这个进程里面,每一个页面 tab 就开通一个渲染进程
- 多个插件进程:插件容易崩溃,独立一个进程进行管理
和我们开篇的截图很像
我们打开了一个页面,但是也必须要有网络进程,浏览器主进程,GPU 进程和这个页面的渲染进程,以及安装了的插件进程。当然也存在一些新的问题
- 更高的资源占用:每个进程都包括公共的基础结构副本(比如 JavaScript 运行环境),导致更多的内存资源消耗
- 更复杂的体系架构:模块之间耦合度高,扩展性差,新需求提出的时候,添加新功能非常复杂
未来面向服务的架构
16 年谷歌提出了面向服务的架构(Services Oriented Architecture,简称 SOA)
- 原来的各个模块都会重构独立成一个服务
- 每个服务独立运行在进程中
- 服务需要定义好接口,通过 IPC 通信
- 构建一个更加内聚,松耦合,易于维护和扩展的系统
- 致力于更加简单,稳定,高速和安全
最终把 UI,数据库,文件,设备,网络等模块重构为基础服务,看看现在的模型图:
也提供了灵活的弹性架构,性能好的设备上用多进程的方式运行基础服务,如果性能受限,则把这些基础服务合并到一个进程中,节省内存的占用,如下图
总结
- 最初的浏览器都是单进程的,它们不稳定、不流畅且不安全,引入了多进程架构,解决了这些遗留问题
- 在更多业务场景,如移动设备、VR、视频等需求下,为了支持这些场景,原来的多进程架构体系变得越来越复杂
- 进行架构的重构,最终选择了面向服务架构(SOA)形式
1.2 - TCP 协议:如何保证页面文件能被完整送达浏览器
一个文件会拆成很多数据包来传输,这些数据包很大概丢失或者出错,如何保证页面正常送到浏览器。
一个数据包的旅程
互联网其实是一系列协议和理念构成的体系架构
协议是众所周知的规则和标准,各方都遵守,通信都畅通无阻
互联网的数据是通过很多小的数据包进行传输的,比如听音乐,不会一次性一个大的数据包传输过来,而是接收一个个的数据包,进行播放的
IP:数据包如何送达主机
数据包在网络上传输就要遵守网际协议(Internet Protocol),即 IP 标准
网络上的在线设备都有一个唯一地址,它是一串数字
和我们家庭地址是类似的,物流系统知道了我们的地址,就可以准确送达
计算机的地址就叫做 IP 地址,访问网站其实就是计算机向另外一台计算器请求获得信息。
首先数据包打上 主机 B 的 IP 地址信息,这是寻址信息;也会打上 A 的 IP 地址信息,才能确保信息能够回传到正确的位置。它们都放到了 IP 头的数据结构里面, IP 头是 IP 数据包的头部信息,包括了 IP 版本,源 IP 地址,目标 IP 地址,生存时间等等信息
看下下面一个简单的网络结构示意图(有四层互联网结构,七层互联网结构等等,看看划分的标准了)
我们看下一个数据包从 A 到 B 是如何进行的
- 上层应用把数据包交给网络层
- 网络层把 IP 头加到数据包上,组成 IP 数据包,交给底层
- 底层把数据包通过物理网络传送给主机 B
- 数据包传送到主机 B 的网络层,主机 B 拆开数据包的 IP 头信息,把拆开后的数据包交给应用层,最终达到了主机 B 的上层应用了
UDP:主机如何把数据包交给应用
IP 数据包是很底层的协议,只把数据包传送到对方电脑,那么如何把数据包传送给应用程序?
基于 IP 协议,要开发和应用程序打交道的协议,最常见的就是 用户数据包协议(User Datagram Protocol)了,简称 UDP
UDP 需要的一个重要信息就是端口号,想要访问网络的程序都需要有这个信息,通过它我们知道要把数据发送到哪个应用程序
IP 通过 IP 地址信息把数据包发送到指定的电脑,而 UDP 通过端口信息把数据包发送到指定的程序
端口号信息会被放到 UDP 头 里面,与原始数据信息组装成 UDP 数据包,UDP 头中也包含了 源端口信息
下图新增了传输层:
我们修改和扩展下前面的流程
- 上层应用把数据包交给传输层
- 网络层把 UDP 头加到数据包上,组成 UDP 数据包,交给网络层
- 网络层把 IP 头加到数据包上,组成 IP 数据包,交给底层
- 底层把数据包通过物理网络传送给主机 B
- 数据包传送到主机 B 的网络层,主机 B 拆开数据包的 IP 头信息,把拆开后的数据包交给传输层
- 在主机 B 的传输层,主机 B 拆开数据包的 UDP 头信息,把拆开后的数据包交给应用程序
发送数据以后,有可能出现错误,UDP 可以校验数据是否正确,但是不提供重发的机制,UDP 发送以后也不能确定是否到达了目的地,UDP 也不知道如何组装数据包还原成原来完整的大文件
虽然它不能保证数据的可靠性,但是传输速度非常快,在线直播和互动游戏等在不要求数据完整性但是关注速度的场景下,会选择使用 UDP
TCP:数据如何完整的送到应用
浏览器,邮箱等都要求数据传输的可靠性,我们一般使用 传输控制协议 (Transmission Control Protocol),简称 TCP,它是一种面向连接,可靠的,基于字节流的传输层通信协议
- 对于数据包丢失,提供重传机制
- 有数据包排序机制,保证乱序的数据包可以重新组合成完整的文件
TCP 的流程,和 UDP 很像,但是 TCP 的头信息中多包含了包的序号,用于排序组装数据包
我们看看 TCP 是如何重传机制和数据包排序功能的
下面是 TCP 传输的生命周期,包括了建立连接,数据传输,断开连接三个阶段
- 建立连接:通过三次握手,建立客户端和服务端的连接,TCP 提供面向连接的通信传输。面向连接的意思是,数据通信前做好两端的准备工作。三次握手指的是建立一个 TCP 连接的时候,客户端和服务端要发送三个数据包确认连接的建立
- 数据传输:接收端需要对每个数据包进行确认操作,即接收到数据包以后要向发送方发送确认数据包。所以如果发送发发送了数据包以后,一定时间内没有收到确认数据包,判定数据丢失,并触发重发机制。一个大文件在传输过程会拆成很多小的数据包,到达接收方以后,会按照 TCP 头信息的序号把它们进行排序,组成完整的数据
- 断开连接:数据传输完毕后,通过四次挥手来确保双方断开连接
TCP 增加了数据包的传输数量,会让传输速度下降,因为需要三次握手的数据包确认,以及数据包校验机制,也会增加数据包的数量
总结
- 互联网数据通过数据包传输,传输过程容易丢失或者出错
- IP 负责把数据包传送到主机
- UDP 负责把数据包传送到应用程序
- TCP 保证传送的数据的完整性,分为建立连接,传输数据,断开连接三个阶段
- 了解 TCP 协议,是为了后续学习 HTTP 做好准备,才能更好知道 HTTP 的实际功能和局限性,也能更好的理解为什么要推出 HTTP2,推出 QUIC 协议(未来的 HTTP3),所有新技术的推出其实都是为了解决原有协议或者技术存在的问题和痛点。另外这也是一个循序渐进的学习过程,后面很多知识的融会贯通,也是水到渠成的事情了
1.3 - HTTP 请求流程:为什么很多站点第二次打开速度会很快
HTTP 是一个允许浏览器向服务器请求资源的协议,也是我们 WEB 的基础,用它来获取不同类型的文件,比如 JavaScript HTML CSS 图片和视频等等
- 为什么第一次打开网页很慢,第二次就很快了,为什么呢
- 登陆了一次以后,后面就不用登陆了,为什么呢
这小结主要主要分析 HTTP 的请求和响应过程,最后这两个问题也自然可以揭开了
浏览器发起 HTTP 请求
我们在浏览器敲入 www.qq.com 会发生什么呢,看看完整的过程
构建请求
浏览器构建下面的请求,准备发起网络请求
1
GET /index.html HTTP1.1
查找缓存
发起真正的网络请求,浏览器会先看看是否有缓存,即本地保存的资源副本,以便下次直接使用
如果本地有缓存,会直接返回资源副本给浏览器,不会再去发起网络请求。这不仅缓解了服务器的压力,也提升了性能,更快的获取到资源
如果没有缓存就要进入到网路请求阶段了
准备 IP 地址和端口
HTTP 是应用层协议,封装了请求的文本信息,TCP/IP 传输 协议让数据能在网络上传输,浏览器先需要通过 TCP 和服务器建立连接,而 HTTP 的内容传输是在 TCP 的数据传输阶段进行的,我们看看下图
所以:
- HTTP 网络请求第一步是浏览器与服务器建立 TCP 连接
- 那么建立 TCP 连接需要的头部信息有了吗?IP 地址和端口号?
- 我们如何获取 IP 地址和端口号,通过 URL 地址吗
数据包是通过 IP 地址识别目的机器的,它一般是一串纯数字,难以记住,所以有了域名
而把域名和 IP 联系起来,一一映射的服务就是域名系统(domain name system),简称 DNS
所以浏览器首先会请求 DNS 返回 IP 地址,而浏览器也会有 DNS 数据缓存服务,如果某个域名已经解析过了,浏览器会把结果缓存起来下次查询的时候直接使用
有了 IP 地址以后,端口号怎么获取呢?也是可以从 URL 地址上面获取,一般是 host:port
的形式,如果没有特别制定端口号的话,HTTP 的端口号就是 80
等待 TCP 队列
如今端口号和 IP 都有了,是否可以开始建立 TCP 连接了?
答案是不可以的,谷歌浏览器限制同一个域名同时最多只能 6 个 连接,换句话说如果同时发起超过 6 个的 TCP 连接,多出来的会处于 TCP 等待队列等待发起请求,否则发起 TCP 连接
建立 TCP 连接
三次握手建立 TCP 连接
发起 HTTP 请求
建立好 TCP 连接后,我们进入了数据传输阶段,我们看看浏览器是怎么发送 HTTP 请求的
请求行包含了请求方法,请求 URL,和 HTTP 版本号
常用的请求方法是 GET 方法,比如首页的 HTML 文件,CSS 文件等
另外一个方法是 POST 方法,一般会给服务器发送一些数据,比如用户登录的时候发送用户信息,这些是放在请求体里面的
另外也需要请求头信息,把一些基础的信息告诉服务器,比如浏览器的操作系统,版本信息,域名,cookie 等待
服务器处理 HTTP 请求
数据包到达了服务器,会根据请求的信息进行处理
返回请求
一般返回的信息如下图
返回了响应行,包括了协议版本和状态码
不是所有请求服务器都可以处理的,服务器会通过状态码来告诉浏览器处理的结果。比如常见的 200 代表处理成功,404 代表服务器上没有对应的资源
浏览器也会返回响应头,包括服务器的信息,返回数据的时间,类型和服务器需要客户端存储的 cookie 信息
之后会继续返回响应体数据,比如我们 HTML 的文件内容
断开连接
通常返回了信息,TCP 就会断开,不过如果浏览器或者服务器加入了下面的头部信息:
1
Connection:Keep-Alive
那么 TCP 在发送完整信息以后,还会继续保持连接状态,浏览器可以继续使用这个 TCP 连接发送请求。这样可以省去下次发起请求的建立连接的时间,提高资源的请求速度。比如我们网站中的图片一般都来自同一个服务器,可以一直复用同一个 TCP 连接
重定向
有时候我们打开一个网站,会自动跳转到另外一个地址,这是一个重定向的操作。比如下图
返回的状态码是 301,告诉浏览器我需要重定向到另外一个地址,包含在了响应头部 Location 字段里面,浏览器就会重新导航到这个地址
问题解答
为什么第二次打开速度快了很多
主要就是第一次打开以后,缓存了一些信息。主要是 DNS 信息和一些资源信息
我们看看资源的缓存是怎样的:
我们看下浏览器是怎么缓存数据的
第一次请求的时候我们是没有本地缓存的,所以需要向服务器发起请求获取资源。服务器的响应头部会有 Cache-Control 字段,浏览器解析完以后知道是否需要缓存这个资源,缓存多久。比如
1
Cache-Control:Max-age=2000
浏览器会缓存这个资源,这个资源缓存失效的时间 2000 秒
在 2000 秒时间内如果再请求这个资源的话,会直接返回缓存起来的资源副本
如果缓存存在,但是过期了,会向浏览器发送请求的时候在请求头上加上 If-None-Match 或者 ETAG 信息,然后服务器会判断资源是否过期了,是否需要更新
- 如果不需要更新,返回 304,告诉浏览器可以继续使用这个资源
- 否则会把最新的资源返回给浏览器,然后也会更新浏览器本地的缓存信息
所以第二次打开快很多的话,一个是使用的缓存的资源文件,另外一个是少了 DNS 查询的时间
登陆信息是怎么保持的
HTTP 本身是无状态的,那浏览器是怎么保持用户登陆信息的?
成功登陆后,服务器会发送一个 Set-Cookie 的响应头信息,浏览器接收到以后会把这些信息存储到本地的 cookie 中
再次发起请求的时候,浏览器会从本地 cookie 中获取到用户的登陆信息,并附加到请求头中
服务器会解析请求头中的登陆信息,判断用户的登陆信息是否有效,有效的话直接返回对应的资源
总结
下图是 HTTP 请求的示意图
1.4 - 导航流程:从输入 URL 到页面展示这中间发生了什么
经典面试题目,涉及到了网络,操作系统,Web 等一系列的问题。我们先在大局上看看整体的流程是怎样的:
需要各个浏览器的进程进行协作
- 浏览器主进程:负责用户的交互,子进程管理,和文件存储等
- 网络进程:面向渲染进程和浏览器主进程,提供网络资源加载功能
- 渲染进程:把加载回来的 HTML CSS JavaScript 资源解析为可显示和交互的页面
整个流程的主要步骤是这样的:
- 用户从浏览器主进程输入请求信息
- 网络进程发起 HTTP 请求
- 服务器响应 HTTP 请求后,浏览器主进程准备渲染进程
- 渲染进程准备好后,先要向渲染进程提交页面数据,称之为提交文档阶段
- 渲染进程接收到文档信息以后,开始解析页面和加载子资源,完成页面的渲染
用户输入
地址栏输入查询关键字,浏览器会判断是搜索内容还是 URL 请求
- 搜索内容的话用默认的搜索引擎,打开地址,进行搜索
- 如果是 URL,会加上协议,合成完成的请求地址
刚开始加载地址的时候,浏览器图标会进入 loading 的状态,这个时候还是原来的页面,因为需要等待提交文档的时间,页面才会替换
URL 请求过程
浏览器进程通过 IPC 告诉网络进程去发起真正的网络请求
网络进程先检查本地是否有缓存,有的话直接返回给浏览器进程,没有的话进入网络请求过程。通过 DNS 查询到 IP 地址,如果是 HTTPS 的话还要建立 TLS 连接。然后建立 TCP 连接。浏览器也会去组装请求行,请求头和请求体部分的信息,并把域名下的 cookie 附加到请求头中,然后向服务器发送构建的请求信息
服务器收到请求后,根据请求信息构建响应头,响应行和响应体,然后发送给网络进程,网络进程会解析响应头的信息,可能有不同的操作
如果是 301 302 说明服务器希望浏览器重定向到其他 URL 地址,网络进程会解析 Location 字段读取重定向的地址,发起新的 HTTP 请求,重头开始
如果返回的是 200,我们可以往下继续处理该请求了
浏览器通过响应头的 Content-Type 可以确定我们获取的资源的类型,然后知道怎么去处理和显示这个内容。比如通常首页是 text/html 类型,代表显示一个 HTML 文件,而 apk 包是 application/octet-stream 类型,该请求会被浏览器的下载管理器拦截,开始下载文件。否则我们就开始浏览器的渲染流程了
准备渲染进程
一般一个标签页面就是一个渲染进程,但是如果在同一站点的话,浏览器会把它们合并到同一个渲染。
同一站点的判断依据是:跟域名加上协议类型相同(所以二级域名,还有不同端口号的,都当作同一站点,和跨域同源策略不太一样)
准备好以后进入了提交文档的阶段
提交文档
这里的文档指的是 URL 请求的响应体内容
- 提交文档的消息是浏览器进程发起的,渲染进程收到提交文档的信息后,会和网络进程建立传输数据的管道
- 文档传输完毕以后,渲染进程会返回确认提交消息给浏览器进程
- 浏览器进程收到了确认提交的消息,更新浏览器信息包括浏览器的界面状态,比如安全状态,地址 URL,前进后退的历史状态,以及 Web 页面
比如更新了下面的内容:
所以输入了地址,还有一段时间显示的老得页面
渲染阶段
渲染进程开始解析页面和子资源的加载(具体的步骤在后面小结阐述)。一旦页面加载完成了,会发消息给浏览器进程,浏览器接收到这个信息以后就会停止图标上的加载动画。于是一个完整的页面就呈现完成了
总结:从输入 URL 到页面展示,这中间发生了什么
- 用户输入地址,回车
- 检查 URL 地址,组装协议等,构成完整的地址
- 浏览器进程通过 进程间通信 IPC 把 URL 请求发给网络进程
- 网络进程判断是否使用缓存
- 没有缓存的话,网络进程发起网络请求
- DNS 解析,获取 IP 地址
- 建立 TCP 连接
- 构建请求头信息
- 发送请求头信息
- 服务器返回,解析响应内容
- 网络进程解析响应内容
- 301/302 状态码,进行重定向,重复第 4 部
- 200 状态码,检查 Content-Type
- 字节流的话交给浏览器下载管理器下载文件
- HTMl 则告诉浏览器准备渲染进程
- 准备渲染流程
- 检查和之前打开过的渲染进程是否同一站点
- 同一站点,复用渲染进程
- 不同站点,开启新的渲染进程
- 检查和之前打开过的渲染进程是否同一站点
- 传输数据,更新状态
- 渲染进程准备好了以后,浏览器进程向渲染进程发起提交文档的信息,渲染进程和网络进程建立起数据传输的通道
- 渲染进程接收完数据以后,向浏览器进程发起确认提交的消息
- 浏览器进程接收到确认的消息以后,更新浏览器的状态
1.5 - 渲染流程:HTML、CSS 和 JavaScript 是如何变成页面的
浏览器是如果通过解析 HTML CSS JavaScript 文件,呈现出我们的网页的?
这节主要讲的就是渲染模块
- HTML: 由标记和文本组成,标记也就是标签,有自己的语义,浏览器会根据语义正确显示它们
- CSS: 层叠样式表,选择器和属性组成,可以把 HTML 的标签选择出来,再把属性应用到标签上
- JavaScript:使得页面 “动” 起来
渲染机制很复杂,化为为多个阶段,输入的 HTML 文件通过这些阶段最终输出像素,我们把这个流程称为渲染流水线,如下图
流水线有下面几个阶段
- 构建 DOM 树
- 样式计算
- 布局阶段
- 分层
- 绘制
- 分块
- 光栅化
- 合成
每个阶段我们重要要关注的方面:
- 每个阶段的输入内容
- 每个阶段的处理过程
- 每个阶段的生成内容
理解这三个方面可以让我们更好的理解这个过程
构建 DOM 树 🌲
浏览器无法直接理解 HTML,所以要转换成浏览器认识的 DOM 树
下图是构建 DOM 树的过程
输入是 HTML 的文本内容,处理过程是使用 HTML 解析器进行解析,最终输出 DOM 树
与 HTML 不一样的是,DOM 是树形结构保存在内存中的,也可以通过 JavaScript 进行修改
样式计算
主要是计算 DOM 结构每个节点的具体样式是什么
把 CSS 转成浏览器能理解的结构
CSS 主要有三个来源
- Link 引入的外部样式文件
- Style 标签写的样式
- 元素内嵌的 Style 样式属性
浏览器也无法理解 CSS,需要进行转换,转成浏览器认识的结构 - styleSheets
渲染引擎会把所有的 CSS 样式转成 styleSheets 结构中的数据,并具备了修改和查询的能力。通过 document.styleSheets 可以看到所有的 styleSheets 值
转化属性中的属性值,使其标准化
属性值有比如 2em bold blue 等等,不容易被渲染引擎理解,所以要转成浏览器所能理解的标准化计算值,这就是标准化过程,比如下图:
计算出 DOM 中每个节点的具体样式
这里主要涉及到了 CSS 的继承规则和层叠规则
首先讲讲继承,代表的是每个节点都包含了父节点的样式,根据继承关系计算每个节点的样式,如
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
body {
font-size: 20px;
}
p {
color: blue;
}
span {
display: none;
}
div {
font-weight: bold;
color: red;
}
div p {
color: green;
}
最后会变成这样:
第二个是层叠的规则,定义了如何合并来自多个源的属性值的算法
最终计算出来的结果放在了 ComputedStyle 里面
布局阶段
我们有了 DOM 树和样式信息,我们还要知道它在页面上的几何位置信息。接下来就是要计算出 DOM 树可见元素的几何位置了,这个阶段叫做布局
这个阶段浏览器会生成布局树,以及进行布局运算
创建布局树
DOM 树里面有一些不可见元素,我们需要把它们去掉,生成一个可见元素布局树,我们看看布局树的生成过程:
它主要做的事情是遍历 DOM 节点,把可见节点加到布局树中
布局计算
我们有了一颗完整的布局树,接下来就是计算布局树节点的位置了。我们在后面详细讨论下。
在布局操作的时候,会把布局运算的结果重新写到布局树中,布局树既是输入内容也是输出内容,这是布局阶段一个不合理的地方,并没有把输入内容和输出内容清晰区分。Chrome 团队也在重构这部分的代码,下一代的布局系统叫做 LayoutNG,试图清晰区分输出内容和输入内容,让布局算法更加简单
阶段总结
下图是前面这部分的一个简单总结
- 浏览器不能直接识别 HTML 代码,所以需要转化成它认识的 DOM 树
- 生成 DOM 树后,依据 CSS 样式,计算所有 DOM 节点的样式
- 计算 DOM 树节点的布局信息,保存在 布局树中,进行后续的布局计算
分层
有了布局树是否可以开始绘制了?答案是否定的
页面经常还有复杂的 3D 变换,页面滚动, z-index 的 z 轴排序,为了更方便的实现它们,渲染引擎需要为这些节点生成特定的图层,生成一个对应的图层树 LayerTree。这些图层的组合最终形成我们看到的页面
我们看看图层树和布局树之间的关系:
布局树中大部分没有特殊属性,就属于父元素的图层
什么情况下会单独形成一个图层?
拥有层叠上下文属性的元素被提升为单独的一个图层
页面是二维的,层叠上下文属性让 HTML 元素具备了三维的概念,HTML 元素按照自身优先级分布在垂直与这个平面 z 轴上,如下图:
这里涉及到几个属性:
- 明确定位属性的元素,比如 position: fixed
- 有透明属性的元素
- 有 CSS 滤镜的属性元素
- 有 z-index 属性的元素
需要剪裁的元素也会提升为单独的一个图层
比如我们有一个 200px * 200px 的 DIV 属性,但是它里面的文字内容很多,超出了范围,这就产生了裁剪,
出现裁剪的时候渲染引擎会为文字部分单独创建一个层,如果出现滚动条,也会提升为单独的层。比如刚刚提到的这个例子
图层绘制
渲染引擎会为每个图层进行绘制
试想一下,如果给你一张纸,让你先把纸的背景涂成蓝色,然后在中间位置画一个红色的圆,最后再在圆上画个绿色三角形
我们就会分三步
- 制蓝色背景
- 在中间绘制一个红色的圆
- 再在圆上绘制绿色三角形
其实浏览器绘制的时候,差不多也是这样的步骤。每个图层的绘制拆分成很多小的绘制指令,按照顺序组成一个带绘制的列表,如下图:
绘制指令的列表其实很简单,都是执行一个个简单的绘制。而绘制元素的话就是一系列的绘制指令集合
栅格化
绘制列表只是记录绘制的顺序和指令,但是实际上的绘制是通过渲染引擎中的合成线程来完成的。我们可以通过下图看看它们之间的关系
当图层的绘制列表准备好了,主线程会把绘制列表交给合成线程。合成线程是怎么工作的呢
我们先来理解下什么是 视口 viewport,页面一般很大,我们当前能看到的那部分页面就叫视口。要绘制所有的图层内容是没必要的,开销非常大
所以合成线程会把页面分成图块,一般是 256*256
或者 512*512
合成线程会按照视口附近的图块优先生成块图,实际生成位图是栅格化来操作的。栅格化的意思就是把块图生成位图,图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池子,所以图块的栅格化都在这个线程池里面进行:
栅格化过程有 GPU 加速完成,使用 GPU 生成位图的过程叫做快速 栅格化,或者 GPU 栅格化,生成的位图保存在 GPU 内存里面
而 GPU 操作是在 GPU 进程里面的,如果 栅格化 使用到了 GPU,最终位图的生成是在 GPU 进程的,也就是跨进程操作。继续扩展上面的图片:
渲染进程把生成图块的指令交给 GPU 进程,GPU 执行生成位图的指令,然后保存在 GPU 内存中
合成和显示
一旦所有的图层都光栅化以后,合成线程会发起绘制图块的指令 DrawQuard,然后交给浏览器进程
浏览器进程有一个叫做 vzi 的组件,用来接收合成线程传过来的 DrawQuard 指令,将绘制的页面内容绘制到内存中,最后将内存显示在屏幕上
渲染流水线大总结
- 渲染进程把 HTML 内容转换成浏览器识别的 DOM 树
- 渲染进程把 CSS 样式表转换成浏览器理解的 styleSheets,计算出 DOM 节点的样式
- 创建布局树,计算元素的布局信息
- 对布局树进行分层,生成布局树
- 每个图层生成绘制列表,提交到合成线程中处理
- 合成线程把图层分成图块,在光栅话线程池中把图块转换成位图
- 合成线程把绘制位图的命令 DrawQuard 发送给浏览器
- 浏览器根据绘制命令,生成页面,显示在浏览器上
相关理念
重排,重绘,合成概念
更新了元素的几何属性
比如我们改变了一个元素的高度,会触发浏览器的重新布局,以及一系列的子阶段,这个就叫做重排
重排需要更新完整的渲染流程,开销最大
更新了元素的绘制属性
如果我们只是修改了颜色,布局阶段不会改变,因为元素的几何属性并没有改变,直接进入到了绘制的阶段,省去了布局和分层的阶段,执行效率币重排高点
直接是合成阶段
如果是一个不需要重新布局和重新绘制的属性呢?浏览器跳过布局和绘制,只执行了后续的合成操作
比如我们给一个元素增加 transform 属性实现动画的效果,这样避开了重绘和重排阶段,在非主线程上直接执行合成动画的操作,效率也是最高的。它不在主线程上合成,并没有占用主线程的资源,也避开了重绘和重排,合成效率大大提升了
优化思考
- 我们把触发重绘和重排的操作尽可能放在一起,比如修改高度和 margin 值分开的话会触发两次,可以把它们放在一起进行操作,放在一起的操作其实就是可以修改类名,属性写在类里面
- 通过虚拟 DOM 计算出操作的差异,一起提交给浏览器执行
- 创建 createDocumentFragment,汇总 DOM 的变化操作,减少重绘重排的次数