浏览器原理详解

从浏览器地址栏输入 URL 到请求返回发生了什么?

(一)浏览器进行 URL 解析

输入 URL 后,会进行解析(URL 的本质就是统一资源定位符)。URL 一般包括以下部分:

  • protocol:协议头,譬如有 http、ftp 等
  • host:主机域名或 IP 地址
  • port:端口号
  • path:目录路径
  • query:即查询参数
  • fragment:即#后的 hash 值,一般用来定位到某个位置

因为网络标准规定了 url 只能是数字和字母,以及一些特殊符号-_.~ ! * ‘ ( ) ; : @ & = + $ , / ? # [ ]等。为了防止出现歧义,需要对 url 进行编码转义。url 编码的一般规则为 UTF-8,可以使用 encodeURIComponent 或者 encodeURI 进行编码。两者区别在于encodeURIComponent 编码范围更广,适合给参数编码,encodeURI 适合给 URL 本身编码,当然项目里一般都是用 qs 库去处理。

(二)根据 DNS 系统查找 IP

如果输入的是域名,需要通过 dns 将其解析成 IP,大致流程:

  • 本地查找:如果浏览器有缓存该域名的 ip,则直接使用浏览器缓存,否则使用本机缓存,再不然就去查询 hosts 文件
  • 服务器查找:如果本地没有,就向 dns 域名服务器查询(全球 13 台,固定 ip 地址),查询到对应的 IP

dns 解析很耗时,如果解析域名过多,会让首屏加载变慢,可以考虑 dns-prefetch 优化,即在 html 页面头部写入 dns 缓存地址。

1
2
<meta http-equiv="x-dns-prefetch-control" content="on" />
<link rel="dns-prefetch" href="http://bdimg.share.baidu.com" />

1. DNS 工作原理

DNS 协议(Domain Name System,域名系统)提供的是一种主机名到 IP 地址的转换服务。它是应用层协议,通常该协议运行在 UDP 协议上,使用 53 端口号。DNS 使用 UDP 协议作为传输层协议的主要原因是为了避免使用 TCP 协议时造成的连接时延。为了得到一个域名的 IP 地址,往往会向多个域名服务器查询,如果使用 TCP 协议,那么每次请求都会存在连接时延,这样使 DNS 服务变得很慢。大多数的地址查询请求,都是浏览器请求页面时发出的,这样会造成网页的等待时间过长。

image-20210412160626241

上图展示了 DNS 在本地 DNS 服务器是如何查询的,而下图是本地 DNS 服务器向其他域名服务器请求的过程:

image-20210412160821759
  • 递归查询:查询请求发出后,域名服务器代为向下一级域名服务器发出请求,最后向用户返回查询的最终结果。使用递归查询,用户只需要发出一次查询请求。
  • 迭代查询:查询请求后,域名服务器返回单次查询的结果。下一级的查询由用户自己请求。使用迭代查询,用户需要发出多次的查询请求。

本地 DNS 服务器向其他域名服务器请求的流程大致如下:

  1. 本地 DNS 服务器向根域名服务器发起请求。
  2. 根域名服务器返回顶级域名服务器的 IP 地址。
  3. 本地 DNS 向顶级域名服务器发起请求。
  4. 顶级域名服务器返回二级域名服务器的 IP 地址。
  5. 本地 DNS 向二级域名服务器发起请求。
  6. 二级域名服务器返回主机的 IP 地址。
  7. 本地 DNS 缓存该 IP 地址,并将其返回给操作系统。
  8. 操作系统将该 IP 地址返回给浏览器。

所以一般而言,本地服务器查询是递归查询,而本地 DNS 服务器向其他域名服务器请求的过程是迭代查询的过程

2. DNS 缓存

在一个请求中,当某个 DNS 服务器收到一个 DNS 回答后,它能够将回答中的信息缓存在本地存储器中。返回的资源记录中的 TTL 代表了该条记录的缓存时间。

3. DNS 实现负载平衡

DNS 可以用于在冗余的服务器上实现负载平衡。现在的大型网站一般都会使用多台服务器提供服务,因此一个域名可能会对应多个服务器地址。当用户在浏览器输入网站域名进行请求时,DNS 服务器返回这个域名所对应的服务器 IP 地址的集合,但在每个回答中,会循环移动 IP 地址的顺序。用户一般会选择排在前面的地址发送请求,因此用户的请求被均衡地分配到不同的服务器上,这样就实现了负载均衡。但由于 DNS 服务器中存在缓存,所以有可能一个服务器出现故障后,域名解析仍然返回的是其 IP 地址,从而导致用户无法访问。

