# 从输入URL到页面加载过程

抛开了对页面的具体分析,任何的性能优化都是站不住脚的,盲目的使用一些优化措施,结果可能会适得其反。因此切实的去分析页面的实际性能表现,不断的改进测试,才是正确的优化途径。

# 大致流程

  1. 强缓存(第一步
  2. 域名解析(DNS查询)
  3. (SSL/TLS)握手(https会有)
  4. TCP三次握手
  5. 发送请求(应用层)
  6. 服务器收到请求
  7. 协商缓存(服务器告诉浏览器使用本地缓存
  8. 微服务架构 -> 负载均衡
  9. 服务器处理请求 并返回响应
  10. TCP四次挥手(关闭连接)
  11. DOM树 + CSS树 -> 渲染树 -> 重排、重绘
    • 边加载边解析的过程
    • 会将各层信息发送给 GPU,GPU将各层合成,显示到屏幕上
  12. JS资源要等脚本下载完成并执行后才会继续解析HTML(此时可以使用defer和async)
    • defer是延迟执行。类似放在body后面
    • async是异步执行。下载完毕执行

# 网络传输过程

因特网

# 域名解析

IP 查找顺序(查询到就返回)

  1. 首先从浏览器缓存中查找 IP
  2. Host文件中查找 IP
  3. 路由器缓存中查找 IP
  4. 发送DNS请求到 本地DNS服务器(运营商)
  5. 本地DNS服务器缓存中查找 IP
  6. 递归的方式往根DNS服务器发起请求
  7. 迭代的方式获取能查询的顶级域名服务器位置
  8. 顶级服务器告诉本地DNS到权限服务器上查询
  9. 权限服务器将IP返回给本地DNS
  10. 本地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加密过程:

  1. 客户端请求服务器获取证书公钥
  2. 客户端(SSL/TLS)解析证书(无效会弹出警告)
  3. 生成随机值
  4. 公钥加密随机值生成密钥
  5. 客户端将秘钥发送给服务器
  6. 服务端用私钥解密秘钥得到随机值
  7. 将信息和随机值混合在一起进行对称加密
  8. 将加密的内容发送给客户端
  9. 客户端用秘钥解密信息

HTTPS

加密过程使用了对称加密和非对称加密。

  • 对称加密: 客户端和服务端采用相同的密钥经行加密
        encrypt(明文,秘钥) = 密文
    
        decrypt(密文,秘钥) = 明文
    
  • 非对称加密:客户端通过公钥加密。服务端通过私钥解密
        encrypt(明文,公钥) = 密文
    
        decrypt(密文,私钥) = 明文
    

因为TLS握手的过程中采用了非对称加密,客户端本身不知道服务器的秘钥,这样通信就不会被中间人劫持。此外这一步服务端还提供了证书,并且可能要求客户端提供证书。关于证书下文会提到,只要有了证书,就能保证和你通信的对方是真实的,而不是别人伪造的。

那然后验证证书呢?

  1. 客户端获取到了站点证书,拿到了站点的公钥
  2. 客户端找到其站点证书颁发者的信息
  3. 站点证书的颁发者验证服务端站点是否可信
  4. 往上回溯,找到根证书颁发者
  5. 通过根证书颁发者一步步验证站点证书颁布者是否可信

附:

  • HTTPS默认使用443端口,而HTTP默认使用80端口。
  • TLS就是从SSL发展而来的,只是SSL发展到3.0版本后改成了TLS
  • 第一次请求中TLS握手的代价很大
  • 后续的请求会共用第一次请求的协商结果

参考:

# HTTP2

主要改进

  1. 头部压缩 -> 减少体积
    • 采用HPACK压缩:利用服务器和客户端之间建立哈希表的映射,传递索引来精简和复用请求头部
  2. 多路复用 -> 解决队头阻塞
    • 由于浏览器对HTTP有并发限制(大部分是6个并发),而且HTTP 基于请求-响应的模型,在同一个 TCP 长连接中,前面的请求没有得到响应,后面的请求就会被阻塞。
    • 因此,HTTP2 采用多路复用解决HTTP队头阻塞的问题(只需要占用一个 TCP 连接)
  3. 二进制 + 分帧传输 -> 减少体积 + 提高安全性
    • 由于 HTTP 的明文传输解析太过复杂(比如 \n 到底是换行还是字符串?),而且并不安全。所以,采用二进制进行传输
    • 将数据以流的形式进行传输,并将请求和响应数据分割成更小的帧,而多个帧之间可以乱序发送,根据帧首部的流标识来重新组装。
  4. 服务器推送

PS:2020年2月chrome更改了SameSite 属性(从默认 None 改为 Lax),用以预防CSRF 攻击。

# TCP三次握手

  1. 客户端请求建立连接

  2. 服务端确认应答

  3. 客户端确认应答

    → ← →

三次握手期间,任何1次未收到对面的回复,则都会重发

目的:应对网络延迟问题,防止网络资源浪费,甚至死锁

# TCP四次挥手

  1. 客户端请求断开连接

  2. 服务端应答

  3. 服务端请求断开连接

  4. 客户端应答

    → ← ← →

目的:保证双方都断开连接

# 关于缓存

浏览器缓存策略相关:比如Cache-Control、Pragma、ETag、Expires、Last-Modified

强缓存是利用Expires或者Cache-Control这两个http header实现的,命中缓存会返回200

  • 强缓存是不会产生 DNS 解析的,更不会发送请求(不请求服务器
  • expires服务器时间客户端时间 不一致导致失效的问题
  • Cache-ControlHTTP 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

参考

# 关于浏览器进程

浏览器是多进程的

多进程的优势

  1. 避免单个标签页影响整个浏览器
  2. 避免第三方插件影响整个浏览器
  3. 利用 cpu 的多核优势

浏览器进程:主要进程

  1. 主进程(Browser进程/控制进程)
    • 负责界面展示,与用户交互(前进、后退等)
    • 负责各个页面的管理,创建和销毁其他进程
    • 将渲染经常的结果绘制到页面上
    • 网络资源的管理、下载等
  2. 插件进程
    • 为浏览器插件创建的进程
  3. GPU进程
    • 用于 3D 绘制
  4. 渲染进程
    • 页面渲染,脚本执行,事件处理
    • 每个浏览器标签页都是一个 render进程

所以:打开浏览器最少会出现2个进程(主进程和标签页的渲染进程)

PS:浏览器还有 SharedWorker进程,方便各页面间的交互

# 渲染进程

渲染进程主要包含以下线程: 主要线程

  1. GUI 渲染线程
    • 负责渲染浏览器页面(解析HTML、CSS 构建DOM和 RenderObject树,布局和绘制等)
    • 当然:重绘(Repaint)和回流(Reflow) 也会触发该线程
    • 重点:GUI渲染线程和JS引擎线程是互斥的,会在JS引擎空闲时执行
  2. JS 引擎线程
    • JS内核(例如: V8引擎):负责解析、运行 JS脚本程序
    • 如果使用 WebWorker 那么JS引擎线程会 向浏览器申请开一个子线程
      • WebWorker 是渲染进程开的子线程,不受JS引擎线程管理
      • JS线程 和 Worker线程的通信通过 postMessage API
      • postMessage 交互数据需要序列化对象
  3. 事件触发线程
    • 宏任务、微任务、事件(比如点击)、AJAX请求等 被事件符合条件触发时。会将任务添加到事件线程中
    • 事件触发线程会把事件添加到待处理队列的队尾,等待 JS 引擎处理
  4. 定时器线程
    • 定时器所在的线程(setIntervalsetTimeout等)
    • 因为JS引擎线程处于阻塞状态会影响计时的准确性,所以单独作为一个线程
    • W3C标准:setTimeout最小间隔为 4ms
  5. 异步http请求线程
    • 处理请求的线程,如果有请求回调函数,会将该回调放入事件队列中,由 JS引擎执行

PS:

  1. DOMContentLoaded 事件触发时:仅 DOM 加载完成(不包括css、图片资源、async脚本)
  2. 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图层

分为:普通图层和复合图层

复合图层就是传说中的硬件加速技术,独立于普通文档流中,改动后可以避免整个页面重绘,提升性能,但是也不能大量使用,会导致资源消耗过度。

如果变成复合图层?

  1. translate3dtranslateZ
  2. will-change(提前告诉浏览器需要优化) 和 opacity/过渡动画(如:transform) 在动画期间是复合图层
  3. videoiframecanvaswebgl

PS:

  1. absolute 虽然可以脱离普通文档流,但是无法脱离默认复合层。
  2. 复合图层的隐式合成:如果a是一个复合图层,而且b在a上面,那么b也会被隐式转为一个复合图层
豫ICP备19025630号