6

输入 url 到页面展示发生了什么?

 2 years ago
source link: https://segmentfault.com/a/1190000040533922
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

整体上,可以分为以下几步:

  1. DNS 域名解析;
  2. 建立 TCP 连接(三次握手);
  3. 发送 HTTP 请求;
  4. 服务器处理请求;
  5. 返回响应结果;
  6. 关闭 TCP 连接(四次握手);
  7. 浏览器解析 HTML;
  8. 浏览器布局渲染;

1. DNS 域名解析

域名的结构

域名通过.拆分成几个部分,【完整域名】从右到左依次是:根域名、顶级域名、二级域名、三级域名...

网址:https://www.baidu.com/
完整域名:www.baidu.com.root
省略域名:www.baidu.com.

root 根域名,全球的根域名都是root,因此根域名常常忽略。
com 顶级域名,TLD(Top-Level Domains)。
baidu 二级域名,别名有:次级域名,用户DNS名称服务器, 权威名称服务器
www 三级域名

为什么需要 DNS 解析域名为 IP 地址?

网络通讯需要相应的网络协议,大部分是基于 TCP/IP 协议,而 TCP/IP 协议是基于 IP 地址,所以计算机网络通讯时只能识别 IP 地址。由于用户很难记住每一个网站的 IP 地址,所以就出现了“DNS服务器”。

什么是 DNS ?

DNS 域名系统就是将域名 (例如: www.baidu.com) 转换为 IP 地址。域名和 IP 地址是一一对应的。

DNS 解析方式

  • 正向解析:将域名转换成对应的 IP地址的过程, 它应用于在浏览器地址栏中输入网站域名时的情形。
  • 反向解析:根据IP地址查找对应的注册域名,经常被一些后台程序使用, 用户看不到。

DNS 查询方式

  • 客户端和浏览器,本地 DNS 之间的查询方式是递归查询
  • 本地 DNS 服务器与根域及其子域之间的查询方式是迭代查询

域名解析记录

可查看域名解析记录

DNS 解析过程

解析过程如下:

第一步:检查浏览器缓存
用户通过浏览器浏览某网站后,浏览器会自动缓存该网站域名对应的 IP 地址,当用户再次访问的时候,浏览器会从缓存中查找该域名对应的 IP 地址,如果有,完成域名解析,如果没有,进行下一步。

  • 因为缓存不仅是有大小限制,而且还有时间限制,所以存在域名对应的找不到的情况。
  • 通过 chrome://net-internals/#dns 可以查看 chrome 的 DNS 缓存信息。

第二步:查找本机 hosts 文件
如果用户的浏览器中缓存中没有,系统会去查找自己本地的 hosts 文件是否有这个域名与 IP 的映射关系,如果有,完成域名解析,如果没有,进行下一步。

  • 打开 Finder 应用,按 Shift+Command+G 三个组合按键,输入 Hosts 文件的所在路径:/etc/hosts,即可查看本机 hosts。

前两步都是在本机上完成的,从第三步开始,才向远程 DNS 服务器发起解析域名的请求。

第三步:向 本地域名解析服务器 发起域名解析请求
如果在本机上没有完成域名解析,系统会请求本地域名解析服务器进行解析, 如果有,完成域名解析,如果没有,进行下一步。

  • 本地域名解析服务器 一般都是本地区的域名服务器,比如连接的校园网,那么域名解析系统就在校园机房里,如果连接的是电信、移动或者联通的网络,那么本地域名解析服务器就在本地区,由各自的运营商来提供服务。

前三步是递归查询,后几步是迭代查询。

第四步:向 根域名解析服务器 发起域名解析请求
从本地域名解析服务器获取到根域名服务器对应的主机名,然后向根域名服务器发起解析请求。根域名服务器接收请求,返回所查域的顶级域名(gtld 域名)服务器地址。

第五步:向 顶级域名服务器 发起解析请求
本地域名解析服务器向顶级域名服务器发起解析请求,顶级域名服务器接收请求,返回二级域名服务器地址。

第六步:向 二级域名服务器 发起解析请求
本地域名解析服务器向二级域名服务器发起请求,二级域名服务器接收请求,返回 IP 地址给本地域名服务器。

