# 从输入URL到页面加载过程
抛开了对页面的具体分析,任何的性能优化都是站不住脚的,盲目的使用一些优化措施,结果可能会适得其反。因此切实的去分析页面的实际性能表现,不断的改进测试,才是正确的优化途径。
# 大致流程
- 强缓存(第一步)
- 域名解析(DNS查询)
- (SSL/TLS)握手(https会有)
- TCP三次握手
- 发送请求(应用层)
- 服务器收到请求
- 协商缓存(服务器告诉浏览器使用本地缓存)
- 微服务架构 -> 负载均衡
- 服务器处理请求 并返回响应
- TCP四次挥手(关闭连接)
- DOM树 + CSS树 -> 渲染树 -> 重排、重绘
- 边加载边解析的过程
- 会将各层信息发送给 GPU,GPU将各层合成,显示到屏幕上
- JS资源要等脚本下载完成并执行后才会继续解析HTML(此时可以使用defer和async)
- defer是延迟执行。类似放在body后面
- async是异步执行。下载完毕执行
# 网络传输过程
# 域名解析
IP 查找顺序(查询到就返回)
- 首先从
浏览器缓存
中查找 IP - 从
Host文件
中查找 IP - 从
路由器
缓存中查找 IP - 发送
DNS请求
到 本地DNS服务器(运营商) - 从
本地DNS服务器
缓存中查找 IP - 以
递归
的方式往根DNS服务器
发起请求 - 以
迭代
的方式获取能查询的顶级域名服务器
位置 - 顶级服务器告诉本地DNS到
权限服务器
上查询 - 权限服务器将IP返回给本地DNS
- 本地DNS将IP保存到自己的缓存中
概念:
- DNS 即 (domain name system,域名系统),一个域名和IP地址相互映射的分布式数据库。
- 根域名:全球共13个根服务器 (包含所有顶级域名服务器的域名和IP地址)
- 顶级域名:域名的最后一部分(如:.com、.cn、.net 等)
- 二级域名:域名的倒数第二个部分,如:example.baidu.com中,二级域名是Baidu
# HTTPS
HTTPS(全称:Hyper Text Transfer Protocol over Secure Socket Layer 超文本传输安全协议)
HTTPS在传统的HTTP和TCP之间加了一层用于加密解密的SSL/TLS层
(安全套接层Secure Sockets Layer/安全传输层Transport Layer Security)层。
PS: 当SSL
发展到第三大版本时才被标准版,成为 TLS
。即:TLS1.0 = SSL3.1
使用HTTPS必须要有一套自己的数字证书(包含公钥和私钥)。
HTTPS解决的问题
- 信息加密传输:第三方无法窃听;
- 校验机制:一旦被篡改,通信双方会立刻发现;
- 身份证书:防止身份被冒充。
HTTPS加密过程:
- 客户端请求服务器获取
证书公钥
- 客户端(SSL/TLS)解析证书(无效会弹出警告)
- 生成随机值
- 用
公钥加密
随机值生成密钥 - 客户端将
秘钥
发送给服务器 - 服务端用
私钥
解密秘钥
得到随机值 将信息和随机值混合在一起
进行对称加密- 将加密的内容发送给客户端
- 客户端用
秘钥
解密信息
加密过程使用了对称加密和非对称加密。
- 对称加密: 客户端和服务端采用相同的密钥经行加密
encrypt(明文,秘钥) = 密文 decrypt(密文,秘钥) = 明文
- 非对称加密:客户端通过公钥加密。服务端通过私钥解密
encrypt(明文,公钥) = 密文 decrypt(密文,私钥) = 明文
因为TLS握手的过程中采用了非对称加密,客户端本身不知道服务器的秘钥,这样通信就不会被中间人劫持。此外这一步服务端还提供了证书,并且可能要求客户端提供证书。关于证书下文会提到,只要有了证书,就能保证和你通信的对方是真实的,而不是别人伪造的。
那然后验证证书呢?
- 客户端获取到了站点证书,拿到了站点的公钥
- 客户端找到其站点证书颁发者的信息
站点证书的颁发者
验证服务端站点
是否可信- 往上回溯,找到
根证书颁发者
- 通过
根证书颁发者
一步步验证站点证书颁布者
是否可信
附:
- HTTPS默认使用443端口,而HTTP默认使用80端口。
- TLS就是从SSL发展而来的,只是SSL发展到3.0版本后改成了TLS
- 第一次请求中TLS握手的代价很大
- 后续的请求会共用第一次请求的协商结果
参考:
# HTTP2
主要改进
- 头部压缩 -> 减少体积
- 采用
HPACK
压缩:利用服务器和客户端之间建立哈希表的映射,传递索引来精简和复用请求头部
- 采用
- 多路复用 -> 解决队头阻塞
- 由于浏览器对HTTP有并发限制(大部分是6个并发),而且HTTP 基于请求-响应的模型,在同一个 TCP 长连接中,前面的请求没有得到响应,后面的请求就会被阻塞。
- 因此,HTTP2 采用多路复用解决HTTP队头阻塞的问题(只需要占用一个 TCP 连接)
- 二进制 + 分帧传输 -> 减少体积 + 提高安全性
- 由于 HTTP 的明文传输解析太过复杂(比如
\n
到底是换行还是字符串?),而且并不安全。所以,采用二进制进行传输 - 将数据以流的形式进行传输,并将请求和响应数据分割成更小的帧,而多个帧之间可以乱序发送,根据帧首部的流标识来重新组装。
- 由于 HTTP 的明文传输解析太过复杂(比如
- 服务器推送
PS:2020年2月chrome更改了SameSite
属性(从默认 None
改为 Lax
),用以预防CSRF
攻击。
# TCP三次握手
客户端请求建立连接
服务端确认应答
客户端确认应答
→ ← →
三次握手期间,任何1次未收到对面的回复,则都会重发
目的:应对网络延迟问题,防止网络资源浪费,甚至死锁
# TCP四次挥手
客户端请求断开连接
服务端应答
服务端请求断开连接
客户端应答
→ ← ← →
目的:保证双方都断开连接
# 关于缓存
浏览器缓存策略相关:比如Cache-Control、Pragma、ETag、Expires、Last-Modified
强缓存是利用Expires
或者Cache-Control
这两个http header实现的,命中缓存会返回200
- 强缓存是不会产生 DNS 解析的,更不会发送请求(不请求服务器)
expires
有 服务器时间 和 客户端时间 不一致导致失效的问题Cache-Control
在HTTP 1.1
中为了解决expires的问题而诞生- 单位为秒,不依赖客户端时间
Cache-Control
优先级高于Expires
协商缓存利用Last-Modified
或者Etag
这两个http header实现,命中缓存会返回304
- 协商缓存由服务器验证缓存的有效性(请求服务器)
- Last-Modified 比较前一个响应头的
Last-Modified
和新请求头的if-modified-since
(单位秒)- 根据时间来缓存
- 最后修改只能精确到
秒
级 - 定期生成文件内容没变化时 Last-Modified 改变
- Etag在
HTTP 1.1
中出现:比较前一个响应头的Etag
和新请求头的If-None-Match
,- 优先级高于Last-Modified
- 基于资源的内容编码生成一串唯一的标识
字符串
来缓存 ETag
优先级高于Last-Modified
在文件变动的时候需要清除缓存。比如:在webpack打包的时候一般会给JS、CSS、图片的文件名添加chunkhash
。
# 关于浏览器进程
浏览器是多进程的
多进程的优势
- 避免单个标签页影响整个浏览器
- 避免第三方插件影响整个浏览器
- 利用 cpu 的多核优势
浏览器进程:主要进程
- 主进程(Browser进程/控制进程)
- 负责界面展示,与用户交互(前进、后退等)
- 负责各个页面的管理,创建和销毁其他进程
- 将渲染经常的结果绘制到页面上
- 网络资源的管理、下载等
- 插件进程
- 为浏览器插件创建的进程
- GPU进程
- 用于 3D 绘制
- 渲染进程
- 页面渲染,脚本执行,事件处理
- 每个浏览器标签页都是一个 render进程
所以:打开浏览器最少会出现2个进程(主进程和标签页的渲染进程)
PS:浏览器还有 SharedWorker
进程,方便各页面间的交互
# 渲染进程
渲染进程主要包含以下线程: 主要线程
- GUI 渲染线程
- 负责渲染浏览器页面(解析HTML、CSS 构建DOM和 RenderObject树,布局和绘制等)
- 当然:重绘(Repaint)和回流(Reflow) 也会触发该线程
- 重点:GUI渲染线程和JS引擎线程是互斥的,会在JS引擎空闲时执行
- JS 引擎线程
- JS内核(例如: V8引擎):负责解析、运行 JS脚本程序
- 如果使用
WebWorker
那么JS引擎线程会 向浏览器申请开一个子线程WebWorker
是渲染进程开的子线程,不受JS引擎线程管理- JS线程 和 Worker线程的通信通过
postMessage API
postMessage
交互数据需要序列化对象
- 事件触发线程
- 宏任务、微任务、事件(比如点击)、AJAX请求等 被事件符合条件触发时。会将任务添加到事件线程中
- 事件触发线程会把事件添加到待处理队列的队尾,等待 JS 引擎处理
- 定时器线程
- 定时器所在的线程(
setInterval
和setTimeout
等) - 因为JS引擎线程处于阻塞状态会影响计时的准确性,所以单独作为一个线程
- W3C标准:setTimeout最小间隔为 4ms
- 定时器所在的线程(
- 异步http请求线程
- 处理请求的线程,如果有请求回调函数,会将该回调放入事件队列中,由 JS引擎执行
PS:
DOMContentLoaded
事件触发时:仅DOM
加载完成(不包括css、图片资源、async脚本)onload
事件触发时:所有资源都加载完成且渲染完毕
# 页面渲染
CSS加载是否阻塞渲染?
- CSS加载 不会阻塞 DOM树的解析
- CSS加载 会阻塞 Render树渲染
- css加载 会阻塞 后续js语句的执行
为什么加载不阻塞 DOM 树的解析,但是会阻塞 Render 树的渲染?
- CSS的加载是由主进程的下载线程异步下载的
- 因为解析过程不影响 渲染流程
- 渲染时需要等CSS加载完毕,以获取CSS信息
- 如果css加载不阻塞DOM树渲染的话,那么当css加载完之后,DOM树可能又得重新重绘或者回流了
为何 GUI渲染线程 和 JS引擎线程会产生互斥?
因为JavaScript是可以操作DOM的,如果再修改这些元素属性的同时渲染页面,那么渲染线程获得的元素数据可能会不一致,(如果不一致那么渲染线程怎么知道该做什么事情?),因此为防止渲染出现异常,所以当JS引擎执行时 GUI线程 会被挂起,GUI更新会等JS引擎线程空闲时执行。
# css图层
分为:普通图层和复合图层
复合图层就是传说中的硬件加速技术,独立于普通文档流中,改动后可以避免整个页面重绘,提升性能,但是也不能大量使用,会导致资源消耗过度。
如果变成复合图层?
translate3d
、translateZ
will-change
(提前告诉浏览器需要优化) 和opacity
/过渡动画(如:transform) 在动画期间是复合图层video
、iframe
、canvas
、webgl
PS:
absolute
虽然可以脱离普通文档流,但是无法脱离默认复合层。- 复合图层的隐式合成:如果a是一个复合图层,而且b在a上面,那么b也会被隐式转为一个复合图层