输入URL后浏览器发生了什么变化
输入 url 后浏览器发生了什么变化
步骤简单总结
网络请求
- DNS 查询(得到 IP),建立 TCP 连接(三次握手)
- 浏览器发送 HTTP 请求
- 收到请求响应,得到 HTML(解析 HTML 过程中,遇到静态资源还会继续发起网络请求)
解析(字符串 -> 结构化数据)
- HTML 构建 DOM 树
- CSS 构建 CSSOM 树
- 两者结合形成 render tree
渲染
- layout 计算布局:(文档流, 盒模型, 计算各个元素的大小、位置)
- paint 绘制:(将边框颜色、文字颜色、阴影等都画出来)
- composite 合成:根据层叠关系显示画面
- 遇到 JS 执行
- 异步 CSS,图片加载,可能会触发重新渲染
DNS 解析
作用
通过域名查找 IP 地址
查找过程
递归实现
先在本地计算机缓存查找,找不到再将请求发送给 dns 服务器
在本地 dns 服务器找
再到根域名服务器找
再到顶级域名服务器找
以 www.google.com 为例:
. -> .com -> google.com. -> www.google.com
DNS优化
DNS缓存
DNS缓存指DNS返回了正确的IP之后,系统就会将这个结果临时储存起来。并且它会为缓存设定一个失效时间 (例如N小时),在这N小时之内,当你再次访问这个网站时,系统就会直接从你电脑本地的DNS缓存中把结果交还给你,而不必再去询问DNS服务器,变相“加速”了网址的解析。
DNS负载均衡
你访问大公司域名的时候,每次响应的并非是同一个服务器(IP地址不同),一般大公司都有成百上千台服务器来支撑访问。DNS可以返回一个合适的机器的IP给用户,例如可以根据每台机器的负载量,该机器离用户地理位置的距离等等,这种过程就是DNS负载均衡。
建立 TCP 连接
SYN(synchronous建立联机)
ACK(acknowledgement 确认)
FIN(finish结束)
等待TCP队列
Chrome 有个机制,同一个域名同时最多只能建立 6 个 TCP 连接,多余的请求会进入排队等待状态。
三次握手
在 TCP/IP 协议中,TCP 协议提供可靠的连接服务,采用三次握手建立一个连接。

第一次握手:建立连接时,客户端发送 syn 包(syn=j)到服务器,并进入 SYN_SEND 状态,等待服务器确认;
第二次握手:服务器收到 syn 包,必须确认客户的 SYN(ack=j+1),同时自己也发送一个 SYN 包(syn=k),即 SYN+ACK 包,此时服务器进入 SYN_RECV 状态;
第三次握手:客户端收到服务器的 SYN+ACK 包,向服务器发送确认包 ACK(ack=k+1),此包发送完毕,客户端和服务器进入 ESTABLISHED 状态,完成三次握手。

通过这样的三次握手,客户端与服务端建立起可靠的双工的连接,开始传送数据。 三次握手的最主要目的是保证连接是双工的,可靠更多的是通过重传机制来保证的。
为什么是三次?
为了保证服务端能收接受到客户端的信息并能做出正确的应答而进行前两次(第一次和第二次)握手,为了保证客户端能够接收到服务端的信息并能做出正确的应答而进行后两次(第二次和第三次)握手。
四次挥手
由于 TCP 连接是全双工的,因此每个方向都必须单独进行关闭。
那TCP 的四次握手,是为了保证通信双方都关闭了连接,具体过程如下:

