玩命加载中 . . .

深入探究“在浏览器输入URL到渲染页面”(上)过程剖析


前言

这是一道十分经典的面试题,涉及到计网、操作系统、web 的一系列知识,不管是深度还是广度,都能够很好地考察面试者的水平,相信很多程序员多多少少都接触到这道题,网上也有很多非常好的文章对此作出了解释,但能深入并且扩展开来的,少之又少。今天笔者会以面试者的角度来回答这个问题,逐步扩展开来,详细讲解这道题,既作为自己面试的准备,也帮助大家能更加深入的理解到其中的奥秘(原来我们再日常不过的操作,还隐藏了这么多技术,而这也是前端的神奇所在啦),预计在真实面试中可以讲 15 分钟左右。

关于这道经典的面试题,笔者准备写三篇文章来对知识点进行深度和广度的挖掘:

正文

我们知道会有这么一个过程:

  • 输入 URL 地址:在浏览器地址栏输入网站连接
  • DNS 解析:将域名解析成 IP 地址
  • TCP 连接:浏览器与服务器进行三次握手
  • 发送 HTTP 请求
  • 服务器处理请求并返回 HTTP 报文
  • 浏览器解析渲染页面
  • 断开连接:TCP 四次挥手

这个过程不能说错,但笔者觉得不够全面,因为在这背后还有更多的技术在默默支撑着这些功能的实现,我们一起来探究下。

先简述总体过程:

一、用户输入

  • 用户在浏览器地址栏输入 url 并回车

二、URL 请求过程

  • 浏览器处理用户输入,把处理后的 url 发送至网络进程
  • 网络进程收到 url 请求后查看本地是否缓存了该资源,如果有则将该资源返回给浏览器进程
  • 如果没有,网络进程向服务器发起 HTTP 请求(网络请求)以请求资源:
    • DNS 解析:将域名解析成 IP 地址
    • 如果请求协议是HTTPS,那么还需要建立TLS连接
    • 利用 ip 地址和服务器建立 TCP 连接:浏览器与服务器进行三次握手
    • 浏览器端构建请求头信息,并将其发送给服务器
    • 服务器处理请求并返回 HTTP 报文给网络进程
    • 处理状态码

三、断开连接

  • TCP 四次挥手

四、浏览器进程开始准备渲染进程

  • 浏览器进程检查当前url是否和之前打开的渲染进程根域名是否相同,如果相同,则复用原来的进程,如果不同,则开启新的渲染进程

五、提交文档

  • 渲染进程准备好之后,需要先向渲染进程提交页面数据,我们称之为提交文档阶段

六、渲染阶段

  • 渲染进程接收完文档信息之后,便开始解析页面和加载子资源,完成页面的渲染。

下面来详述这些过程的具体细节:

一、用户输入

当用户在地址栏中输入信息时,地址栏会判断该信息是文字还是路径。

  • 如果是文字,地址栏会使用浏览器默认的搜索引擎,来合成新的带有该文字的 url。
  • 如果是路径(通常用户会输入如 baidu.com 的路径),那么地址栏若判断其符合 URL 规则,就把这段路径加上协议,合成完整的 url,就变成了 https://www.baidu.com

当用户完成输入后并回车之后,浏览器便进入下图的状态:
image.png
从图中可以看出,标签页上的图标进入了加载状态,但此时页面仍是之前的页面,并没有立即替换成百度的首页。这是因为需要等待提交文档阶段,页面内容才会被替换。

而提交文档需要获得服务器返回所请求的文件信息,才能进行下一步操作。

二、URL 请求过程

用户已经完成了他的输入操作,接下来便进入了页面资源请求过程。这时,浏览器进程通过进程间通信(IPC)把 url 请求发送给网络进程,网络进程接收到 URL 请求后会查看本地缓存。

简单说一下:
浏览器进程主要负责用户交互、子进程管理和文件储存等功能。
网络进程是面向渲染进程和浏览器进程等提供网络下载功能。
渲染进程的主要职责是把从网络下载的HTML、JavaScript、CSS、图片等资源解析为可以显示和交互的页面。

