网站访问流程

总体

  • URL解析

  • DNS解析

  • TCP连接

  • 发送HTTP请求

  • 服务器处理请求

  • 服务器响应请求

  • 浏览器解析渲染页面

URL 解析

  1. 检查 URL 是否合法,并根据你输入的内容进行自动完成、字符编码等操作

  2. 由于安全隐患,会使用 HSTS 强制客户端使用 HTTPS 访问页面

  3. 进行如安全检查、访问限制

  4. 检查缓存

DNS 查询

https://ex.com, 被定位到IP地址为 93.184.216.34 的服务器。以前没有访问过这个网站,就需要进行DNS查找

浏览器通过服务器名称请求DNS进行查找,最终返回一个IP地址,第一次请求之后,这个IP地址可能会被缓存一段时间,这段时间内可以通过从缓存里面检索IP,加速后续的请求

通过主机名加载一个页面通常仅需要DNS查找一次。但 DNS 需要对不同的页面指向的主机名进行查找,如fonts, images, scripts 都不同的主机名,DNS会对每一个进行查找(DNS预加载优化dns-prefetch)

  1. 先检查浏览器缓存中有没有对应域名解析的IP地址,有则返回

  2. 否则到操作系统的hosts文件中查找,有则返回

  3. 否则到路由器缓存查找,有则返回

  4. 否则向外网的本地区域名服务器(运营商提供)发起查询请求,本地服务器收到请求之后,会先查询本地缓存,有则返回

  5. 否则会向跟域名服务器发起请求,根域名返回来的是一个所查询域(根的子域,.com)的主域名服务器的地址

  6. 接着,本地服务器再向上一步(4步)返回来的域名服务器下发送请求

  7. 得返回此域名对应的Name Server域名服务器的地址,通常就是你注册的域名服务器

  8. 得到Name Server 服务器地址之后,Local DNS 再次向Name server 服务器发送请求,域名服务器会查询存储的域名和IP的映射关系表,得到目标IP记录

基本流程图

递归查找

一路查下去中间不返回,得到最终结果才返回信息(浏览器到本地DNS服务器的过程)

DNS 劫持

通过某种手段,使用户在输入网址后,无法访问到正确的 DNS 服务器

  • 本机DNS劫持: 用户的计算机感染上木马病毒,或者恶意软件之后,恶意修改本地DNS配置,比如修改本地hosts文件,缓存等

  • 路由DNS劫持: 路由器密码被破解,攻击者可以侵入到路由管理员账号中,修改路由器的默认配置

  • 攻击DNS服务器:直接攻击DNS服务器,例如对DNS服务器进行DDOS攻击,可以是DNS服务器宕机,出现异常请求,还可以利用某些手段感染dns服务器的缓存,使给用户返回来的是恶意的ip地址

前端DNS预解析

DNS Prefetching 是让具有此属性的域名不需要用户点击链接就在后台解析,减少用户的等待时间,提升用户体验

1
2
3
4
5
6
7
// HTTPS页面开启自动解析功能
<meta http-equiv="x-dns-prefetch-control" content="on">

// 手动添加解析
<link rel="dns-prefetch" href="http://www.google.com">

// 通过js初始化一个iframe异步加载一个页面,而这个页面里包含本站所有的需要手动dns prefetching的域名

针对那些域名

1、静态资源域名cdn

2、JS里会发起跳转的域名

3、会重定向的域名

使用场景

1、新用户访问

2、登录页,提前在处理下一跳页用到资源的 DNS Prefetch

过多的prefetch并不一定能提高网页加载效率

TCP 连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 七层协议和四层