(三)TCP/IP 请求(http 协议三次握手)

http 的本质就是 tcp/ip 请求,tcp 将 http 长报文划分为短报文,通过三次握手与服务端建立连接,进行可靠传输。TCP 三次握手是指建立一个 TCP 连接时,需要客户端和服务器总共发送 3 个包。

  • 第一次握手:建立连接时,客户端发送 SYN 包(seq=x)到服务器,并进入 SYN_SEND 状态,等待服务器确认;
  • 第二次握手:服务器收到 SYN 包后,回传一个 ACK 包(ack=x+1),同时发送自己的 SYN 包(seq=y),此时服务器进入 SYN_RECV 状态;
  • 第三次握手:客户端收到服务器的 SYN+ACK 包后,回传一个 ACK 包(ack=y+1),此时客户端进入 ESTABLISHED 状态,服务器收到这个 ACK 包后也进入 ESTABLISHED 状态。

SYN 包是指同步序列编号(Synchronize Sequence Numbers)包,用于建立连接时,客户端发送 SYN 包(seq=x)到服务器,并进入 SYN_SEND 状态,等待服务器确认。ack 包是指确认应答(Acknowledgement)包,用于确认收到数据包。

三次握手的目的是为了防止已经失效的连接请求报文段传到服务端,因而产生错误。假如没有第三次握手,服务端接收到失效的请求报文段就会认为连接已建立,从而进入等待客户端发送数据的状态。但客户端并没有发出请求,所以不会发送数据。于是服务端就会一直处于等待状态,从而浪费资源。

浏览器对同一域名下并发的 tcp 连接是有限制的(2-10 个不等),而且在 http1.0 中往往一个资源下载就需要对应一个 tcp/ip 请求,所以针对这个瓶颈,又出现了很多的资源优化方案(HTTP2 实现的多路复用或者使用多个子域名)。

(四)连接建立后请求 HTML 文件

如果 html 文件在浏览器缓存里面,则浏览器直接返回。如果没有,就去服务端拿。

1. 浏览器缓存

浏览器缓存,又称客户端缓存,分为强缓存和协商缓存。它既是网页性能优化里面静态资源相关优化的一大利器,也是无数 web 开发人员在工作过程不可避免的一大问题:在产品开发时总是想办法避免产生缓存,而在产品发布时又通过配置缓存来提升网页的访问速度。了解浏览器的缓存命中原理,是开发 web 应用的基础。

① 浏览器缓存基本过程

1)浏览器向服务器发送 http 请求报文来获取资源;

2)服务器返回 http 响应报文,并通过响应头设置缓存策略;

3)客户端根据响应头的策略决定是否缓存资源。如果是,则将响应头与资源缓存下来;

4)在客户端再次请求且命中资源时,此时客户端去检查上次缓存的缓存策略,根据策略的不同、是否过期等判断是直接读取本地缓存还是与服务器协商缓存,具体过程如下:

  • 浏览器在加载资源时,先根据这个资源的一些 http header 判断它是否命中强缓存,如果命中强缓存,浏览器直接从自己的缓存中读取资源,不会发请求到服务器。假设浏览器在加载某个 css 文件所在的网页时,这个 css 文件的缓存配置命中了强缓存,浏览器就直接从缓存中加载这个 css,不会发送请求到网页所在服务器;

  • 当强缓存没有命中时,浏览器一定会发送一个请求到服务器,通过服务器端依据资源的另外一些 http header 验证这个资源是否命中协商缓存。如果协商缓存命中,服务器会将这个请求返回,但不返回请求的资源,以此告诉客户端可以直接从缓存中获取,于是浏览器就又会从自己的缓存中去加载这个资源;

  • 强缓存与协商缓存的共同点是:如果命中,都是从客户端缓存中加载资源,而不从服务器加载资源数据;区别是:强缓存不发请求到服务器,协商缓存会发请求到服务器。

  • 当协商缓存也没有命中时,浏览器直接从服务器加载资源数据。

② 强缓存的原理