第七步:本地域名服务器缓存结果
本地域名服务器缓存解析后的结果,缓存时间由时间来控制。

第八步:返回解析结果给用户
解析结果将直接返回给用户,用户系统将缓存该地址,缓存时间由来控制,至此,解析过程结束。

解析域名命令,www.baidu.com 为例:

  • 直接查看域名结果:nslookup www.baidu.com
  • 查看整个解析过程: dig www.baidu.com +trace
    ​ 一般情况下,DNS解析到别名就停止,并返回了具体的IP地址。如果想看到具体的地址,可以进一步对别名进行解析。

具体可查看:浅析DNS域名解析过程

2. 建立 TCP 连接

TCP 是什么?

TCP/IP

计算机与网络设备之间如果要相互通信, 双方就必须基于相同的规则(例如由哪一方先发起通信, 使用哪种语言进行通信, 怎样发送与接收数据,怎样结束通信等,不同的硬件,操作系统之间如何通信),这种规则称为协议 (protocol)。而 TCP/IP 是互联网相关各类协议族的总称。

TCP/IP协议族按层次分别为:应用层,传输层,网络层,数据链路层,物理层。

传输层
该层为两台主机上的应用程序提供端到端的通信。传输层有两个传输协议:TCP (传输控制协议) 和 UDP (用户数据报协议)。

传输控制协议 TCP(Transmission Control Protocol)

  • 是一种面向连接的、可靠的、全双工的、基于字节流的传输层通信协议。

用户数据报协议 UDP(User Datagram Protocol)

  • UDP 在传送数据之前不需要先建立连接,远程主机在收到 UDP 报文后,不需要给出任何确认。
  • 虽然 UDP 不提供可靠交付,但在某些情况下 UDP 确是一种最有效的工作方式(一般用于即时通信),比如: QQ 语音、 QQ 视频 、直播等等

TCP 报文段首部格式

  • 源端口和目的端口:各占 2 个字节,分别是源端口和目的端口。IP 地址 + 端口号就可以确定一个地址。
  • 序号/序列号(Sequense Number,seq):在一个 TCP 连接中传送的字节流中的每一个字节都按顺序编号。该字段表示本报文段所发送的数据的第一个字节的序号。初始序号称为 Init Sequense Number, ISN。

    例如,一报文段的序号是 101,共有 100 字节的数据。这就表明:本报文段数据的第一个字节的序号是 101,最后一个字节的序号是 200。显然,下一个报文段的数据序号应当从 201 开始,即下一个报文段的序号字段值应为 201。

  • 确认号 ack:期望收到对方下一个报文段的第一个数据字节的序号。若确认号为 N,则表明:到序号 N-1 为止的所有数据都已正确收到。

保留位右边是 6 个控制位, 用来说明该报文段性质。

  • 紧急位 URG:当 URG = 1 时,表明此报文段中有紧急数据,是高优先级的数据,应尽快发送,不用在缓存中排队。
  • 确认 ACK:仅当 ACK = 1 时确认号字段才有效,当 ACK = 0 时确认号无效。TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 置为 1。
  • 推送 PSH:当两个应用进程进行交互式的通信时,有时在一端的应用进程希望在键入一个命令后立即就能收到对方的响应。在这种情况下,TCP 就可以使用推送(push)操作。这时,发送方 TCP 把 PSH 置为 1,并立即创建一个报文段发送出去。接收方 TCP 收到 PSH = 1 的报文段,就尽快地交付接收应用进程。而不用等到整个缓存都填满了后再向上交付。
  • 复位 RST:当 RST = 1 时,表明 TCP 连接中出现了严重错误(如由于主机崩溃或其他原因),必须释放连接,重新建立传输连接。
  • 同步 SYN:SYN = 1 表示这是一个连接请求或连接接受报文。

    当 SYN = 1 而 ACK = 0 时,表明这是一个连接请求报文段。对方若同意建立连接,则应在响应的报文段中使 SYN = 1 且 ACK = 1。

  • 终止 FIN:当 FIN = 1时,表明此报文段的发送发的数据已发送完毕,并要求释放运输连接。

如何建立客户端与服务器的 TCP 连接?