1)客户端 A 发送一个 FIN,用来关闭客户 A 到服务器 B 的数据传送;
2)服务器 B 收到这个 FIN,它发回一个 ACK,确认序号为收到的序号加 1。和 SYN 一样,一个 FIN 将占用一个序号;
3)服务器 B 关闭与客户端 A 的连接,发送一个 FIN 给客户端 A;
4)客户端 A 发回 ACK 报文确认,并将确认序号设置为收到序号加 1。
为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?
这是因为服务端的 LISTEN 状态下的 SOCKET 当收到 SYN 报文的建连请求后,它可以把 ACK 和 SYN(ACK 起应答作用,而 SYN 起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的 FIN 报文通知时,它仅仅表示对方没有数据发送给你了,但是你还可以给对方发送数据,也有这么种可能,你还有一些数据在传给对方的途中,所以你不能立马关闭连接,也即你可能还需要把在传输途中的数据给对方之后,又或者,你还有一些数据需要传输给对方后,(再关闭连接)再发送FIN 报文给对方来表示你同意现在可以关闭连接了,所以它这里的 ACK 报文和 FIN 报文多数情况下都是分开发送的。
TCP/UDP 的区别
TCP | UDP |
---|---|
面向连接,是指发送数据之前必须在两端建立TCP连接,连接方式为三次握手(打电话) | 不面向连接(写信) |
可靠传输,流量控制与拥塞控制 | 不可靠传输,尽最大努力交付 |
传输方式上以字节流的形式传输 | 以报文形式传输 |
只能是一对一通信 | 支持一对一、一对多、多对多、多对一交互通信 |
最小20字节,最多60字节 | 首部开销较小,只有 8 字节 |
适用于要求可靠传输的应用(传文件) | 适用于实时应用,如视频会议、直播等(丢几帧无所谓) |
DNS使用什么协议传输?为什么?
- UDP 机制简单 开销小 性能更好
发送 HTTP 请求
1. HTTP报文结构
HTTP报文由报文首部和报文主体构成,中间由一个空行分隔。报文首部包含请求行和请求头部,报文主体主要包含被发送的信息。
报文首部是客户端或服务端需要处理请求或响应的内容及属性,可以传递额外的信息。
一个HTTP报文示例:
1 |
|
1-1. 请求报文
一个HTTP请求报文由请求行,请求头部,空行和请求数据4部分组成。

请求行:三部分组成,方法 + URI + HTTP版本
请求头部:首部字段名和字段值构成,中间用 : 分隔。首部字段格式: 首部字段名:字段值
1-2. 响应报文
HTTP响应报文由状态行、响应头部、空行和响应体4部分组成

状态行:HTTP版本 + 状态码 + 响应短语
2. HTTP首部字段
给浏览器和服务器提供报文主体大小、所使用的语言、认证信息等
2-1. HTTP通用首部字段
通用首部字段是请求报文和响应报文都会使用的字段,例如:
通用头部字段 | HTTP1.0 | HTTP1.1 | 含义 |
---|---|---|---|
Date | 有 | 表示请求和响应生成的日期,GTM时间。例如 Tue, 02 Mar 2021 12:31:25 GMT |
|
Pragma | 有 | 表示数据是否允许被缓存的通信选项 | |
Cache-Control | 有 | 控制缓存的相关信息 no-store表示不缓存 no-cache表示不缓存过期的资源 |
|
Connection | 有 | 设置发送响应之后 TCP 连接是否继续保持 http1.1前需如下所示手动设置 Connection: Keep-Alive ![]() |
|
Transfer-Encoding | 有 | 表示消息主体的编码格式 | |
Via | 有 | 记录途中经过的代理和网关 |
2-2. HTTP请求首部字段
补充请求的附加信息、客户端信息、对相应内容相关的优先级
通用头部字段 | HTTP1.0 | HTTP1.1 | 含义 |
---|---|---|---|
Host | 有 | 接受请求的服务器IP地址和端口号 | |
Accept | 有 | 有 | 客户端可支持的数据类型![]() ![]() |
User-Agent | 有 | 有 | |
If-Modified-Since | 有 | 有 | UMT时间,表示该时间之后资源是否修改![]() |
If-None-Match | 有 | 返回服务器响应头的 Etag 值 | |
Referer | 有 | 有 | 通过点击超链接进入下一个页面时,在这里会记录上一个页面的 URI |
Accept-Encoding | 有 | 有 | 客户端可支持的编码格式 gzip/compress/deflate/identify |
Accept-Language | 有 | 有 | 客户端可支持的语言(中文/英文) |
If-Match | 有 | If-xxx 条件请求 判断为真时,服务器才会执行请求 If-Match 和 ETag 值一致时,服务器才会接受请求 |
|
If-Unmodified-Since | 有 | ||
Range | 有 | 当只需要回去部分数据时,可通过这个字段指定要获取的数据范围 | |
Authorization | ![]() |
2-3. HTTP响应首部字段
通用头部字段 | HTTP1.0 | HTTP1.1 | 含义 |
---|---|---|---|
Location | 有 | 有 | 令客户端重定向至URI,绝对路径 |
Server | 有 | 有 | 服务器程序的名称和版本号相关信息 |
2-4. HTTP实体(消息体)首部字段
通用头部字段 | HTTP1.0 | HTTP1.1 | 含义 |
---|---|---|---|
Allow | 有 | 有 | 表示指定的 URI 支持的方法 |
Content-Encoding | 有 | 有 | 消息的编码格式 |
Content-Length | 有 | 有 | 消息体的长度 |
Content-Type | 有 | 有 | 消息体的数据类型 |
Expires | 有 | 有 | 消息体的有效期,UMT 时间 |
Last-Modified | 有 | 有 | 数据最后更新的日期 |
Etag | 有 | 资源的唯一标识符,控制是否使用缓存 |
3. HTTP方法
根据 HTTP 标准,HTTP 请求可以使用多种请求方法。
HTTP1.0 定义了三种请求方法:GET、POST 和 HEAD 方法
HTTP1.1 新增了六种方法:OPTIONS、PUT、PATCH、DELETE、TRACE 和 CONNECT 方法
方法 | 描述 |
---|---|
GET | 用于从服务器获取数据 |
HEAD | 类似于 GET 请求,只不过响应中没有具体的内容,用户获取报头 |
POST | 向指定资源提交数据进行处理请求。数据被包含在请求体中,POST请求可能导致新的资源的建立或已有资源的修改 |
PUT | 客户端向服务器传送的数据取代指定的文档内容 |
DELETE | 请求服务器删除指定的资源 |
OPTIONS | 允许客户端查看服务器的性能 |
CONNECT | HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器。 |
TRACE | 回显服务器收到的请求,主要用于测试或诊断。 |
PATCH | 是对 PUT 方法的补充,用来对已知资源进行局部更新 。 |

GET与POST方法的区别:
- get 是从指定的资源请求数据,post 是向指定的资源提交要处理的数据
- get 请求可以被缓存,post 请求不会被缓存
- get 请求传输的数据有长度限制,一般为 2048 字符,post 请求传输的数据没有大小限制
- get 请求的数据一般追加在 URL 的末尾,post 请求的数据在 http 请求体中
一般不使用 GET 请求发送如密码这样的敏感信息。我认为 post 请求比 get请求更安全。
缓存
304状态码是在协商缓存,缓存命中的时候服务器返回的,告诉客户端,服务器资源没有修改,可以使用客户端自己的缓存。
浏览器缓存分为本地缓存(强缓存)和协商缓存(弱缓存)。

如上图所示,在浏览器第一次发出请求之后,需要再次发送请求的时候,浏览器首先获取该资源缓存的 header 信息,然后根据 Cache-Control 和 Expires 字段判断缓存是否过期。如果没有过期,直接使用浏览器缓存,并不会与服务器通信。该过程为判断是否使用强缓存,即本地缓存。
Cache-Control
设置是否缓存与缓存时间等,通用字段,请求头和响应头中都有
该字段是 HTTP1.1 规范,一般利用该字段的 max-age 属性来判断,这个值是一个相对时间,单位为 s,代表资源的有效期。例如:
1
Cache-Control:max-age=3600
除此之外还有几个常用的值:
- no-cache:表示不使用强缓存,需要使用协商缓存
- no-store:禁止浏览器缓存数据,每次请求下载完整的资源
- public:可以被所有用户缓存,包括终端用户和中间代理服务器
- private:只能被终端用户的浏览器缓存
Expires
该字段是 HTTP1.0 规范,他是一个绝对时间的 GMT 格式的时间字符串。例如:
1
expires:Mar, 06 Apr 2020 10:57:09 GMT
这个时间代表资源的失效时间,只要发送请求的时间在这之前,都会使用强缓存。
由于失效时间是一个绝对时间,因此当服务器时间与客户端时间偏差较大时,就会导致缓存混乱。
如果缓存过期,浏览器会向服务器发送请求,即 使用协商缓存。本次请求会带着第一次请求返回的有关缓存的 header 字段信息,比如以下两个字段:最后更新时间(last-modified
)和文件标识(ETag
)
Etag/If-None-Match
判断响应头中是否存在 Etag 字段,如果存在,浏览器则发送一个带有 If-None-Match 字段的请求头的请求,该字段的值为 Etag 值。服务器通过对比客户端发过来的Etag值是否与服务器相同。如果相同,说明缓存命中,服务器返回 304 状态码,并将 If-None-Match 设为 false,客户端继续使用本地缓存。如果不相同,说明缓存未命中,服务器返回 200 状态码,并将 If-None-Match 设为 true,并且返回请求的数据。
Last-Modified/If-Modified-Since
除了 Etag 字段之外,客户端还会通过服务器返回的 Last-Modified 字段判断是否继续使用缓存,该字段为服务器返回的资源的最后修改时间,为UMT时间。浏览器发送一个带有 If-Modified-Since 字段的请求头的请求给服务器,该字段的值为 Last-Modified 值。服务器收到之后,通过这个时间判断,在该时间之后,资源有无修改,如果未修改,缓存命中,返回 304 状态码;如果未命中,返回 200 状态码,并返回最新的内容。
Etag和Last-Modified是服务器响应头里的;请求头里会加上If-None-Match和If-Modified-Since给服务器用于判断是否采用协商缓存
Cache-Control 与 Expires 的优先级:
两者可以在服务端配置同时使用,Cache-Control 的优先级高于 Expires。
Last-Modified/If-Modified-Since 已经可以判断缓存是否失效了,为什么出现 Etag/If-None-Match?
Etag/If-None-Match 是实体标签,是一个资源的唯一标识符,资源的变化都会导致 ETag 的变化。出现 Etag 的主要原因是解决 Last-Modified 比较难解决的问题:
- 一些文件也许会周期性的修改,但是他的内容并不发生改变,这个时候我们并不希望客户端认为这个文件修改了
- 某些文件在秒以下的时间内进行修改了,If-Modified-Since无法判断。UNIX时间只能精确到秒
Last-Modified 和 Etag 可以一起使用, Etag 的优先级更高。
E-tag 的缺点
服务器需要计算Etag,会有性能损失
不同操作系统,web服务器对于ETag的计算方法也不同,当使用不同操作系统,不同类型的web服务器做负载均衡的时候,如果用ETag作为判断条件,在被负载均衡到不同服务器后,则很容易导致缓存失效。
刷新页面的问题:
F5刷新:不使用强缓存,使用协商缓存
ctrl+F5:二者都不使用
浏览器解析渲染页面
浏览器渲染步骤
(1)首先解析收到的文档(HTML文件),根据文档定义构建一棵 DOM 树,DOM 树是由 DOM 元素及属性节点组成的。

(2)然后对 CSS 进行解析,生成 CSSOM 规则树。

(3)根据 DOM 树和 CSSOM 规则树构建渲染树。渲染树的节点被称为渲染对象,渲染对象是一个包含有颜色和大小等属性的矩形,渲染对象和 DOM 元素相对应,但这种对应关系不是一对一的,不可见的 DOM 元素不会被插入渲染树。还有一些 DOM 元素对应几个可见对象,它们一般是一些具有复杂结构的元素,无法用一个矩形来描述。

(4)当渲染对象被创建并添加到树中,它们并没有位置和大小,所以当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流)。这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。通常这一行为也被称为“自动重排”。