+-----------------+
| 应用层 | —————————— // 文件传输(利用FTP机器间拷贝传输),电子邮件,HTTP,FTP,DNS,Telnet
+-----------------+ |
| |
+-----------------+ |
| 表示层 | ————————-- 应用层 // 处理编码、数据格式转换和加密解密 https
+-----------------+ |
| |
+-----------------+ |
| 会话层 | —————————— // 组织和协调两个会话进程之间的通信,并对数据交换进行管理
+-----------------+
|
+-----------------+
| 运输层 | // 通信子网和资源子网的接口和桥梁,向用户提供可靠的端到端的差错和流量控制,保证报文的正确传输
+-----------------+
|
+-----------------+
| 网络层 | // mac地址和ip,区分不同计算机是否在同一个子网
+-----------------+
|
+-----------------+
| 数据链路 | —————————— // 确定了0和1分组,在实体层上面,规定解读电平信号的方式规则
+-----------------+ |
| | 物理链路层 // 线路,无线,光纤等,传输0和1信号
+-----------------+ |
| 物理层 | ——————————
+-----------------+

图片来自于这里

数据包:从上往下,每经过一层,协议就会在包头上面做点手脚,加点东西,传送到接收端,再层层解套出来

1. 应用层:发送 HTTP 请求

  • 上一步得到服务器 IP 地址,浏览器开始构造一个 HTTP 报文,包括:请求报头、请求方法、目标地址、遵循的协议,请求主体(其他参数)

  • 浏览器只能发送 GET、POST 方法,而打开网页使用的是 GET 方法

2. 传输层:TCP 传输报文

  • 传输层会发起一条到达服务器的 TCP 连接,为方便传输,对数据进行分割(段为单位),并编号,方便服务器准确地还原报文信息

  • 在建立连接前,会先进行 TCP 三次握手

三次握手建立连接

TCP的”三次握手“技术经常被称为”SYN-SYN-ACK“—更确切的是 SYN, SYN-ACK, ACK— 通过TCP首先发送了三个消息进行协商,开始一个TCP会话在两台电脑之间,意味着每台服务器之间还要来回发送三条消息,而请求尚未发出

图片来自这里

  1. 开始客户端和服务端都处于 CLOSED 状态 -> 先是服务端主动监听某个端口,处于 LISTEN 状态

  2. 第一个 SYN 报文:客户端会随机初始化序号(client_isn),将此序号置于 TCP 首部的「序号」字段中,同时把 SYN 标志位置为 1 。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN-SENT 状态

  3. 第二个 SYN + ACK 报文
    服务端收到客户端的 SYN 报文后,首先服务端也随机初始化自己的序号,将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入 client_isn + 1, 接着把 SYN 和 ACK 标志位置为 1。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD 状态

  4. 第三个 ACK 报文
    客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次「确认应答号」字段填入 server_isn + 1 ,最后把报文发送给服务端,这次报文可以携带客户到服务器的数据,之后客户端处于 ESTABLISHED 状态

第三次握手是可以携带数据的,前两次握手是不可以携带数据的

3. HTTPS 的 TLS 协商

这里

为了在HTTPS上建立安全连接,另一种握手是必须的。更确切的说是TLS协商 ,它决定了什么密码将会被用来加密通信,验证服务器,在进行真实的数据传输之前建立安全连接。在发送真正的请求内容之前还需要三次往返服务器

3. 网络层:IP协议查询Mac地址

  • 将数据段打包,并加入源及目标的IP地址,并且负责寻找传输路线

  • 判断目标地址是否与当前地址处于同一网络中,是的话直接根据 Mac 地址发送,否则使用路由表查找下一跳地址,以及使用 ARP 协议查询它的 Mac 地址

4. 链路层:以太网协议

  • 把数据分割成若干小块作为传输单位进行发送(数据包)

  • 以太网规定连入网络的所有设备都必须具备“网卡”接口,数据包都是从一块网卡传递到另一块网卡,网卡的地址就是 Mac 地址。每一个 Mac 地址都是独一无二的

服务器处理请求

  • 接受 TCP 报文后,会对连接进行处理,对HTTP协议进行解析(请求方法、域名、路径等),并且进行一些验证

  • 如果需要重定向,则重定向

  • 请求的是否是真实资源,是返回资源,否返回API响应