这个我们在前面简述总流程时已经说过了,但是所谓的“查看本地缓存”是怎么个查看法,缓存又是存到何处,我们来探究下其中细节:

浏览器缓存

所谓“查看本地缓存”,其实就是我们常听到的浏览器缓存。

浏览器缓存有两种,分别是 强缓存协商缓存

浏览器在发送请求时,根据请求头的 ExpiresCache-Control 判断是否命中强缓存策略,如果命中,直接从缓存获取资源,不会发送请求了;如果没有命中,发送请求,根据请求头 If-Modified-SinceLast-Modified 值和 If-None-MatchEtag 值判断是否命中协商缓存,如果命中,直接从缓存获取资源,如果没有命中,则直接从服务端获取资源。

强缓存

强制缓存就是向浏览器缓存查找该请求结果,并根据该结果的缓存规则来决定是否使用该缓存结果的过程。

怎么判断是否命中强缓存呢?有三种情况:

  • ①第一次请求,不存在缓存结果和缓存标识,这时强缓存失效,直接向服务器发送请求。
  • ②存在缓存结果和缓存标识,但已经失效,这时强缓存失败,使用协商缓存。
  • ③存在缓存结果和缓存标识,且尚未失效,这时强缓存生效,直接返回该结果。

怎么做到强缓存呢?可以通过设置两种 HTTP Header 实现:ExpiresCache-ControlCache-Control 的优先级高于 Expires)。

  • Expires 是 HTTP/1.0 控制网页缓存的字段,其值为服务器返回该请求结果缓存的到期时间。也就是说,Expires = max-age + 请求时间 ,需要和 Last-modified 结合使用。

Expires 控制缓存的原理是使用客户端的时间与服务端返回的时间作对比,那么如果①客户端的时间被人为修改,或者②由于时区不同服务端的时间不准确,那么强缓存会直接失效。因此到了 HTTP/1.1 Expire 已经被 Cache-Control 替代,这也是为什么 Expires 的优先级低于 Cache-Control 的原因。

  • Cache-Control 是 HTTP/1.1 控制网页缓存的字段,主要取值为:
    • public:默认值,所有内容都将被缓存(浏览器和代理服务器都可以缓存,并且在多用户间共享)
    • private:所有内容只有客户端可以缓存(不能在用户间共享)
    • no-cache:客户端缓存内容,但是否使用缓存则需要经过协商缓存来验证决定
    • no-store:所有内容都不会被缓存,既不强缓存,也不协商缓存
    • max-age=xxx:单位为 s,缓存内容将在 xxx 秒后失效
    • s-maxage:单位为 s,同 max-age,只用于共享缓存(比如CDN缓存)。
      • 比如,当 s-maxage=60 时,在这 60 秒中,即使更新了 CDN 的内容,浏览器也不会进行请求。也就是说 max-age 用于普通缓存,而 s-maxage 用于代理缓存。如果存在 s-maxage,则会覆盖掉 max-ageExpires header。
    • must-revalidate:指定如果页面是过期的,则去服务器进行获取。

      注意:规则可以同时多个

强缓存不会向服务器发送请求,直接从缓存中读取资源,在 chrome 控制台的 Network 选项中可以看到该请求返回 200 的状态码,并且 Size 显示 from disk cache(磁盘缓存) 或 from memory cache(内存缓存) 。

先加载一次页面,然后刷新一下就可以在控制台看到哪里缓存了:
image.png
那问题来了:什么时候会使用 from disk cache,什么时候会使用 from memory cache 呢?
答案是先 memorydisk ,原因如下:

  • from memory cache 有两个特点,分别是快速读取和时效性。
    • 快速读取:内存缓存会将编译解析后的文件,直接存入该进程的内存中,占据该进程一定的内存资源,以方便下次运行使用时的快速读取。
    • 时效性:一旦该进程关闭,则该进程的内存则会清空。
  • from disk cache 直接将缓存写入硬盘文件中,读取缓存需要对该缓存存放的硬盘文件进行I/O操作,然后重新解析该缓存内容,读取复杂,速度比内存缓存慢。
  • 在浏览器中,浏览器会在 js 和图片等文件解析执行后直接存入内存缓存中,那么当刷新页面时只需直接从内存缓存中读取(from memory cache);而 css 文件则会存入硬盘文件中,所以每次渲染页面都需要从硬盘读取缓存(from disk cache)

协商缓存

协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程。

怎么判断是否命中协商缓存呢?有两种情况:

  • ①协商缓存生效,返回 304 ,表示该资源无更新。
  • ②协商缓存失效,返回 200 和请求结果,表示该资源更新了。

怎么做到协商缓存呢?也可以通过设置两种 HTTP Header 实现:Last-Modified/If-Modified-SinceEtag/If-None-MatchEtag/If-none-match 的优先级高于 Last-Modified/If-Modified-Since)。

  • Last-Modified/If-Modified-Since
    • Last-Modified 是服务器响应请求时,返回该资源文件在服务器最后被修改的时间。
    • If-Modified-Since 是客户端再次发起该请求时,携带上次请求返回的 Last-Modified 值。
      服务器收到请求,发现请求头含有 If-Modified-Since ,会根据该字段值与该资源在服务器的最后被修改时间做比较:若服务器的资源最后被修改时间大于 If-Modified-Since 的字段值,则重新返回资源,状态码为 200 ;否则返回 304 ,代表资源无更新。
  • Etag/If-None-Match
    • Etag 是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成一段 hash 字符串)
    • If-None-Match 是客户端再次发起该请求时,携带上次请求返回的唯一标识 Etag 值,通过此字段告诉服务器该资源上次请求返回的唯一标识值。
      服务器收到请求,发现该请求头中含有 If-None-Match ,会根据该字段值与该资源在服务器的 Etag 值作对比:若发现一致则返回 304 ,代表资源无更新,继续使用缓存文件;不一致则重新返回资源文件,状态码为 200

浏览器缓存总结

强制缓存优先于协商缓存进行,若强制缓存(ExpiresCache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-SinceEtag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,重新获取请求结果,再存入浏览器缓存中;生效则返回 304 ,继续使用缓存。

图解:image.png

如果你想亲手操作浏览器缓存,体会其中奥妙,这篇文章 可以助你一臂之力 ^_^

到这里,“查看本地缓存”就结束了,如果仍然需要向服务器请求资源的话,就需要先进行下面的操作了。

DNS 解析

要向域名所在服务器请求资源,首先当然得知道这个域名的 ip 地址是什么,才可以发起请求。关于这个 ip 地址,计算机会先在本地进行查找,这里会分成三小步:

  • 先到浏览器的 DNS 缓存中查询是否有对应记录,如有则直接返回 ip ,完成解析,如果没有则下一步;
  • 判断软件或浏览器是否有进行域名直接查询,如果有的话,会直接连到软件服务商提供的 DNS 服务器上,并返回 ip ;如果没有,则继续查询操作系统的缓存,如有缓存则直接返回 ip ,如果没有则下一步;
  • 查看本地 host 文件(因为 hosts 文件会建立域名到 ip 地址的绑定关系),比如 windowshost 文件一般位于 “C:\Windows\System32\drivers\etc” ,如果这里也没有的话就需要到本地 DNS 服务器上查找了。

如果至此还没有命中域名,这时浏览器才会向本地 DNS 服务器发起一个请求来解析这个域名(题外话:通常本地域名服务器在你的城市的某个角落,距离不会很远,一般都会缓存域名解析结果,大约 80% 的域名解析到这里就完成了)。
提一嘴:DNS 域名服务器一般分3种,分别是根域名服务器(.)、顶级域名服务器(.com、.cn、.net 等)、权限域名服务器(xxx.com、xxx.cn、xxx.net 等)。

  • 查询 DNS 请求到达本地 DNS 服务器之后,本地 DNS 服务器会首先查询它自己的缓存记录,如果缓存中有此条记录,就可以直接返回结果。如果没有,本地 DNS 服务器还要向根域名服务器进行查询【由本地 DNS 服务器代替浏览器发起这个请求】。
  • 根域名服务器没有记录具体的域名和 ip 地址的对应关系,它会返回域名的顶级域名服务器的地址,让本地 DNS 服务器去这个地址获取 ip 。
  • 于是本地 DNS 服务器得到新的地址后,向顶级域名服务器发出请求。顶级域名服务器收到请求,会返回权威域名服务器的地址,让本地 DNS 服务器其这个地址获取 ip 。
  • 于是本地 DNS 服务器得到新的地址后,向权限域名服务器发出请求。权限域名服务器收到请求,在自己的缓存表中发现了该域名和 ip 地址的对应关系,就将 ip 地址返回给本地 DNS 服务器。
  • 本地 DNS 服务器将获取到与域名对应的 ip 地址返回给客户端,并且将域名和 ip 地址的对应关系保存在缓存中,以备下次别的用户查询时使用。
  • 这样,DNS 解析就完成了。

这样解释可能比较抽象,也不太容易懂,我们来看个栗子:
用户输入的 url 为 https://www.baidu.com/ ,这里假设本地缓存无效,浏览器直接向本地 DNS 服务器发起域名解析请求。

  • 浏览器发送带有 url 的请求,询问本地 DNS 服务器:我这个域名的 ip 是什么?
  • 本地 DNS 服务器:咱也不知道啊,我帮你问问吧,待会把结果告诉你。
  • 本地 DNS 服务器发送带有 url 的请求,询问根服务器:这个域名的 ip 你知道吗?
  • 根域名服务器:我给你 .com 顶级域名服务器的地址,你去问问它知不知道吧。
  • 本地 DNS 服务器拿到 .com 顶级域名服务器的地址,便发起请求,询问 .com 顶级域名服务器:这个域名的 ip 你知道吗?
  • .com 顶级域名服务器:我给你 baidu.com 权限域名服务器的地址,你去问问它知不知道吧。
  • 本地 DNS 服务器拿到 baidu.com 权限域名服务器的地址,便发起请求,询问 baidu.com 权限域名服务器:这个域名的 ip 你知道吗?
  • baidu.com 权限域名服务器:我找找看哈,是 1.2.3.4 ,诺,给你。
  • 本地 DNS 服务器拿到 ip 后,将其给了浏览器:我问到了 ip 了,这就是了,你拿去吧。
  • 浏览器:好的,我把它存起来一段时间,这段时间就可以不用麻烦你了。

这样比较好理解一些有木有,还是不行的话可以看下图解:
image.png

关于 DNS 解析的 TTL 参数:
购买域名后,我们在配置 DNS 解析的时候,有一个参数常常容易忽略,就是 DNS 解析的 TTL 参数(Time To Live)。TTL 这个参数告诉本地 DNS 服务器,域名缓存的最长时间。
用阿里云解析来举例,阿里云解析默认的 TTL 是 10 分钟,10 分钟的含义是,本地 DNS 服务器对于域名的缓存时间是 10 分钟,10 分钟之后,本地 DNS 服务器就会删除这条记录,删除之后,如果有用户访问这个域名,就要重复一遍上述复杂的流程。
其实,如果网站已经进入稳定发展的状态,不会轻易更换 IP 地址,我们完全可以将 TTL 设置到协议最大值,即24小时。带来的好处是,让域名解析记录能够更长时间的存放在本地DNS服务器中,以加快所有用户的访问。

推荐阅读:超详细 DNS 协议解析

TCP 三次握手

DNS 解析完成之后,浏览器已经知道了要请求域名的 ip 地址,可以建立向该地址建立连接了:
image.png

  • 一开始客户端和服务器都处于 CLOSED 状态。然后服务器开始监听某个端口,并进入 LISTEN 状态。

    CLOSED:没有任何连接状态
    LISTEN:侦听来自远方 TCP 端口的连接请求

  • 客户端主动发起连接,开始第一次握手:客户端向服务器发送 SYN 报文(SYN = 1,表明这是一个 TCP 连接请求报文段),并指明客户端的初始化序列号 seq = x (表示本报文段所发送的数据的第一个字节的序号)。客户端进入 SYN-SENT 状态,等待服务器的确认。

    SYN-SENT :同步已发送状态,在发送连接请求后等待匹配的连接请求

  • 服务器收到客户端的 SYN 报文,如果同意建立连接,就开始第二次握手:服务器发送 SYN + ACK 报文作为应答(SYN = 1, ACK = 1,表明这是一个 TCP 连接请求确认报文段),并指定自己的初始化序列号 seq = y (作为 TCP 服务器进程所选择的初始序号),同时把客户端的 seq + 1 作为确认号 ack 的值,即 ack = x + 1 (表示已经收到了客户端发来的 SYN 报文,希望收到的下一个数据的第一个字节的序号是 x + 1 )。服务器进入 SYN-RCVD 状态。

    SYN-RCVD:在收到和发送一个连接请求后等待对连接请求的确认

  • 客户端收到服务器响应的 SYN 报文之后,开始第三次握手:客户端发送 ACK 报文(ACK = 1 ,表明这是一个普通的 TCP 确认报文段),把服务器的 seq + 1 作为 ack 的值,即 ack = y + 1 (表示已经收到了服务器发来的 SYN 报文,希望收到的下一个数据的第一个字节的序号是 y + 1 ),并指明此时客户端的序列号 seq = x + 1 (这是因为 TCP 客户进程发送的第一个 TCP 报文段的序号为 x ,并且不携带数据)。客户端进入 ESTABLISHED 状态。

    ESTABLISHED:代表一个打开的连接,数据可以传送给用户

  • TCP 服务器进程收到该确认报文段后也进入 ESTABLISHED 状态。现在,TCP 双方都进入了连接已建立状态,它们可以基于已建立好的 TCP 连接进行通信,即开始发送 HTTP 请求。

浏览器向服务器发送 HTTP 请求

服务器处理请求并返回 HTTP 响应报文给网络进程

HTTP 报文分为请求报文和响应报文,都有固定的格式以及各自的属性。
详情可见:HTTP首部

处理状态码

  1. 在收到服务器返回的响应报文后,网络进程开始解析响应头。
    如果发现返回的状态码是 301302 ,那么说明服务器需要浏览器重定向到其他 url ,于是网络进程会从响应头的 Location 字段里读取重定向的地址,然后再发起新的 HTTP 或者 HTTPS 请求,这也意味着要从头开始重复以便之前的流程。

  2. 在处理了跳转信息之后,接下来就要看看服务器返回来的数据类型了。
    怎么查看这个数据类型呢?答案是响应头的 Content-Type 字段。
    Content-Type 表示服务器返回的响应体数据的类型,浏览器会根据它的值来决定如何显示响应体的内容。

如果值为 text/html ,这就是告诉浏览器,服务器返回的数据是 HTML 格式;如果值为 application/octet-stream,就代表显示数据是字节流类型的,通常情况下,浏览器会按照下载类型来处理该请求。

注意:如果服务器配置 Content-Type 不正确,比如将 text/html 类型配置成 application/octet-stream 类型,那么浏览器可能会曲解文件内容,可能会将一个本来是用来展示的页面,变成一个下载文件。

三、TCP 四次挥手

浏览器收到服务器响应的数据后,要看看请求头或响应头中是否包含 Connection: Keep-Alive ,表示建立了持久连接,这样 TCP 连接会一直保持,之后请求统一站点的资源会复用这个连接;否则进行 TCP 四次挥手断开连接:
image.png

  • 一开始客户端和服务器都处于 ESTABLISHED 状态。当数据传输结束后,通信的双方均可释放连接。

  • 假设是客户端先发起关闭请求,开始第一次挥手:客户端发送 FIN 报文(FIN = 1 ,表明这是一个 TCP 连接释放报文段,同时也对之前收到的报文段进行确认),报文中指定一个序列号 seq = u (表示 TCP 客户进程之前已传送过的数据的最后一个字节的序号 +1)。客户端进入 FIN-WAIT-1 状态。

    FIN-WAIT-1:待远程TCP的连接中断请求,或先前的连接中断请求的确认

  • 服务器收到 FIN 包,结束 ESTABLISHED 阶段,开始第二次挥手:服务器发送 ACK 报文(ACK = 1 ,表明这是一个普通的 TCP 确认报文段),并且令 ack = u + 1 (表明这是在收到客户端报文的基础上,将其序号值加 1 作为本段报文确认号 ack 的值,这是对 TCP 连接释放报文段的确认),设置 seq = v 。服务器进入 CLOSE_WAIT 状态。

    CLOSE-WAIT:等待从本地用户发来的连接中断请求;

  • 此时的 TCP 处于半关闭状态,从 “客户端–>服务器” 方向的连接释放,客户端无法再发送数据给服务器。客户端收到服务器的确认后,进入 FIN-WAIT-2 状态,等待服务器发出的连接释放报文段。

    FIN-WAIT-2:从远程TCP等待连接中断请求

  • 如果服务器没有要东西要传送给客户端,要断开连接了,就开始第三次挥手:发送 FIN + ACK 报文(FIN = 1, ACK = 1 ,表示服务器已经准备好释放连接了),并指定确认号 ack = u + 1 (表示这是在收到客户端报文的基础上,将其序号 seq 的值加 1 作为本段报文确认号 ack 的值),同时指定一个序列号 seq = w 。服务器进入 LAST-ACK 状态,等待客户端的确认。

    LAST-ACK - 等待原来发向远程TCP的连接中断请求的确认;

  • 客户端收到服务器发来的 FIN + ACK 包之后,确认服务器已经做好释放连接的准备了,于是开始第四次挥手:发送一个 ACK 报文(ACK = 1 ,表示接收到了服务器准备好释放连接的信号)作为应答,令 ack = w + 1 (表示这是在收到了服务器报文的基础上,将其序号 seq 的值 +1 作为本段报文确认号的值),并且 seq = u + 1 (表示这是在已收到服务器报文的基础上,将其确认号 ack 的值作为本段序号的值)。客户端进入 TIME-WAIT 状态。

    TIME-WAIT:等待足够的时间以确保远程TCP接收到连接中断请求的确认

  • 随后客户端开始在 TIME-WAIT 阶段等待 2 MSL

  • 服务器收到从客户端发出的 TCP 报文之后结束 LAST-ACK 阶段,进入 CLOSED 阶段。由此正式确认关闭从 “服务器–>客户端” 方向的连接。

  • 客户端在等待了 2 MSL 之后,自动结束 TIME-WAIT 阶段,进入 CLOSED 阶段。至此,四次握手完成。

四、准备渲染进程

默认情况下,Chrome 会为每一个页面分配一个渲染进程,也就是说,在浏览器上每打开一个新的 Tab 页面就会创建一个新的渲染进程(这句话其实不太正确,我们后面会讲到)。

如下图所示,我在百度首页打开了百度贴吧,总共开了两个标签页,查看 Chrome 的任务管理器,我们发现确实创建了两个渲染进程,两个进程 ID 都不一样。
image.png

但这并不是说打开一个标签页就一定会创建新的渲染进程,只要两个标签页属于同一个站点,浏览器就会让这两个标签页直接运行在同一个渲染进程中。

Q:那么什么叫“同一站点”呢?
A:我们将“同一站点”定义为:只要协议 + 根域名相同,就说他们属于同一站点。
下面这三个 url 就属于同一站点:

https://www.baidu.com
https://www.baidu.com:8080
https://map.baidu.com/

因为他们的协议都是 https:// ,根域名都是 baidu.com ,因此均属于同一站点。这也意味着浏览器不会创建三个渲染进程,而是只创建一个渲染进程,其他标签页复用该进程。

五、提交文档

  • 渲染进程准备好后,还不能立即进入文档解析状态,因为此时的文档数据还在网络进程中,并没有提交给渲染进程,因此浏览器进程向渲染进程发起“提交文档”的消息,渲染进程接收到消息后与网络进程建立传输数据的“管道”。
  • 于是网络进程就开始传送数据。当渲染进程接收完数据后,会返回“确认提交”的消息给浏览器进程。
  • 浏览器进程收到确认消息后更新浏览器界面。

    这就是为什么在浏览器的地址栏输入了一个地址之后,之前的页面没有立马消失,而是要加载一会才会更新页面。这中间要请求资源、等待响应、提交数据等等,当然不会一下子就完成好。
    注意:此时页面还没渲染完毕,只是将浏览器的界面状态更新了而已,如前进后退的状态、地址栏信息、安全状态、页面变化(可能是页面变成白屏或者骨架屏吧?)。

六、浏览器页面的渲染过程

当文档数据被提交后,渲染进程就要开始页面解析和子资源加载了。

  1. 解析 HTML 构建 DOM
  2. 解析 CSS 构建 CSSOM
  3. 创建 Layout(布局) 树,并计算元素的几何信息。
    • 遍历 DOM 树中的所有可见节点,并把这些节点加到布局中;
    • 而不可见的节点会被布局树忽略掉,如 head 标签下面的全部内容,再比如属性为 dispaly:none 等的元素也不会被包进布局树
  4. 对布局树进行分层,并生成层次树。
    • 分层的作用确定哪些元素需要放置在哪一图层。
    • 通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。
  5. 为每个图层生成绘制列表,并将其提交到合成线程。
    • 渲染引擎会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表。当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程。
  6. 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
  7. 当所有图块都被光栅化后,合成线程就会将它们合并成一张图片,并生成一个绘制图块的命令 DrawQuad ,然后将该命令发送给浏览器进程。
  8. 浏览器进程里的一个叫 viz 的组件是专门用来接收合成线程发过来的 DrawQuad 命令,然后根据该命令将其页面内容绘制到内存中,最后再将内存显示在屏幕上。(然后发送给显卡。)
  9. 接着放入通过显卡缓存的后缓冲区,然后显卡中前后缓冲去交换,显示器显示页面并显示到显示器上。
  10. 当页面生成完成,渲染进程会发送一个消息给浏览器进程,浏览器收到消息后,会停止标签图标上的加载动画。

    ①你以为渲染过程只是这样吗?那么大错特错了,就解析 HTML 而言,里面还有很多学问。
    ②你知道为什么第3步不是应该生成 Render 树吗?因为那已经是之前的事情了,现在 Chrome 团队已经做了大量的重构,已经没有生成Render Tree的过程了。
    ③你知道需要满足什么条件,渲染引擎才会为特定的节点创建新的层吗?需要拥有层叠上下文属性的元素会被提升为单独的一层;或者需要剪裁(clip)的地方也会被创建为图层。
    ④你知道什么是光栅化吗?所谓栅格化,是指将图块转换为位图

    还有很多更加深层次的东西这里没有写出来,笔者强推:
    poetry 的 渲染流程(上):HTML、CSS和JavaScript是如何变成页面的
    神三元的 002 说一说从输入URL到页面呈现发生了什么?——解析算法篇
    这两篇文章写得非常之好,分享给大家。前端的世界真的非常神奇,这两篇文章会告诉你,你平常看到的那些很好看的网页背后,究竟做了些什么。这里笔者就不班门弄斧了。

最后

看完这篇文章后,你的大脑应该会感觉很充实,学到了很多底层知识,同时知道了在面试中遇到这道题时该怎么回答,这是好事。但其实这里面还可以继续深挖下去,你知道的,面试官的水平通常会比面试者高,他们可能会问更加深层次的东西,要想回答上来,我们平时要注意多思考,多问为什么。


文章作者: hcyety
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hcyety !
评论
  目录