(5)对渲染树进行分层,并生成分层树(Layer Tree)
并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层
满足下面两点中任意一点的元素就可以被提升为单独的一个图层:
拥有层叠上下文属性的元素会被提升为单独的一层(明确定位属性的元素、定义透明属性的元素、使用 CSS 滤镜的元素等,都拥有层叠上下文属性)
需要剪裁的地方也会被创建为图层(例如文字太多超出容器范围,或是具有滚动条)

(6)为每个图层生成绘制列表,并将其提交到合成线程(Paint)

(7)合成线程发送绘制图块命令 DrawQuad 给浏览器进程
首先,合成线程会将图层划分为图块。然后,合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的。
通常,栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。
一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。

(8)浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上
浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。

阻塞
阻塞关系
CSS
- CSS 不会阻塞 HTML 的解析,解析 HTML 和解析 CSS 是并行的;
- 页面渲染依照 CSS Tree +DOM Tree 合成 Render Tree,因此 CSS 会阻塞页面的渲染
- CSS 下载解析会阻塞 JS 执行,JS 需要读取 CSS 的结果
JS 会阻塞 HTML 解析:
- JS 可能会修改 DOM,需要阻塞 HTML 的解析
- JS 执行线程是单独的线程,和浏览器解析渲染线程互斥
解析页面过程中如果遇到一个 script 标签,会停止 HTML 解析,去下载 script 脚本,下载完毕之后立即执行脚本,然后接着解析 HTML,所以如果 script 下载速度很慢,会造成页面白屏。因此我们常常把 script 标签放到 body 底部。当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性。
async 和 defer 的作用是什么?有什么区别?
script :会阻碍 HTML 解析,只有下载好并执行完脚本才会继续解析 HTML。
async script :解析 HTML 过程中进行脚本的异步下载,下载成功立马执行,有可能会阻断 HTML 的解析。
defer script :完全不会阻碍 HTML 的解析,解析完成之后再按照顺序执行脚本。