当浏览器对某个资源的请求命中了强缓存时,返回的 http 状态为 200。在 chrome 开发者工具的 network 里面 size 会显示为 from cache,比如京东首页里就有很多静态资源配置了强缓存,用 chrome 打开几次后用 f12 查看 network,可以看到有不少请求就是从缓存中加载的。

image-20210324141352086

强缓存是利用 Expires 或者 Cache-Control 这两个响应头部实现的,它们都用来表示资源在客户端缓存的有效期。

Expires

Expires 是 http1.0 提出的一个表示资源过期时间的 header,它描述的是绝对时间,由服务器返回,用 GMT 格式的字符串表示,如:Expires: Thu, 31 Dec 2037 23:55:55 GMT,其缓存原理是:

1)浏览器初次跟服务器请求一个资源,服务器在返回该资源的同时,会在响应报文的头部加上 Expires,如:

image

2)浏览器在接收到这个资源后,会把该资源连同 Response Headers 一起缓存下来;

3)浏览器再请求这个资源时,会先从缓存中寻找,找到这个资源后,拿出它的 Expires 跟当前的请求时间比较,如果请求时间在 Expires 指定的时间之前,就命中该缓存,否则没有命中;

4)如果缓存没有命中,浏览器则会从服务器加载资源,同时 Expires 头部会在重新加载时更新。

Expires 是较老的强缓存管理头部,由于它是服务器返回的一个绝对时间,在服务器时间与客户端时间相差较大时,缓存管理容易出现问题,比如随意修改下客户端时间,就能影响缓存命中的结果。所以在 http1.1 引入了如下这个新的 header。

Cache-Control

这是一个相对时间,在配置缓存时以秒为单位,用数值表示,如 Cache-Control: max-age=315360000,缓存原理如下。

1)浏览器初次跟服务器请求一个资源,服务器在返回这个资源的同时,在响应报文的头部加上 Cache-Control;

image

2)浏览器在接收到这个资源后,会把这个资源连同响应报文的头部一起缓存下来;

3)浏览器再请求这个资源时,先从缓存中寻找,找到这个资源后,根据它第一次的请求时间和 Cache-Control 设定的有效期计算出资源过期时间,再拿这个过期时间跟当前请求时间比较,如果请求时间在过期时间之前,则命中缓存,否则就不会命中;

4)如果缓存没有命中,浏览器直接从服务器加载资源,Cache-Control 头部在重新加载时会更新。

Cache-Control 描述的是一个相对时间,在进行缓存命中时,都通过客户端时间进行判断。与 Expires 相比,Cache-Control 的缓存管理更为有效安全。

Cache-Control 取值

在 HTTP/1.1 中,Cache-Control 是最重要的规则,主要用于控制网页缓存,主要取值为:

  • public:所有内容可以被任何用户缓存,包括客户端和 CDN 等中间代理服务器。
  • private:所有内容只有客户端可以缓存,不允许 CDN 等中继缓存服务器对其缓存。Cache-Control 的默认取值
  • no-cache:客户端缓存内容,但是否使用缓存则需要经过协商缓存来验证决定
  • no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存
  • max-age=xxx (xxx is numeric):缓存内容将在 xxx 秒后失效

如果使用 private,则表示该资源仅仅属于发出请求的最终用户,这将禁止中间服务器(如代理服务器)缓存此类资源。对于包含用户个人信息的文件(如一个包含用户名的 HTML 文档),可以设置 private,一方面由于这些缓存对其他用户来说没有任何意义,另一方面用户可能不希望相关文件储存在不受信任的服务器上。需要指出的是,private 并不会使得缓存更加安全,它同样会传给中间服务器(如果网站对于传输的安全性要求很高,应该使用传输层安全措施)。public 则允许所有服务器缓存该资源。通常情况下,对于所有人都可以访问的资源(例如网站的 logo、图片、脚本等),Cache-Control 默认设为 public 是合理的。

当响应报文的头部同时存在 Expires 和 Cache-Control 时,Cache-Control 优先级高于 Expires。

image

③ 强缓存的管理与应用

Ⅰ. 缓存的位置
image-20210412134807919

图像和网页等资源主要缓存在 disk cache,操作系统缓存文件等资源主要缓存在 memory cache 中。具体操作由浏览器自动分配,看谁的资源利用率低就分给谁。

可以看到 memory cache 请求时间都是 0ms,这个是不是太神奇了,这方面我来梳理下。

查找浏览器缓存时会按顺序查找:Service Worker -> Memory Cache ->Disk Cache ->Push Cache。