浏览器接受响应

浏览器接收到来自服务器的响应资源后,会对资源进行分析

  • 解压缩(比如 gzip)

  • 根据不同状态码做不同的事(重定向)

  • 对响应资源做缓存

  • 根据资源的 MIME 类型去解析不同内容(js, img, css)

解析渲染页面

“解析”是浏览器将通过网络接收的数据转换为DOM和CSSOM的步骤,通过渲染器把DOM和CSSOM在屏幕上绘制成页面

解析HTML生成DOM树

浏览器解析是从上往下一行一行地解析的

  1. 解码:传输回来的其实都是一些二进制字节数据,浏览器根据文件指定编码(例如UTF-8)转换成字符串(HTML)

  2. 预解析:提前加载资源,减少处理时间,会识别一些会请求资源的属性(img的src),并将这个请求加到请求队列中

  3. 符号化(Tokenization):词法分析的过程,将输入解析成符号(开始|结束标签、属性名、属性值等)

  4. 构建DOM树,树反映了不同标记之间的关系和层次结构。嵌套在其他标记中的标记是子节点。DOM节点越多,构建DOM树时间越长

  5. 浏览器构建DOM树时,这个过程占用了主线程

注意

  • 如果文档格式良好,则解析它会简单而快速(语义化)

  • DOM节点越多,构建DOM树时间越长(嵌套过深)

  • 当解析器发现非阻塞资源(图片),浏览器会请求这些资源并且继续解析。当遇到一个CSS文件时,解析也可以继续进行

  • 但script标签(没有 async 或 defer 属性)会阻塞渲染并停止HTML的解析。浏览器的预加载扫描器会加速这个过程,但过多仍是瓶颈

预加载扫描仪

构建DOM树时,占用了主线程。此时,预加载扫描仪将解析可用的内容并请求高优先级资源,如CSS、JavaScript和web字体。它将在后台检索资源,以便在主HTML解析器到达请求的资源时,它们可能已经在运行,或者已经被下载

1
2
3
4
<link rel="stylesheet" src="styles.css"/>
<script src="myscript.js" async></script>
<img src="myimage.jpg" alt="image description"/>
<script src="anotherscript.js" async></script>

主线程在解析HTML和CSS时,预加载扫描器将找到脚本和图像,并开始下载它们。为了确保脚本不会阻塞进程,当JavaScript解析和执行顺序不重要时,可以添加async属性或defer属性

等待获取CSS不会阻塞HTML的解析或者下载,但是它的确阻塞JavaScript,因为JavaScript经常用于查询元素的CSS属性

解析CSS生成CSSOM规则树

  • DOM和CSSOM是两棵树,它们是独立的数据结构,浏览器遍历CSS中的每个规则集,根据CSS选择器创建具有父、子和兄弟关系的节点树

  • 匹配一个节点对应的 CSS 规则时,是按照从右到左的顺序的,例如:div p {}会先寻找所有的p标签然后判断它的父元素是否为div

所以有如下 css 注意事项

1
2
3
4
5
6
7
// 避免最右侧选择器范围高于左侧,毫无疑问 div 数量远远大于 id,这种写法性能较差

#id div {}

// 避免层级太深

#id .class1 .class2 .class3 .class4 .class5 {}

渲染阻塞

  • 当浏览器遇到一个 script 标记时,DOM 构建将暂停,直至脚本完成执行,然后继续构建DOM

  • 每次去执行JavaScript脚本都会严重地阻塞DOM树的构建,如果JavaScript脚本还操作了CSSOM,而正好这个CSSOM还没有下载和构建,浏览器甚至会延迟脚本执行和构建DOM,直至完成其CSSOM的下载和构建

script 标签的位置很重要

  • CSS 优先:引入顺序上,CSS 资源先于 JavaScript 资源

  • JS置后:我们通常把JS代码放到页面底部,且JavaScript 应尽量少影响 DOM 的构建

将DOM树与CSSOM规则树合并在一起生成渲染树

DOM 树和 CSS 规则树合并的过程,渲染树会忽略那些不需要渲染的节点,比如设置了display:none的节点

遍历渲染树开始布局,计算每个节点的位置大小信息

布局阶段会从渲染树的根节点开始遍历,然后确定每个节点对象在页面上的确切大小与位置,布局阶段的输出是一个盒子模型,它会精确地捕获每个元素在屏幕内的确切位置与大小

将渲染树每个节点绘制到屏幕

在绘制阶段,遍历渲染树,调用渲染器的paint()方法在屏幕上显示其内容。渲染树的绘制工作是由浏览器的 UI后端组件完成的

webkit 和 gecko

webkit

gecko

测试css的影响

// bootstrap 4m大小,将网速调整为3g

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script type="text/javascript">
setTimeout(() => {
console.log(document.getElementById('root'))
})
</script>
<link rel="stylesheet" href="./bootstrap.css">
<body>
<div id="root">过大的css是否阻塞dom展示呢?</div>
<script type="text/javascript">
console.log('scripts 脚本是否被css阻塞呢?')
</script>
</body>
</html>

根据下面的图片得出三点

  • css 加载没有阻塞 DOM 解析,此时css没加载完,但是打印出来 div 标签,说明DOM解析成功了

  • 虽然DOM树解析了,但是页面依然空白,说明渲染被阻塞

  • script 脚本的 console 没有被输出,所以阻塞了js的解析

浏览器每一帧做什么

  • 目前大多数设备的屏幕刷新率为 60 次/秒,每个帧的预算时间仅比 16 毫秒多一点 (1 秒/ 60 = 16.66 毫秒)

  • 如果在页面中有一个动画或渐变效果,那么浏览器渲染动画或页面的每一帧的速率也需要跟设备屏幕的刷新率保持一致

  • 浏览器并不需要执行所有步骤。如果没有新的 HTML 要解析,那么解析 HTML 的步骤就不会触发

  1. 开始新的一帧:垂直同步信号触发,开始渲染新的一帧图像

  2. 输入事件和用户交互事件处理:所有的事件处理函数(touchmove,scroll,click)都应该最先触发

  3. requestAnimationFrame:样式计算,在本次任务之后,如果你改变了很多样式,不会引起多次样式计算,所以是变更元素的理想位置,它们会在稍后被批量处理。注意:不要查询进行计算才能得到的样式或者布局属性(el.style.offsetWidth)。会导致重新计算样式,或者布局,或者二者都发生,进一步导致强制同步布局,乃至布局颠簸

  4. 解析 HTML(Parse HTML):处理新添加的 HTML,创建 DOM 元素

  5. 重新计算样式(Recalc Styles):为新添加或变更的内容计算样式,根据css选择器计算出哪些元素应用哪些 CSS 规则

  6. 布局(Layout):在知道一个元素应用哪些规则之后,和它要占据的空间大小及其在屏幕的位置

  7. Paint: 第一步,对所有新加入的元素,或进行改变显示状态的元素,记录 draw 调用,创建绘图调用的列表;第二步是栅格化(Rasterization),在这一步实际执行了 draw 的调用,并进行填充像素

  8. 合成(Composite):页面的各部分可能被绘制到多层,由此它们需要按正确顺序绘制到屏幕上,以便正确渲染页面

  9. 帧结束:各个层的所有的块都被栅格化成位图后,新的块和输入数据(可能在事件处理程序中被更改过)被提交给 GPU 线程

  10. 发送帧:图块被 GPU 线程上传到 GPU。GPU 使用四边形和矩阵(所有常用的 GL 数据类型)将图块 draw 在屏幕上

  11. 本帧 还有剩余时间,执行requestIdleCallback

结合谷歌开发者文档提到的 render performance