渲染页面时常见哪些不良现象?
FOUC:主要指的是样式闪烁的问题,由于浏览器渲染机制(比如firefox),在 CSS 加载之前,先呈现了 HTML,就会导致展示出无样式内容,然后样式突然呈现的现象。会出现这个问题的原因主要是 css 加载时间过长,或者 css 被放在了文档底部。
白屏:有些浏览器渲染机制(比如chrome)要先构建 DOM 树和 CSSOM 树,构建完成后再进行渲染,如果 CSS 部分放在 HTML 尾部,由于 CSS 未加载完成,浏览器迟迟未渲染,从而导致白屏;也可能是把 js 文件放在头部,脚本的加载会阻塞后面文档内容的解析,从而页面迟迟未渲染出来,出现白屏问题。
生成 CSSOM 树的过程
1.当渲染引擎接收到 CSS 文本时,会执行一个转换操作,将 CSS 文本转换为浏览器可以理解的结构——styleSheets。

2.转换样式表中的属性值,使其标准化

3.计算出 DOM 树中每个节点的具体样式
在计算过程中需要遵守 CSS 的继承和层叠两个规则,被保存在 ComputedStyle 的结构内。

浏览器的绘制过程
什么是重绘、回流(重排)、合成?
重绘: 当渲染树中的一些元素需要更新属性,而这些属性只是影响元素的外观、风格,而不会影响布局的操作,比如 background-color,我们将这样的操作称为重绘。

