目录
Chapter 1 宏观视角下的浏览器
01 | 仅仅打开了1个页面,为什么有4个进程?
进程和线程的关系:
- 进程中的任意一个线程出错,都会导致整个进程的崩溃
- 线程之间可以共享进程中的数据
- 当一个进程关闭之后,os会回收进程所申请的全部资源
- 进程之间的内容相互隔离
单进程浏览器时代
单进程浏览器,顾名思义就是所有的功能、模块都运行在一个进程里,在2007年以前,市面上所有的浏览器都是单进程的。毫无疑问肯定很不好使。它会出现不稳定、不流畅、不安全等状况。
- 不稳定
我们知道,进程中任意一个线程出错,都会导致整个进程的崩溃。所以如果任意一个插件或者渲染引擎出了问题,就会导致整个浏览器崩溃掉。 - 不流畅
单进程导致同一时刻只有一个模块可以执行,如果碰巧你这个模块写成了死循环,那后果就可想而知了。
除了这个原因外,页面的内存泄漏也是单进程变慢的一个重要原因。通常浏览器的内核都是非常复杂的,运行一个复杂点的页面再关闭页面,会存在内存不能完全回收的情况,这样导致的问题是使用时间越长,内存占用越高,浏览器会变得越慢。
多进程浏览器时代
早期
- 解决不稳定:
进程之间相互隔离,当一个页面或者一个插件崩溃时,影响的只是他自己,并不会影响到浏览器和其他页面 - 解决不流畅:
JavaScript 也是运行在渲染进程中的,所以即使 JavaScript 阻塞了渲染进程,影响到的也只是当前的渲染页面,而并不会影响浏览器和其他页面,因为其他页面的脚本是运行在它们自己的渲染进程中的。所以当我们再在 Chrome 中运行上面那个死循环的脚本时,没有响应的仅仅是当前的页面。
而对于内存泄漏:当关闭一个页面时,整个渲染进程也会被关闭,之后该进程所占用的内存都会被系统回收,这样就轻松解决了浏览器页面的内存泄漏问题。 - 解决不安全:
采用多进程架构的额外好处是可以使用安全沙箱,你可以把沙箱看成是操作系统给进程上了一把锁,沙箱里面的程序可以运行,但是不能在你的硬盘上写入任何数据,也不能在敏感位置读取任何数据,例如你的文档和桌面。Chrome 把插件进程和渲染进程锁在沙箱里面,这样即使在渲染进程或者插件进程里面执行了恶意程序,恶意程序也无法突破沙箱去获取系统权限。
目前
分析下进程的功能
- 浏览器主进程:主要负责页面显示、用户交互、子进程管理、存储
- 渲染进程:将三件套转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
- GPU进程:绘制网页、chrome的ui界面
- 网络进程:负责页面的网络资源加载
- 插件进程:负责插件的运行,因为插件容易崩溃,所以用插件进程来隔离插件,保证插件崩溃了也不会影响页面和浏览器。
那么我们现在知道了,仅仅打开了 1 个页面,为什么有 4 个进程?因为打开 1 个页面至少需要 1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程,共 4 个;如果打开的页面有运行插件的话,还需要再加上 1 个插件进程。
but 多进程浏览器就没有缺点吗?当然不是了,它的问题在于,占用了更高的资源,体系架构也更复杂了。
未来面向服务的架构
为了解决这两个问题,2016年,chrome官方团队使用SOA(面向服务的架构)设计了新的架构
在如今多进程浏览器时代下,偶尔也会出现单个页面卡死最终崩溃导致所有页面崩溃的情况,这是因为:
通常情况下是一个页面使用一个进程,但是如果几个页面符合同一站点,那么他们将被分配到一个渲染进程里面去。所以,这种情况下,一个页面崩溃了,会导致同一站点的页面同时崩溃,因为他们使用了同一个渲染进程。那为什么要让他们跑在一个进程里面呢?因为在一个渲染进程里面,他们就会共享JS的执行环境,也就是说A页面可以直接在B页面中执行脚本。因为是同一家的站点,所以是有这个需求的。
02 | TCP协议:如何保证页面文件能被完整送达浏览器?
首先要了解的:互联网中的数据是通过数据包来传输的。如果发送的数据很大,那么该数据就会被拆分为很多小数据包来传输。比如你现在听的音频数据,是拆分成一个个小的数据包来传输的,并不是一个大的文件一次传输过来的。
2.1 IP:把数据包送达目的主机
IP就是Internet Protocol – 网络协议
计算机的地址就称为 IP 地址,访问任何网站实际上只是你的计算机向另外一台计算机请求信息。
IP 头是 IP 数据包开头的信息,包含 IP 版本、源 IP 地址、目标 IP 地址、生存时间等信息。
2.2 UDP:把数据包送达应用程序
IP是非常底层的协议,它只负责把数据包传送到对方的电脑上,具体要传给哪个程序是由UDP(User Datagram Protocol)(用户数据包协议决定的)
UDP 中一个最重要的信息是端口号,端口号其实就是一个数字,每个想访问网络的程序都需要绑定一个端口号。通过端口号 UDP 就能把指定的数据包发送给指定的程序了,所以IP 通过 IP 地址信息把数据包发送给指定的电脑,而 UDP 通过端口号把数据包分发给正确的程序。和 IP 头一样,端口号会被装进 UDP 头里面,UDP 头再和原始数据包合并组成新的 UDP 数据包。UDP 头中除了目的端口,还有源端口号等信息。
在使用 UDP 发送数据时,有各种因素会导致数据包出错,虽然 UDP 可以校验数据是否正确,但是对于错误的数据包,UDP 并不提供重发机制,只是丢弃当前的包,而且 UDP 在发送之后也无法知道是否能达到目的地。
虽说UDP 不能保证数据可靠性,但是传输速度却非常快,所以 UDP 会应用在一些关注速度、但不那么严格要求数据完整性的领域,如在线视频、互动游戏等。
2.3 TCP:把数据完整地送达应用程序
对于浏览器请求,或者邮件这类要求数据传输可靠性的应用,如果使用 UDP 来传输会存在两个问题:
- 数据包在传输过程中容易丢失;
- 大文件会被拆分成很多小的数据包来传输,这些小的数据包会经过不同的路由,并在不同的时间到达接收端,而 UDP 协议并不知道如何组装这些数据包,从而把这些数据包还原成完整的文件。
为了解决这两个问题,我们引入了 TCP (Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
-
对于数据包丢失的情况,TCP 提供重传机制;
-
TCP 引入了数据包排序机制,用来保证把乱序的数据包组合成一个完整的文件。
-
三次握手建立连接:
这个阶段是通过“三次握手”来建立客户端和服务器之间的连接。- TCP 提供面向连接的通信传输。
- 面向连接是指在数据通信开始之前先做好两端之间的准备工作。
- 三次握手是指在建立一个TCP连接时,客户端和服务器总共要发送三个数据包以确认连接的建立。
-
传输数据:
在此阶段,接收端需要对每个数据包进行确认操作,在接收到数据包后,要向发送端发送确认数据包。所以如果发送端发送了一个数据包,但是却没有收到回复,那么代表这个数据包丢失了。然后就会触发发送端的重发机制。
同样,一个大的文件在传输过程中会被拆分成很多小的数据包,这些数据包到达接收端后,接收端会按照 TCP 头中的序号为其排序,从而保证组成完整的数据。
-
四次挥手断开连接:
- 第一次挥手:客户端发送FIN=M,用来关闭客户端到服务器端的数据传送,然后客户端就会进入FIN_WAIT_1状态,意思就是说,我没有数据要发了,但是你如果要发数据的话,还可以发给我,不用急着关闭我们之间的连接
- 第二次挥手:服务器端收到FIN=M后,发送ack=M+1,告诉客户端,我知道啦,但是,我还没准备好现在就关掉连接,请你继续等待我的消息。然后客户端就进入了FIN_WAIT_2状态,继续等待服务器端
- 第三次挥手:当服务器端确定数据已发送完成,则向客户端发送FIN=N报文,告诉客户端,好了,我这边数据发完了,准备好关闭连接了。然后服务器端进入LAST_ACK状态。
- 第四次挥手:服务器端收到了FIN=N报文,知道可以关闭连接了,但是他还是不相信网络,怕服务器端不知道要关闭,所以发送ack=N+1后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重传。服务器端收到ACK后,就知道可以断开连接了。客户端等待了2MSL后依然没有收到回复,则证明服务器端已正常关闭,那好,我客户端也可以关闭连接了。最终完成了四次握手
TCP 为了保证数据传输的可靠性,牺牲了数据包的传输速度,因为“三次握手”和“数据包校验机制”等把传输过程中的数据包的数量提高了一倍。
03 | HTTP请求流程:为什么很多站点第二次打开速度会很快?
HTTP是一种允许浏览器向服务器获取资源的协议,是WEB的基础。
那么现在以http://time.geekbang.org/index.html
为例,来探讨一下浏览器发起HTTP请求的流程吧
浏览器端发起 HTTP 请求流程
1.构建请求
浏览器会先构建请求行信息,构建好后,浏览器准备发起网络请求
GET /index.html HTTP1.1
2.查找缓存
在真正发起请求之前,浏览器会先找找浏览器缓存(是指在本地保存资源副本,以供下次请求直接使用的技术)里有没有这个文件。
- 如果找到了,拦截请求,直接返回这个资源的副本
- 没找到,进入网络请求。
3. 准备 IP 地址和端口
- 了解HTTP和TCP的关系
- 浏览器使用HTTP协议作为应用层协议,用来封装请求的文本信息;
- 使用TCP/IP作为传输层协议,将文本信息发送到网络上。
- 所以,HTTP 的内容是通过 TCP 的传输数据阶段来实现的
所以我们知道了,HTTP网络请求的第一步是建立TCP连接。建立TCP连接需要有IP地址和端口号,IP地址和端口号我们有吗?我们知道我们现在有一个URL地址http://time.geekbang.org/index.html
,那么从这个URL地址可以获得IP地址和端口号吗?
- 拿到IP
我们可以看到这个url地址的域名是time.geekbang.org
。那么想要从域名获得IP地址,就要用到DNS(Domain Name System 域名系统)
所以,HTTP网络请求的第一步,浏览器会请求DNS返回域名对应的IP。
当然浏览器还提供了DNS 数据缓存服务,如果某个域名已经解析过了,那么浏览器会缓存解析的结果,以供下次查询时直接使用,这样也会减少一次网络请求。这样我们就拿到IP地址了 - 拿到端口号
通常情况下,如果 URL 没有特别指明端口号,那么 HTTP 协议默认是 80 端口。
4. 等待TCP队列
现在IP地址和端口号都已经拿到了,按理说已经可以开始建立TCP连接了,但是因为Chrome同一域名最多只能建立6个TCP连接,,如果在同一个域名下同时有 10 个请求发生,那么其中 4 个请求会进入排队等待状态,直至进行中的请求完成。,如果当前请求数量少于 6,会直接进入下一步,建立 TCP 连接。
5. 建立 TCP 连接
排队等待结束之后,终于可以和服务器握手了,在 HTTP 工作开始之前,浏览器通过 TCP 与服务器建立连接
6. 发送 HTTP 请求
建立TCP连接后,浏览器就可以和服务器进行通信了。
- 首先浏览器会向服务器发送请求行,它包括了请求方法、请求 URI(Uniform Resource Identifier)和 HTTP 版本协议。
发送请求行,就是告诉服务器浏览器需要什么资源,最常用的请求方法是Get。比如,直接在浏览器地址栏键入极客时间的域名(time.geekbang.org),这就是告诉服务器要 Get 它的首页资源。 - 如果要发送一些数据给服务器,就要用到POST请求,要发送的数据就放在请求体里。
- 在浏览器发送请求行命令之后,还要以请求头形式发送其他一些信息,把浏览器的一些基础信息告诉服务器。比如包含了浏览器所使用的操作系统、浏览器内核等信息,以及当前请求的域名信息、浏览器端的 Cookie 信息,等等。
服务器端处理 HTTP 请求流程
请求送到了,接下来就轮到服务器端来处理请求信息了。
1. 返回请求
我们在命令行里输入
curl -i url
2. 断开连接
通常情况下,一旦服务器向客户端返回了请求数据,它就要关闭 TCP 连接。不过如果浏览器或者服务器在其头信息中加入了:Connection:kepp-alive
,那么 TCP 连接在发送后将仍然保持打开状态,这样浏览器就可以继续通过同一个 TCP 连接发送请求。保持 TCP 连接可以省去下次请求时需要建立连接的时间,提升资源加载速度
3.重定向
当你在浏览器中打开 geekbang.org 后,你会发现最终打开的页面地址是 https://www.geekbang.org。
这两个 URL 之所以不一样,是因为涉及到了一个重定向操作。跟前面一样,我们依然可以使用 curl 来查看下请求 geekbang.org 会返回什么内容。
问题汇总
- 为什么很多站点第二次打开速度会很快?
第一次加载页面过程中,缓存了一些耗时的数据。如DNS缓存和页面资源缓存 - 登录状态是如何保持的?
如果服务端发送的响应头内含有Set-Cookie的字段,那么浏览器会把字段里的内容保存到本地,当下次客户端再往该服务器发送请求时,客户端会自动在请求头中加入 Cookie 值后再发送出去。服务器端发现客户端发送过来的 Cookie 后,会去检查究竟是从哪一个客户端发来的连接请求,然后对比服务器上的记录,最后得到该用户的状态信息。
从图中可以看到,浏览器中的 HTTP 请求从发起到结束一共经历了如下八个阶段:构建请求、查找缓存、准备 IP 和端口、等待 TCP 队列、建立 TCP 连接、发起 HTTP 请求、服务器处理请求、服务器返回请求和断开连接。
04 | 导航流程:从输入URL到页面展示,这中间发生了什么?
典中典面试题。
首先回顾一下这三个进程都干嘛的吧
- 浏览器进程: 负责页面显示、用户交互、子进程管理、存储。
- 网络进程:负责页面的网络资源加载
- 渲染进程:把三件套转换为用户可以与之交互的网页。因为渲染进程所有的内容都是通过网络获取的,会存在一些恶意代码利用浏览器漏洞对系统进行攻击,所以运行在渲染进程里面的代码是不被信任的。这也是为什么 Chrome 会让渲染进程运行在安全沙箱里,就是为了保证系统的安全。
然后我们来看这个过程 - 首先,用户从浏览器进程里输入请求信息
- 网络进程发起URL请求
- 在服务器响应URL请求后,浏览器进程就要开始准备渲染进程了
- 渲染进程准备好之后,要先向渲染进程提交页面数据,可以称之为提交文档阶段。
- 渲染进程接收完文档信息之后,便开始**解析页面和加载子资源,**完成页面的渲染。
这其中,用户发出 URL 请求到页面开始解析的这个过程,就叫做导航。
1. 从输入 URL 到页面展示
当用户在地址栏中输入一个查询关键字时,地址栏会判断输入的关键字是搜索内容,还是请求的 URL。
- 如果是搜索内容,地址栏会使用浏览器默认的搜索引擎,来合成新的带搜索关键字的 URL。
- 如果判断输入内容符合 URL 规则,比如输入的是 time.geekbang.org,那么地址栏会根据规则,把这段内容加上协议,合成为完整的 URL,如 https://time.geekbang.org。
当浏览器刚开始加载一个地址之后,标签页上的图标便进入了加载状态。但此时图中页面显示的依然是之前打开的页面内容,并没立即替换为你需要的页面。因为需要等待提交文档阶段,页面内容才会被替换。
2. URL 请求过程
浏览器进程会通过进程间通信(IPC)把 URL 请求发送至网络进程,网络进程接收到 URL 请求后,会在这里发起真正的 URL 请求流程。
首先,网络进程会查找本地缓存是否缓存了该资源。如果有的话,直接返回该资源。如果没找到,那么直接进入网络请求流程。
那么我们来复习一下上面的流程。 HTTP网络请求的第一步是建立TCP连接,建立TCP连接需要有IP地址和端口号。IP地址要通过DNS解析获得。所以我们第一步就是要进行DNS解析,来获取IP地址
接下来就是利用 IP 地址和服务器建立 TCP 连接。连接建立之后,浏览器端会构建请求行、请求头等信息,并把和该域名相关的 Cookie 等数据附加到请求头中,然后向服务器发送构建的请求信息。
服务器收到请求信息后,根据请求信息生成响应数据,并发给网络进程。
(1)重定向
如果状态码是301或者302 说明服务器需要浏览器重定向到其他URL。Location里存的就是重定向的地址,这时候网络进程会读取这个地址,然后发起新的HTTP或HTTPS请求。
如果状态码是200,表示不需要重定向,一切正常,继续往下处理就好。
(2)响应数据类型处理
URL 请求的数据类型,有时候是一个下载类型,有时候是正常的 HTML 页面,那么浏览器是如何区分它们呢?
答案是通过Content-Type
从图中可以看到,响应头中的 Content-type 字段的值是 text/html,这就是告诉浏览器,服务器返回的数据是HTML 格式。
如果 Content-Type 的值是 application/octet-stream,那么表示数据是字节流类型的,通常情况下,浏览器会按照下载类型来处理该请求。
所以,不同 Content-Type 的后续处理流程也截然不同。如果 Content-Type 字段的值被浏览器判断为下载类型,那么该请求会被提交给浏览器的下载管理器,同时该 URL 请求的导航流程就此结束。但如果是HTML,那么浏览器则会继续进行导航流程。由于 Chrome 的页面渲染是运行在渲染进程中的,所以接下来就需要准备渲染进程了。
3. 准备渲染进程
- 通常情况下,打开新的页面都会使用单独的渲染进程;
- 如果从 A 页面打开 B 页面,且 A 和 B 都属于同一站点的话,那么 B 页面复用 A 页面的渲染进程;如果是其他情况,浏览器进程则会为 B 创建一个新的渲染进程。
渲染进程准备好之后,还不能立即进入文档解析状态,因为此时的文档数据还在网络进程中,并没有提交给渲染进程,所以下一步就进入了提交文档阶段。
4. 提交文档
“文档”是指 URL 请求的响应体数据。
- “提交文档”的消息是由浏览器进程发出的,渲染进程接收到“提交文档”的消息后,会和网络进程建立传输数据的“管道”。
- 等文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程。
- 浏览器进程在收到“确认提交”的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新 Web 页面。
这也就解释了为什么在浏览器的地址栏里面输入了一个地址后,之前的页面没有立马消失,而是要加载一会儿才会更新页面。
5.渲染阶段
一旦文档被提交,渲染进程便开始页面解析和子资源加载了。一旦页面生成完成,渲染进程会发送一个消息给浏览器进程,浏览器接收到消息后,会停止标签图标上的加载动画。
- 服务器可以根据响应头来控制浏览器的行为,如跳转、网络数据类型判断。
- Chrome 默认采用每个标签对应一个渲染进程,但是如果两个页面属于同一站点,那这两个标签会使用同一个渲染进程。
- 浏览器的导航过程涵盖了从用户发起请求到提交文档给渲染进程的中间所有阶段。
总结:如果面试官问你,从输入URL到页面展示,这中间发生了什么?怎么回答呢?
-
用户输入关键词,地址栏判断是搜索内容还是url地址。
如果是搜索内容,会使用浏览器默认搜索引擎加上搜索内容合成url;
如果是域名会加上协议(如https)合成完整的url。 -
然后按下回车。浏览器进程通过IPC(进程间通信)把url传给网络进程(网络进程接收到url才发起真正的网络请求)。
-
网络进程接收到url后,先查找有没有缓存。
有缓存,直接返回缓存的资源。
没有缓存。(进入真正的网络请求)。首先获取域名的IP,系统会首先自动从hosts文件中寻找域名对应的 IP 地址,一旦找到,和服务器建立TCP连接;如果没有找到,则系统会将网址提交 DNS 域名解析服务器进行 IP 地址的解析。 -
利用IP地址和服务器建立TCP连接(3次握手)。
-
建立连接后,浏览器构建数据包(包含请求行,请求头,请求正文,并把该域名相关Cookie等数据附加到请求头),然后向服务器发送请求消息。
-
服务器接收到消息后根据请求信息构建响应数据(包括响应行,响应头,响应正文),然后发送回网络进程。
-
网络进程接收到响应数据后进行解析。
如果发现响应行的返回的状态码为301,302,说明这个url并不是最终的url,需要重定向。所以就要找响应头中的Location字段,Location的内容就是需要重定向的地址url。获取到这个url一切重新来过。
如果返回的状态码为200,说明服务器返回了数据。 -
好了,获取到数据以什么方式打开呢?打开的方式不对的话也不行。打开的方式就是 Content-Type。这个属性告诉浏览器服务器返回的数据是什么类型的。如果返回的是网页类型则为 text/html,如果是下载文件类型则为 application/octet-stream 等等。打开的方式不对,则得到的结果也不对。
如果是下载类型,则该请求会被提交给浏览器的下载管理器,同时该请求的流程到此结束。
如果是网页类型,那么浏览器就要准备渲染页面了。 -
渲染页面开始。浏览器进程发出“提交文档”(文档是响应体数据)消息给渲染进程,渲染进程接收到消息后会和网络进程建立传输数据的通道,网络进程将“文档”传输给渲染进程。
-
一旦开始传输,渲染进程便开始渲染界面(详细渲染过程待续。。。)
-
传输完毕,渲染进程会发出“确认提交”消息给浏览器进程。
-
浏览器在接收到“确认提交”消息后,更新浏览器界面状态(包括地址栏信息,仟前进后退历史,web页面和网站安全状态)。
-
页面此时可能还没有渲染完毕,而一旦渲染完毕,渲染进程会发送一个消息给浏览器进程,浏览器接收到这个消息后会停止标签图标的加载动画。
自此,一个完整的页面形成了。
05 | 渲染流程(上):HTML、CSS和JavaScript,是如何变成页面的?
通过学习前面的内容,已经大概知道是渲染模块在做这个事情。那么我们现在就来了解一下渲染模块是怎么工作的
按照渲染的时间顺序,流水线可分为如下几个子阶段:构建 DOM 树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成。
- 每个子阶段都有输入内容
- 都有处理过程
- 都有输出内容
子阶段一:构建DOM树
为什么要构建DOM树?因为浏览器无法直接理解和使用HTML,所以需要将HTML转换为浏览器能够理解的结构—DOM树
所以构建DOM树这一子阶段的
- 输入内容就是 HTML文件。
- 处理过程就是 HTML解析器
- 输出内容就是 DOM树
子阶段二:样式计算
样式计算的目的是为了计算出 DOM 节点中每个元素的具体样式
1. 把 CSS 转换为浏览器能够理解的结构
当渲染引擎接收到 CSS 文本时,会执行一个转换操作,将 CSS 文本转换为浏览器可以理解的结构——styleSheets。
2. 转换样式表中的属性值,使其标准化
3. 计算出 DOM 树中每个节点的具体样式
子阶段三:布局阶段
现在,我们有 DOM 树和 DOM 树中元素的样式,但这还不足以显示页面,因为我们还不知道 DOM 元素的几何位置信息。那么接下来就需要计算出 DOM 树中可见元素的几何位置,我们把这个计算过程叫做布局。
1. 创建布局树
DOM 树含有很多不可见的元素,比如 head 标签,还有使用了 display:none 属性的元素。所以在显示之前,我们还要额外地构建一棵只包含可见元素布局树。
2. 布局计算
子阶段四:分层
因为页面有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树
f12 然后3d view 就可以看到图层了
现在我们知道了浏览器的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面。
并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。
- 拥有层叠上下文属性的元素会被提升为单独的一层。
- 需要剪裁(clip)的地方也会被创建为图层。
<style>
div {
width: 200;
height: 200;
overflow:auto;
background: gray;
}
</style>
<body>
<div >
<p> 所以元素有了层叠上下文的属性或者需要被剪裁,那么就会被提升成为单独一层,你可以参看下图:</p>
<p> 从上图我们可以看到,document 层上有 A 和 B 层,而 B 层之上又有两个图层。这些图层组织在一起也是一颗树状结构。</p>
<p> 图层树是基于布局树来创建的,为了找出哪些元素需要在哪些层中,渲染引擎会遍历布局树来创建层树(Update LayerTree)。</p>
</div>
</body>
子阶段五:绘制
子阶段六:分块以及栅格化(raster)操作
当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程,然后就轮到合成线程开始工作。
通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做视口(viewport)。
在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。
基于这个原因,合成线程会将图层划分为图块(tile),这些图块的大小通常是 256x256 或者 512x512
然后合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。
渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的。
栅格化过程通常由GPU加速生成,这个过程叫做快速栅格化,或者GPU栅格化,生成的位图将会被保存在GPU内存中。
我们可以看出渲染进程把生成图块这个指令发送给GPU,然后在GPU中执行生成图块的位图,然后保存在GPU内存中
子阶段八:合成和显示
当所有图块都被光栅化后,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。
在浏览器进程中的VIZ组件,将接收合成线程发过来的DrawQuad命令,将页面内容绘制到内存中,最后把内存显示在屏幕上。
总结
结合上图,一个完整的渲染流程大致可总结为如下:
- 渲染进程将 HTML 内容转换为能够读懂的DOM 树结构。
- 渲染引擎将 CSS 样式表转化为浏览器可以理解的styleSheets,计算出 DOM 节点的样式。
- 创建布局树,并计算元素的布局信息。
- 对布局树进行分层,并生成分层树。
- 为每个图层生成绘制列表,并将其提交到合成线程。
- 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
- 合成线程发送绘制图块命令DrawQuad给浏览器进程。
- 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。
相关概念
1. 更新了元素的几何属性(重排)
可以看出,如果我们通过 JavaScript 或者 CSS 修改元素的几何位置属性,例如改变元素的宽度、高度等,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排。无疑,重排需要更新完整的渲染流水线,所以开销也是最大的。
2. 更新元素的绘制属性(重绘)
从图中可以看出,如果修改了元素的背景颜色,那么布局阶段将不会被执行,因为并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程就叫重绘。相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。
3. 直接合成阶段
那如果更改一个既不要布局也不要绘制的属性,会发生什么变化呢?
在上图中,我们使用了 CSS 的 transform 来实现动画效果,这可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作。这样的效率是最高的,因为是在非主线程上合成,并没有占用主线程的资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率。
问题:如何减少重绘重排?
- 使用 class 操作样式,而不是频繁操作 style
- 避免使用 table 布局
- 批量dom 操作,例如 createDocumentFragment,或者使用框架,例如 React
- Debounce window resize 事件
- 对 dom 属性的读写要分离
- will-change:transform做优化