Service Worker

是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker 的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。

Memory Cache

内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,例如,从远程 web 服务器直接提取访问文件可能需要 500 毫秒 (半秒),那么磁盘访问可能需要 10-20 毫秒,而内存访问只需要 100 纳秒,更高级的还有 L1 缓存访问 (最快和最小的 CPU 缓存) 只需要 0.5 纳秒。内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。

Disk Cache

存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。在所有浏览器缓存中,Disk Cache 覆盖面是最大的。它会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。绝大部分的缓存都来自 Disk Cache。

prefetch cache(预取缓存)

link 标签上如果带了 prefetch,则再次加载会出现。prefetch 是预加载的一种方式,被标记为 prefetch 的资源,将会被浏览器在空闲时间加载。

Push Cache

Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在 Chrome 浏览器中只有 5 分钟左右,同时它也并非严格执行 HTTP 头中的缓存指令。

CPU、内存、硬盘都是计算机的主要组成部分。
  • CPU:中央处理单元 (Central Processing Unit) 的缩写,也叫处理器,是计算机的运算核心和控制核心。电脑靠 CPU 来运算、控制。让电脑的各个部件顺利工作,起到协调和控制作用。
  • 硬盘:存储资料和软件等数据的设备,有容量大,断电数据不丢失的特点。
  • 内存:负责硬盘等硬件上的数据与 CPU 之间数据交换处理。特点是体积小,速度快,有电可存,无电清空,即电脑在开机状态时内存中可存储数据,关机后将自动清空其中的所有数据。
Ⅱ. 强缓存的取消

由于在开发时不会专门去配置强缓存,而浏览器默认会缓存图片、css 和 js 等静态资源,所以开发环境下经常会因为强缓存导致资源没有及时更新而看不到最新效果。解决这个问题的方法有以下几种:

1)直接 ctrl+f5,解决页面直接引用的资源更新的问题;

2)使用浏览器的隐私模式开发;

3)如果用的是 chrome,可以 f12 在 network 那里把缓存给禁掉:

image

4)在开发阶段,给资源加上一个动态的参数,如 css/index.css?v=0.0001。但由于每次资源的修改都要更新引用的位置,同时修改参数的值,所以操作起来并不方便(除非在动态页面比如 jsp 里开发可以用服务器变量来解决 v=${sysRnd},或者用一些前端的构建工具来处理这个参数修改的问题);

5)如果资源引用的页面,被嵌入到了一个 iframe 里面,可以在 iframe 的区域右键单击重新加载该页面,以 chrome 为例:

image

6)如果缓存问题出现在 ajax 请求中,最有效的解决办法就是 ajax 的请求地址追加随机数;

7)还有一种情况就是动态设置 iframe 的 src 时,有可能也会因为缓存问题,导致看不到最新的效果,这时在要设置的 src 后面添加随机数也能解决问题;

8)如果你用的是 grunt 和 gulp 这种前端工具开发,通过它们的插件比如 grunt-contrib-connect 来启动一个静态服务器,则完全不用担心开发阶段的资源更新问题,因为在这个静态服务器下的所有资源返回的 respone header 中,cache-control 始终被设置为不缓存。
image-20210324142846833

强缓存是前端性能优化最有力的工具,对于有大量静态资源的网页,合理利用强缓存提高响应速度。通常的做法是,为这些静态资源配置一个过期时间超长的 Expires 或 Cache-Control,这样用户在访问网页时,只会在第一次加载时从服务器请求静态资源,其它时候只要缓存没有失效并且用户没有强制刷新,资源都会从浏览器自身缓存中加载。

然而缓存配置会带来发布时资源更新的问题,比如某张图片,在用户访问初版时便对其进行了缓存,当网站发布新版本替换了该图片时,用户由于缓存设置,导致没能请求服务器最新的图片资源。除非清掉或禁用缓存或者强制刷新,否则就看不到最新的图片效果。这个问题已经有成熟的解决方案,详情可阅读:http://www.zhihu.com/question/20790576

强缓存通常都是针对静态资源的处理,对动态资源须慎用。服务端页面和引用静态资源的 html 都是动态资源,如果这种 html 也被缓存,那么当其更新后,可能就没有机制能够通知到浏览器。尤其在前后端分离的应用里,页面都是纯 html 页面,这些页面通常不加强缓存,以保证浏览器访问这些页面时始终请求服务器最新的资源。