回流:当渲染树中的一部分(或全部)因为元素的规模尺寸、布局、隐藏等改变而需要重新构建的操作,会影响到布局的操作,这样
的操作我们称为回流。

合成:更改一个既不要布局也不要绘制的属性,渲染引擎将跳过布局和绘制,只执行后续的合成操作。

常见引起回流属性和方法:
任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发回流。
(1)添加或者删除可见的 DOM 元素;
(2)元素尺寸改变——边距、填充、边框、宽度和高度
(3)内容变化,比如用户在 input 框中输入文字
(4)浏览器窗口尺寸改变——resize事件发生时
(5)计算 offsetWidth 和 offsetHeight 属性
(6)设置 style 属性的值
(7)当你修改网页的默认字体时。
回流必定会发生重绘,重绘不一定会引发回流。回流的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流。
常见引起重绘属性和方法:

常见引起回流属性和方法:

如何减少回流?
- 最小化重绘和重排,比如样式集中改变,使用添加新样式类名
.class
或cssText
。 - 批量操作 DOM,比如读取某元素
offsetWidth
属性存到一个临时变量,再去使用,而不是频繁使用这个计算属性;又比如利用document.createDocumentFragment()
来添加要被添加的节点,处理完之后再插入到实际 DOM 中。 - 使用 absolute 或 fixed 使元素脱离文档流,这在制作复杂的动画时对性能的影响比较明显。
- 开启 GPU 加速,利用 css 属性
transform
、will-change
等,比如改变元素位置,我们使用translate
会比使用绝对定位改变其left
、top
等来的高效,因为它不会触发重排或重绘,transform
使浏览器为元素创建⼀个 GPU 图层,这使得动画元素在一个独立的层中进行渲染。当元素的内容没有发生改变,就没有必要进行重绘。
注:will-change 用于告知浏览器该元素会有哪些变化的方法,这样浏览器可以在元素属性真正发生变化之前提前做好对应的优化准备工作。这种优化可以将一部分复杂的计算工作提前准备好,使页面的反应更为快速灵敏。(目前还处于实验阶段)
- opacity替代visibility: 前者回流重绘都不会触发(前提是它单独在一个图层,否则回流重绘都触发),后者触发重绘。
为什么操作 DOM 慢?
一些 DOM 的操作或者属性访问可能会引起页面的回流和重绘,从而引起性能上的消耗。
DOM 是属于渲染引擎中的东西,而 JS 又是 JS 引擎中的东西。当我们通过 JS 操作 DOM 的时候,其实这个操作涉及到了两个线程之间的通信,那么势必会带来一些性能上的损耗。
JS 解析
事件循环,放在 JS 部分里