TCP 连接的建立采用客户-服务器模式:主动发起连接建立的叫做客户,被动等待连接建立的叫做服务器。进行三次握手,建立TCP连接。

所谓三次握手(Three-way Handshake)是指建立一个 TCP 连接时,需要客户端和服务器总共发送3个包。

进行’三次握手‘的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号(Init Sequense Number, ISN) 为后面的可靠性传输做准备。

三次握手具体过程如下:

SYN:是否是连接请求/接收报文段;
seq:发送的第一个字节的序号;
ACK:是否是确认报文段;
ack:确认号。希望收到的下一个数据的第一个字节的序号;

第一次握手(SYN=1, seq=x)
客户端向服务端发送一个 SYN 报文(SYN = 1)。报文指定客户端的初始化序列号 ISN(x),即 seq = x,表示本报文段所发送的数据的第一个字节的序号。

  • SYN 报文发送完毕后,客户端进入 SYN_SENT 状态,等待服务端确认。
  • 客户端不能确认任何;
    服务器确认了: 对方发送正常,自己接收正常;

第二次握手(SYN=1, ACK=1, seq=y, ack=x+1)
服务端收到客户端的 SYN 报文,对 SYN 报文进行确认,并向客户端发送 SYN + ACK 报文。报文指定服务端的初始化序列号 ISN(y),即 seq = y,同时会把客户端的 x+1(ISN + 1)作为确认号 ack 的值,表示已经收到了客户端发来的的 SYN 报文,希望收到的下一个数据的第一个字节的序号是 x + 1。

  • 这个报文段发送完毕后,服务端进入 SYN_RECV 状态。
  • 客户端确认了:自己发送、接收正常,对方发送、接收正常;
    服务器确认了:对方发送正常,自己接收正常;

第三次握手(ACK=1, seq=x+1, ack=y+1)
客户端收到服务端的 SYN + ACK 报文,会向服务器发送 ACK 报文。把服务器的 y + 1(ISN + 1)作为 ack 的值,表示已经收到了服务端发来的的 SYN 报文,希望收到的下一个数据的第一个字节的序号是 y + 1,并指明此时客户端的序列号 seq = x + 1(初始为 seq = x,所以第二个报文段要 +1)

  • 这个报文段发送完毕后,客户端和服务端都进入 ESTABLISHED (连接成功)状态,此时完成TCP 的三次握手。
  • 客户端确认了:自己发送、接收正常,对方发送、接收正常;
    服务器确认了:自己发送、接收正常,对方发送、接收正常;

在三次握手过程中,通信双方的状态有:

CLOSED:没有连接状态。初始客户端和服务器都是处于CLOSED状态。
LISTEN:收听状态,侦听来自远方 TCP 端口的连接请求。
SYN-SENT:同步已发送,在发送连接请求后等待匹配的连接请求。
SYN-RCVD:同步收到。服务端被动打开后, 接收到了客户端的 SYN 并且发送了 ACK 时的状态。
ESTABLISHED:连接已建立,可以数据传输。

三次握手的生活版:

客户端:“你好,在家不?” -- SYN
服务端:“在的,你来吧。” -- SYN + ACK
客户端:“好嘞。” -- ACK

  • 第三次握手的时候,是可以携带数据的。第一次、第二次握手绝对不可以携带数据。简单来说,请求连接/接收 即 SYN = 1的时候不能携带数据。
  • 理想状态下,TCP 连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。
  • 当一端为建立连接而发送它的 SYN 时,它会为连接选择一个初始序号。ISN 随时间而变化,因此每个连接都将具有不同的 ISN。如果 ISN 是固定的,攻击者很容易猜出后续的确认号,因此 ISN 是动态生成的

3. 发送 HTTP 请求

TCP 连接建立后,浏览器就可以基于 HTTP/HTTPS 协议向服务器发送 GET 请求获取网页信息。

HTTP 是什么