工作时需要注意五个主要区域

javascript -> style -> layout -> paint -> composite

在实现视觉变化时,管道针对指定帧的运行通常有三种方式:

修改元素的“layout”属性,例如改变了元素的几何属性(例如宽度、高度、左侧或顶部位置等),那么浏览器将必须检查所有其他元素,然后“自动重排”页面

  • javascript -> style -> layout -> paint -> composite

只改“paint only”属性(例如背景图片、文字颜色或阴影等),即不会影响页面布局的属性,则浏览器会跳过布局,但是会重绘

  • javascript -> style -> paint -> composite

更改一个既不要布局也不要绘制的属性,则浏览器将跳到只执行合成(transform 和 opacity)

  • javascript -> style -> composite

其实从上面我们也很清楚的明白一些事情

性能开销: 重排 > 重绘 > 直接合成

怎么避免

  • 尽量减少重排,也就是位置,大小,布局的改变

  • 坚持使用 transform 和 opacity 属性(使其变成复合图层,开启GPU硬件加速)更改来实现动画;Chrome源码调试 -> More Tools -> Rendering -> Layer borders黄色是复合图层;如果a是一个复合图层,而且b在a上面,那么b也会被隐式转为一个复合图层,这种情况需要避免,因此复合图层尽量使用 z-index 使其在最上方。

  • 使用 will-change 或 translateZ 提升移动的元素(ios层叠兼容问题,A,B兄弟元素B的层级高于A,A子元素的z-index无论多少无法覆盖B元素,可以使用translateZ)

  • 避免过度使用提升规则;各层都需要内存和管理开销

优化javascript

1、对于动画,避免 setTimeout 或 setInterval,请使用 requestAnimationFrame

requestAnimationFrame 除了与浏览器刷新频率一致这一特点外,还有就是正好在帧的开头,css 样式布局等未发生计算。避免了多次计算的情况

2、将长时间运行的 JavaScript 从主线程移到 Web Worker(兼容性要考虑)

  • Web Worker 新开线程,存在兼容问题,需要考量

  • 没有DOM访问权限

  • 可以处理大量的数据,避免阻塞页面

  • 主要 api,发送消息 postMesssage,接收消息 onmessage 或 addEventListener(‘message’, () =>{})

3、使用微任务来执行对多个帧的 DOM 更改

  • 将大任务,分成微任务,时间分片设计

强制布局布局抖动

强制布局

将一帧送到屏幕会采用如下顺序 javascript -> style -> layout -> paint -> composite,但可以使用 JavaScript 强制浏览器提前执行布局。这被称为强制同步布局

如果在获取某个元素的高度之前,已更改其样式

1
2
3
4
5
6
7
8
9
10
11
// 错误
function getBoxHeight() {
box.classList.add('super-big');
console.log(box.offsetHeight);
}

// 正确
function getBoxHeight() {
console.log(box.offsetHeight);
box.classList.add('super-big');
}

为了获取高度,必须先应用样式更改(增加super-big类),然后运行布局。这时它才能返回正确的高度。这是不必要的,并且开销很大

布局抖动:大量的进行强制布局操作

布局(也就是重排)改变几乎总是作用到整个文档

  • 循环的每次迭代读取一个样式值 (box.offsetWidth),然后立即使用此值来更新段落的宽度 (paragraphs[i].style.width)

  • 在循环的下次迭代时,浏览器必须考虑样式已更改这一事实,因为 offsetWidth 是上次请求的(在上一次迭代中),因此它必须应用样式更改,然后运行布局,拿到更新的宽度(其实宽度的值并没变化)

1
2
3
4
5
6
7
8
9
10
// 错误方法
for (var i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = box.offsetWidth + 'px';
}

// 修正方法先读后写
var width = box.offsetWidth;
for (var i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = width + 'px';
}

资料

浏览器每一帧

The Anatomy of a Frame

视频

渲染

返回
顶部