④ 协商缓存的原理

当浏览器对某个资源的请求没有命中强缓存,就会发请求到服务器,验证协商缓存是否命中。如果命中协商缓存,服务端返回的响应 http 状态为 304 Not Modified

打开京东首页,按 f12 启动开发者工具,再按 f5 刷新页面,查看 network,可以看到有不少请求命中了协商缓存。

image

查看 Response Header,也能看到 304 Not Modified,说明该资源命中了协商缓存,浏览器收到后便从客户端缓存中加载资源。

image

协商缓存利用如下两对 Header 进行管理。

Last-Modified & If-Modified-Since

1)浏览器初次跟服务器请求某个资源后,服务器会在返回资源时给响应报文的头部添加 Last-Modified,用来表示该资源在服务器上的最后修改时间;

image

2)当浏览器再次跟服务器请求该资源时,会在请求报文的头部添加 If-Modified-Since,用来表示上次请求时服务端返回的 Last-Modified 值;

无标题

3)服务器再次收到该资源请求时,会比较浏览器传来的 If-Modified-Since 和该资源在服务器上的最新修改时间,判断资源是否发生变化。如果没有变化则只返回 304 Not Modified 的响应,而且 response header 中不会再添加 Last-Modified,因为既然资源没有变化,那么 Last-Modified 自然保持不变;如果资源有变化,就返回该资源的最新版本。

image

4)浏览器收到 304 的响应后,就会从缓存中加载资源。

5)如果协商缓存没有命中,浏览器直接从服务器加载资源时,Last-Modified 头部在重新加载时会被更新,下次请求时,If-Modified-Since 会启用上次返回的 Last-Modified 值。

Last-Modified 和 If-Modified-Since 的时间以服务器为准,在没有改动服务器时间和篡改客户端缓存的情况下,使用这两个 header 管理协商缓存是很可靠的。但服务器可能会出现资源发生变化,但最后修改时间却不变的情况,而这种问题又难以定位,从而影响到协商缓存的可靠性。于是就有了下面这对 header 来管理协商缓存。

ETag & If-None-Match

1)浏览器初次跟服务器请求某个资源后,服务器在返回该资源时给响应报文的头部加上 ETag,这是服务器根据当前请求资源生成的用字符串表示的唯一标识。只要资源发生变化,标识就一定改变,跟最后修改时间没有关系。

image

2)浏览器再次跟服务器请求该资源时,会在请求报文的头部上添加 If-None-Match,值为上次请求时服务端返回的 ETag。

image

3)服务器再次收到该资源请求时,对该资源重新生成 ETag,与浏览器传来的 If-None-Match 进行比较,判断资源是否发生变化。如果没有变化,则返回 304 Not Modified 的响应。与 Last-Modified 不同的是,由于 ETag 重新生成过,response header 中还会返回 ETag,即使这个 ETag 跟之前的一样;如果有变化,就返回该资源的最新版本。

image

4)浏览器收到 304 的响应后,就会从缓存中加载资源。

⑤ 协商缓存的管理

强缓存不向服务器请求,因此有时资源更新了浏览器并不知道,但协商缓存会发请求到服务器,所以能够确定资源是否发生变化。包括 Apache 在内的多数 web 服务器都默认开启协商缓存,并同时启用【Last-Modified,If-Modified-Since】和【ETag、If-None-Match】。

image

如果没有协商缓存,那么服务器得对每个到达的请求都返回资源内容,这样服务器性能会变差。

【Last-Modified,If-Modified-Since】和【ETag、If-None-Match】一般都是同时启用,这是为了处理 Last-Modified 不可靠的情况。有一种场景需要注意:

  • 分布式系统里多台机器间文件的 Last-Modified 必须保持一致,以免负载均衡到不同机器导致比对失败;
  • 分布式系统尽量关闭掉 ETag (每台机器生成的 ETag 都会不一样);

京东页面的资源请求,返回的 repsones header 就只有 Last-Modified,没有 ETag:

image

协商缓存需要配合强缓存使用,如果不启用强缓存的话,协商缓存根本没有意义。

⑥ 浏览器行为对缓存的影响

如果资源已被浏览器缓存,在缓存失效前,再次请求时默认会先检查是否命中强缓存,如果强缓存命中则直接读取缓存,如果强缓存没有命中则发请求到服务器检查是否命中协商缓存,如果协商缓存命中,则告诉浏览器还是可以从缓存读取,否则才从服务器返回最新的资源。这是默认的处理方式,这个方式可能被浏览器的行为改变:

  • 当 ctrl+f5 强制刷新网页时,直接从服务器加载,跳过强缓存和协商缓存;
  • 当 f5 刷新网页时,跳过强缓存,但是会检查协商缓存;

2. 服务器对请求的处理

一般后端有统一的验证,如安全拦截,跨域验证。如果这一步不符合规则,就直接返回相应的 http 报文(如拒绝请求等)。当验证通过后,才会进入实际的后台代码,此时是程序接收到请求,然后执行(譬如查询数据库,大量计算)等程序执行完毕后,就会返回一个 http 响应包(一般这一步也会经过多层封装),最后将这个包从后端发送到前端,完成交互。

(五)浏览器解析 HTML

preview

渲染树的渲染流程如下,分别为计算 CSS 样式、构建渲染树、布局(主要定位坐标和大小,是否换行,各自 position、overflow、z-index 属性)、绘制(将图像绘制出来)。

image-20210323214602332

1. 解析 HTML,构建 DOM 树

解析 HTML 到构建出 DOM 的过程可以简述为:Bytes → Characters → Tokens → Nodes → DOM

img

① Conversion 转换:浏览器将获得的 HTML 内容(Bytes)基于某种编码转换为单个字符

② Tokenizing 分词:浏览器按照 HTML 规范标准将这些字符转换为不同的标记 token。每个 token 都有自己独特的含义以及规则集

③ Lexing 词法分析:分词的结果是得到一堆 token,此时把它们转换为对象,这些对象分别定义它们的属性和规则

④ DOM 构建:因为 HTML 标记定义的就是不同标签之间的关系,这个关系就像是一个树形结构一样

2. 解析 CSS,生成 CSS 规则树

CSS 规则树的生成过程:Bytes → Characters → Tokens → Nodes → CSSOM

img

3. 合并 DOM 树和 CSS 规则树,生成 render 树

一般来说,渲染树和 DOM 树是相对应的,但并非严格意义上的一一对应。因为有一些不可见的 DOM 元素不会插入到渲染树中,如 head 这种不可见的标签或者 display: none

image-20210323213513417

4. 布局和渲染 render 树

布局(Layout/reflow)负责各元素尺寸、位置的计算,渲染(Paint)负责绘制页面像素信息。当通过 JS 动态修改了 DOM 或 CSS,会导致重新布局(Layout)或重绘(Repaint)。这里 Layout 和 Repaint 的概念是有区别的:

  • Layout,也称为 Reflow,即回流。一般意味着元素的内容、结构、位置或尺寸发生了变化,需要重新计算样式和渲染树
  • Repaint,即重绘。意味着元素发生的改变只是影响了元素的一些外观之类的时候(例如背景色,边框颜色,文字颜色等),此时只需要应用新样式绘制这个元素就可以了

回流的成本开销要高于重绘,而且一个节点的回流往往会导致子节点以及同级节点的回流,所以优化方案中一般都包括尽量避免回流。

① 引起回流的因素

  • 页面渲染初始化

  • DOM 结构改变,比如删除了某个节点

  • render 树变化,比如减少了 padding

  • 窗口 resize

  • 获取某些属性,引发回流

很多浏览器会对回流做优化,等到数量足够时做一次批处理回流,但除了 render 树的直接变化,在获取一些属性时,浏览器为了获得正确值也会触发回流,这样使得浏览器优化无效,包括
(1) offset(Top/Left/Width/Height)
(2) scroll(Top/Left/Width/Height)
(3) client(Top/Left/Width/Height)
(4) width, height
(5) 调用了 getComputedStyle() 或者 IE 的 currentStyle

② 避免回流的方案

回流一定伴随着重绘,重绘却可以单独出现。一些优化方案如下:

  • 减少逐项更改样式,最好一次性更改 style,或者将样式定义为 class 并一次性更新;
  • 避免循环操作 DOM,创建一个 document Fragment 或 div,在它上面应用所有 DOM 操作,最后再把它添加到 document;
  • 避免多次读取 offset 等属性。无法避免则将它们缓存到变量;
  • 将复杂的元素绝对定位或固定定位,使得它脱离文档流,否则回流代价会很高;
  • HTML 文档结构层次尽量少,最好不深于六层;
  • 脚本尽量后放,放在前即可;
  • 少量首屏样式内联放在标签内;
  • 样式结构层次尽量简单;
  • 在脚本中尽量减少 DOM 操作,尽量缓存访问 DOM 的样式信息,避免过度触发回流;
  • 减少通过 JavaScript 代码修改元素样式,尽量使用修改 class 名方式操作样式或动画;
  • 动画尽量使用在绝对定位或固定定位的元素上;
  • 隐藏在屏幕外,或在页面滚动时,尽量停止动画;
  • 尽量缓存 DOM 查找,查找器尽量简洁;
  • 涉及多域名的网站,可以开启域名预解析。

(六)TCP 四次挥手

MSL 是 TCP 报文里面最大生存时间,它是任何报文段被丢弃前在网络内的最长时间。

img

  • 第一次挥手:A->B,A 向 B 发出释放连接请求的报文,其中 FIN(终止位) = 1,seq(序列号)=u;在 A 发送完之后,A 的 TCP 客户端进入 FIN-WAIT-1 (终止等待 1)状态。此时 A 还是可以进行收数据的。
  • 第二次挥手:B->A:B 在收到 A 的连接释放请求后,随即向 A 发送确认报文。其中 ACK=1,seq=v,ack(确认号) = u +1;在 B 发送完毕后,B 的服务器端进入CLOSE_WAIT(关闭等待)状态。此时 A 收到这个确认后就进入FIN-WAIT-2(终止等待 2)状态,等待 B 发出连接释放的请求。此时 B 还是可以发数据的。(如果 B 直接跑路,则 A 永远处与这个状态。TCP 协议里面并没有对这个状态的处理,但 Linux 有,可以调整 tcp_fin_timeout 这个参数,设置一个超时时间。)
  • 第三次挥手:B->A:当 B 已经没有要发送的数据时,B 就会给 A 发送一个释放连接报文,其中 FIN=1,ACK=1,seq=w,ack=u+1,在 B 发送完之后,B 进入 LAST-ACK(最后确认)状态。
  • 第四次挥手:A->B;当 A 收到 B 的释放连接请求时,必须对此发出确认,其中 ACK=1,seq=u+1,ack=w+1; A 在发送完毕后,进入到 TIME-WAIT (时间等待)状态。B 在收到 A 的确认之后,进入到 CLOSED(关闭)状态。在经过时间等待计时器设置的时间之后,A 才会进入 CLOSED 状态。

为什么需要四次挥手

因为 TCP 是全双工通信的

(1)第一次挥手

​ 因此当主动方发送断开连接的请求(即 FIN 报文)给被动方时,仅仅代表主动方不会再发送数据报文了,但主动方仍可以接收数据报文。

​ (2)第二次挥手

​ 被动方此时有可能还有相应的数据报文需要发送,因此需要先发送 ACK 报文,告知主动方“我知道你想断开连接的请求了”。这样主动方便不会因为没有收到应答而继续发送断开连接的请求(即 FIN 报文)。

(3)第三次挥手

​ 被动方在处理完数据报文后,便发送给主动方 FIN 报文;这样可以保证数据通信正常可靠地完成。发送完 FIN 报文后,被动方进入 LAST_ACK 阶段(超时等待)。

(4)第四挥手

​ 如果主动方及时发送 ACK 报文进行连接中断的确认,这时被动方就直接释放连接,进入可用状态。

1
2
3
4
5
其实是客户端和服务端的两次挥手,也就是客户端和服务端**分别释放**连接的过程。可以看到,客户端在发送完最后一次确认之后,还要**等待 2MSL** 的时间。主要有两个原因,一个是为了让 B 能够按照**正常**步骤 **进入 CLOSED** 状态,二是为了防止**已经失效**的请求连接报文出现在下次连接中。

1)由于**客户端最后一个 ACK 可能会丢失**,这样 B 就无法正常进入 CLOSED 状态。于是B会重传请求释放的报文,而此时A如果已经关闭了,那就收不到B的重传请求,就会**导致B不能正常释放**。而如果 A 还在等待时间内,就会收到 B 的重传,然后进行应答,这样 B 就可以进入 CLOSED 状态了。

2)在这 2MSL 等待时间里面,本次连接的所有的报文都已经**从网络中消失**,从而不会出现在下次连接中。