全称:超文本传输协议(HyperText Transfer Protocol
HTTP 是一种能够获取像 HTML、图片等网络资源的通讯协议(protocol),是应用层协议,其报文分为请求报文和响应报文。
当客户端请求一个网页时,先通过 HTTP/HTTPS 协议将请求的内容封装在请求报文之中,服务器收到该请求报文后根据协议规范进行报文解析,然后向客户端返回响应报文。

请求报文的结构

请求行:请求方法(Method) + 空格 + 统一资源标识符(URI) + 空格 + HTTP版本 + CR LF。
请求头:字段名 + 冒号 + 值 + CR LF。
空行: CR LF。代表所有关于请求的头部信息已经发送完毕。
请求体: 不是所有的请求都有一个 body,由用户自定义添加。例如获取资源的请求:GET,HEAD,DELETE 和 OPTIONS,通常它们不需要 body。 有些请求将数据发送到服务器以便更新数据:常见的的情况是 POST 请求(包含 HTML 表单数据)。

打开百度,请求报文如下:

为什么浏览器里请求报文里没有请求体?

因为为了开发人员方便,浏览器把请求体放到了Request Headers 下方的 Request Payload / QueryString / FormData里。三者区别可查看: Request Payload是什么

请求方法描述GETGET 请求会显示请求指定的资源。一般来说GET方法应该只用于数据的读取,而不应当用于会产生副作用的非幂等的操作中。POSTPOST 请求会向指定资源提交数据,请求服务器进行处理,如:表单数据提交、文件上传等。POST方法是非幂等的方法,因为这个请求可能会创建新的资源或/和修改现有资源。PUT用来对资源进行整体修改。 幂等。DELETE请求服务器删除资源。幂等。OPTIONS1. 获取服务器支持的 HTTP 请求方法。2. 检查服务器的性能。例如:跨域请求时的预检,需要向另外一个域名的资源发送一个HTTP OPTIONS请求头,用以判断实际发送的请求是否安全。HEADHEAD 请求与 GET 请求响应相同,但是在响应中只返回首部,没有响应体。这一方法可以再不必传输整个响应内容的情况下,就可以获取包含在响应小消息头中的元信息。TRACE主要用于测试或诊断。客户端发起一个请求时,这个请求可能要穿过防火墙、代理、网关或其他一些应用程序。每个中间节点都可能会修改原始的 HTTP 请求。TRACE 请求会在目的服务器端发起一个 环回 诊断,在响应主体中携带它收到的原始请求报文。客户端可以查看整个请求响应链上,原始报文是否,以及如何被毁坏或修改过。CONNECTHTTP/1.1 协议中预留,能够将连接改为管道方式的代理服务器。通常用于 SSL 加密服务器的链接与非加密的 HTTP 代理服务器的通信。PARCH用于对资源进行部分修改。非幂等。

4. 服务器处理请求

服务器接受到请求,解析请求头。
如果头部有缓存相关信息如 if-none-match与 if-modified-since,则验证缓存是否有效,若有效则返回状态码为304,若无效则重新返回资源,状态码为200。
如果没有缓存,直接返回资源。

5. 返回响应结果

服务器处理完请求,以响应报文的格式向客户端做出响应。

响应报文的结构

状态行:HTTP版本 + 空格 + 状态码 + 空格 + 状态码描述 + CR LF。
响应头:字段名 + 冒号 + 值 + CR LF 。
空行:CR LF,代表所有关于请求的头部信息已经发送完毕。
响应体:不是所有的响应都有 body,由用户自定义添加。响应状态码 (如 201204) 的响应,通常不会有 body。

响应状态码

状态代码由服务器发出,以响应客户端对服务器的请求。
1xx(信息)- 收到请求,继续处理
2xx(成功)- 请求已成功接收与处理
3xx(重定向)- 需要采取进一步措施才能完成请求
4xx(客户端错误)- 请求包含错误的语法或无法满足
5xx(服务器错误)- 服务器无法满足明显有效的请求

6. 关闭 TCP 连接

为了避免服务器与客户端双方的资源占用和损耗,当双方没有请求或响应传递时,任意一方都可以发起关闭请求。关闭 TCP 连接, 需要四次挥手。
所谓四次挥手是指关闭一个 TCP 连接时,需要客户端和服务器总共发送 4 个包。

四次挥手具体过程如下:

FIN:是否是终止报文段;
seq:发送的第一个字节的序号;
ACK:是否是确认报文段;
ack:确认号。希望收到的下一个数据的第一个字节的序号;

刚开始双方都处于ESTAB-LISHED 状态,假设是客户端先发起关闭请求:

第一次挥手(FIN=1, seq=u)
客户端发送一个请求终止连接 FIN 报文。报文中会指定一个初始序列号 seq = u(ISN),并停止再发送数据,主动关闭 TCP 连接。

  • 发送 FIN 报文后,客户端处于 FIN_WAIT1 状态,等待服务端的确认。

第二次挥手(ACK=1, seq=v, ack=u+1)
服务端收到 FIN 报文后,会发送 ACK 报文。报文中会指定一个初始序列号 seq = v(ISN),并把客户端的 u+1(ISN+1)作为 ack 的值,表明已经收到客户端的报文。

  • 报文发送后,服务端处于 CLOSE_WAIT 状态。
  • 此时的 TCP 处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2状态,等待服务端发出的连接释放报文段。

第三次挥手(FIN=1, ACK=1, seq=w, ack=u+1)
如果服务端也想断开连接(没有要向客户端发出的数据),和客户端的第一次挥手一样,发送 FIN 报文,且指定一个序列号 w,并把客户端的 u+1(ISN+1)作为 ack 的值,表明已经收到客户端的报文。

  • 报文发送后,服务端处于 LAST_ACK 的状态,等待客户端的确认。

第四次挥手(ACK=1, seq=u+1, ack=w+1)
客户端收到 FIN 报文后,会发送 ACK 报文。并指定一个序列号 u+1, 把服务端的 w +1 作为ack 的值。

  • 报文发送后,客户端处于 TIME_WAIT 状态, 等待 2MSL 才会变成 CLOSED 状态。

在四次握手过程中,通信双方的状态有:

FIN-WAIT-1:等待远程TCP的连接中断请求,或先前的连接中断请求的确认;
CLOSE-WAIT:等待从本地用户发来的连接中断请求;
FIN-WAIT-2:从远程TCP等待连接中断请求;
LAST-ACK:等待原来发向远程TCP的连接中断请求的确认;
TIME-WAIT:等待足够的时间以确保远程TCP接收到连接中断请求的确认;

四次挥手的生活版:

客户端:“兄弟,我这边没数据要传了,咱关闭连接吧。” -- FIN + seq
服务端:“收到,我看看我这边有木有数据了。” -- ACK + seq + ack
服务端:“兄弟,我这边也没数据要传你了,咱可以关闭连接了。” - FIN + ACK + seq + ack
客户端:“好嘞。” -- ACK + seq + ack

为什么需要等待 2MSL 后才会进入 CLOSED 状态?

2MSL: 一个报文的来回时间

这样做的目的是确保服务端收到客户端发送的 ACK 报文。如果服务端在规定时间内没有收到客户端发来的 ACK 报文的话,服务端会重新发送 FIN 报文给客户端,客户端再次收到 FIN 报文之后,就知道之前的 ACK 报文丢失,会再次发送 ACK 报文给服务端。服务端收到 ACK 报文之后,就关闭连接了,处于 CLOSED 状态。

为什么要四次挥手?

由于 TCP 的半关闭(half-close)特性,TCP 提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了TCP连接。
通俗的来说,两次握手就可以释放一端到另一端的 TCP 连接,完全释放连接一共需要四次握手

7. 浏览器解析 HTML

服务器返回 HTML -- 响应头和数据后,浏览器的 渲染引擎 开始解析 HTML。

渲染引擎解析主流程

渲染引擎通过解析 HTML,生成 DOM 树,解析 CSS,生成 CSSOM 树,然后把 DOM 树和 CSSOM 树结合在一起构建渲染树。

img

构建 DOM 树

HTML 解析器的任务是将 HTML 标记解析成 DOM 树,涉及到 tokenization 和树的构造。

  • HTML标记包括开始和结束标记,以及属性名和值。
  • DOM 树描述了文档的内容。<html>元素是第一个标签也是文档树的根节点。树反映了不同标记之间的关系和层次结构。嵌套在其他标记中的标记是子节点。DOM 节点的数量越多,构建 DOM 树所需的时间就越长。
  • DOM 树构建是增量的。

构建 CSSOM 树

CSS 解析器会将 CSS 文件解析成 CSSOM 树。浏览器将 CSS 规则转换为可以理解和使用的样式映射,遍历 CSS 中的每个规则集,根据 CSS 选择器创建具有父、子和兄弟关系的节点树。

  • CSS 是渲染阻塞的:浏览器会阻塞页面渲染直到它执行了所有的 CSS。
  • DOM 和 CSSOM 是两棵树,它们是独立的数据结构。

处理脚本和样式表

阻塞渲染

“阻塞渲染”仅是指浏览器是否需要暂停网页的首次渲染,直至该资源准备就绪。

JavaScript 脚本

通过 <script> 标签可以向 html 插入 JavaScript 脚本。JavaScript 是一种运行在浏览器中的动态语言。

脚本的默认执行时间

JavaScript 脚本在文档的哪儿插入,就在哪儿执行。当 HTML 解析器遇到一个 <script> 标签时,它会暂停构建 DOM,将控制权移交给 JavaScript 引擎;等 JavaScript 引擎运行完毕,浏览器会从中断的地方恢复 DOM 构建。

脚本与其他资源的依赖关系

JavaScript不仅可以读取和修改 DOM 属性,还可以读取和修改 CSSOM 属性。所以造成了JavaScript 与 DOM、CSSOM 之间很强的依赖关系,从而可能导致浏览器在处理以及在屏幕上渲染网页时出现大幅延迟。

  • 脚本在文档中的位置很重要,建议放在文档底部。
  • JavaScript 执行过程中依赖 CSSOM,直至 CSSOM 就绪才会继续执行。

例如:运行一个将 span 元素的 display 属性从 none 更改为 inline 的脚本

  • 如果浏览器尚未完成 DOM 树的构建,而在此时运行脚本,不存在 span 标签,脚本运行失败。
  • 如果浏览器尚未完成 CSSOM 树的下载和构建,而在此时运行脚本,就会出现竞态问题。此时浏览器将延迟脚本执行和 DOM 构建,直至其完成 CSSOM 的下载和构建。这样对性能不利。

改变脚本默认行为的方法

  • defer: 立即下载,延迟执行

加载和渲染后续文档元素的过程将和脚本的加载并行进行(异步),但是脚本的执行会在所有元素解析完成之后。 脚本总会按照声明顺序执行。

<script defer="defer" src="example.js"></script>
  • async: 异步脚本

加载和渲染后续文档元素的过程将和脚本的加载与执行并行进行(异步),但是脚本加载后马上执行,会阻塞HTML的解析。脚本下载后立即执行,不一定按照声明顺序执行。

<script async="async" src="example.js"></script>

asyncdefer 属性仅仅对外联脚本起作用,在 src 不存在时会被自动忽略。

CSS 是阻塞渲染的资源,这意味着浏览器将不会渲染任何已处理的内容,直至 CSSOM 树构建完毕。所以需要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间。

优化样式表下载

通过使用媒体查询,我们可以根据特定用例(比如显示或打印),也可以根据动态情况(比如屏幕方向变化、尺寸调整事件等)声明样式表,只有符合条件时,浏览器会优先下载并执行此 css 样式表。(无论是否有媒体查询,浏览器都会下载 CSS 资源,只不过不阻塞渲染的资源优先级较低)

<link href="style.css" rel="stylesheet">
<link href="style.css" rel="stylesheet" media="all">
<link href="portrait.css" rel="stylesheet" media="orientation:portrait">
<link href="print.css" rel="stylesheet" media="print">
  • 第一个声明阻塞渲染,适用于所有情况。
  • 第二个声明同样阻塞渲染: “all”是默认类型。第一个声明和第二个声明是等效的。
  • 第三个声明具有动态媒体查询,将在网页加载时计算。根据网页加载时设备的方向,portrait.css 可能阻塞渲染,也可能不阻塞渲染。
  • 第四个只在打印网页时应用,因此网页首次在浏览器中加载时,它不会阻塞渲染。

理论上来说,应用样式表不会更改 DOM 树,所以没有必要等待样式表并停止文档解析。但是如果脚本在文档解析阶段请求样式信息,当时还没有加载和解析样式,对于这种情况,不同的浏览器有不同的解决方案:Firefox 在样式表加载和解析的过程中,会禁止所有脚本。WebKit 仅当脚本尝试访问的样式属性可能受尚未加载的样式表影响时,它才会禁止该脚本。

构建渲染树

渲染树包括了内容和样式:DOM 和 CSSOM 树结合为渲染树。为了构造渲染树,浏览器检查每个节点,从 DOM 树的根节点开始,并为每个可见节点添加其 CSSOM 规则。

  • 渲染树只包含了可见内容。头部(通常)不包含任何可见信息,因此不会被包含在渲染树中。如果有元素上有 display: none;,它本身和其后代都不会出现在渲染树中。
  • 渲染树是和 DOM 树是相对应的,但并非一一对应。非可视化的 DOM 元素不会插入渲染树中。

8. 浏览器渲染页面

渲染树构建完成后,进入布局阶段,布局为每个节点分配一个应出现在屏幕上的确切坐标,决定了每个元素的宽和高,以及节点之间的相关性。

  • 布局性能受 DOM 影响,节点数越多,布局时间越长。

布局完成后,进入绘制阶段,将各个节点绘制在屏幕上,其包括文本、颜色、图片、边框、阴影等任何可视部分。

  • 绘制可以将布局后的元素分解为多个层。将内容提升到 GPU 上的层(而不是 CPU 上的主线程)可以提高首次绘制和重绘性能。

    一些特定的属性和元素可以实例化一个层,包括 <video><canvas>,任何 CSS 属性为 opacity、3D 转换、will-change的元素等。这些节点将与子节点一起绘制到它们自己的层上。

  • 层确实可以提高性能,但是它以内存管理为代价,因此不应作为web性能优化策略的一部分过度使用。
  • 如果屏幕上的绘图被分解成数层,需要进行合成。

当文档的各个部分以不同的层绘制,相互重叠时,必须进行合成,以确保它们以正确的顺序绘制到屏幕上,并正确显示内容。

浏览器运行单个帧的渲染流水线,称为像素管道。如果其中的一个或多个环节执行时间过长用户就会觉得卡顿。像素管道是像素绘制到屏幕上的关键步骤,有如下五个过程:

JS/CSS —> Style —> Layout —> Paint —> Composite

JS/CSS:JS/CSS 代码变动。
Style:重新计算样式
Layout:重新计算布局
Paint:绘制
Composite:合成

目前大多数设备的刷新率都是 60 FPS,如果浏览器在交互的过程中每帧都保持在 60 FPS 左右,用户就不会感到卡顿。从数学角度而言,每帧的预算约为 16.7 毫秒(1000 毫秒 / 60 帧 = 16.66 毫秒/帧)。如果频繁执行 JS 或者 JS 执行时间过长会导致,超出每帧绘制时间,那么就会造成卡顿,例如监听页面滑动,动画等

性能最差的像素管道版本:重排(reflow)

改变了元素的几何属性(例如宽度、高度等),那么浏览器将检查所有元素,布局计算后“自动重排”页面。任何受影响的部分都需要重新绘制,而且最终绘制的元素需进行合成。

重排进行了像素管道的每一步,性能受到较大影响。

性能一般的像素管道版本:重绘(replaint)

改变元素的背景图片、文字颜色或阴影等(元素的几何尺寸和位置不变),不影响页面布局,则会跳过 Layout 布局,直接进行绘制与合成。

性能最佳的像素管道版本:避免 Layout 和 Paint

使用只会触发 composite 的 CSS 属性,就可以避免 Layout 和 Paint。

常用的只会触发 composite 的 CSS 属性有:transform, opacity,pointer-events(是否响应鼠标事件)、perspective (透视效果)、perspective-origin(perspective 的灭点)、cursor(指针样式)、orphans(设置当元素内部发生分页时必须在页面底部保留的最少行数(用于打印或打印预览))、widows(设置当元素内部发生分页时必须在页面顶部保留的最少行数(用于打印或打印预览))。

通过 csstriggers.com 查看每种 CSS 属性的更改是否会触发 Layout/Paint/Composite。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK