加餐——浏览器工作原理

网络协议

传输协议

TCP/IP

IP

网际协议,处于网络层中用来获取网络中设备唯一的地址。

UDP

User Datagram Protocol(用户数据报协议),无连接的传输层协议。 有了 IP 地址只能找到对应设备,如何将数据传到正确应用,这就需要 UDP 协议提供端口

UDP

  • UDP 可以校验和算法校验数据是否正确;
  • 对于错误的数据包,UDP 并不提供重发机制,只是丢弃当前的包;
  • 无连接的涵义指发送之后无法知道是否能达到目的地;
  • UDP 协议并不知道如何组装这些数据包,从而把这些数据包还原成完整的文件

TCP

  • 对于数据包丢失的情况,TCP 提供重传机制;
  • TCP 引入数据包排序机制,用来保证把乱序的数据包组合成一个完整的文件;
  • TCP 头除了包含了目标端口和本机端口号外,还提供了用于排序的序列号,以便接收端通过序号来重排数据包;
  • TCP 通过三次握手来保证重传机制;

TCP链接过程

建立一个 TCP 连接时,客户端和服务器总共要发送三个数据包以确认连接的建立。接收端在接收到数据包之后,需要发送确认数据包给发送端。“四次挥手”来保证双方都能断开连接。

TCP 为了保证数据传输的可靠性,牺牲了数据包的传输速度,因为“三次握手”和“数据包重传机制”等把传输过程中的数据包的数量提高了一倍。

HTTP

HTTP 是一种允许浏览器向服务器获取资源的协议,浏览器使用 HTTP 协议作为应用层协议,用来封装请求的文本信息

http请求流程

缓存

DNS 缓存和页面资源缓存是会被浏览器缓存的。当浏览器发现请求的资源已经在浏览器缓存中存有副本,它会拦截请求,返回该资源的副本,并直接结束请求。

如果缓存过期了,浏览器则会继续发起网络请求,并且在 HTTP 请求头中带上:

If-None-Match:"response ETag"

浏览器缓存

cookie 被机制用来保存用户登录状态。

服务端通过设置 👇

SetCookie: uid = cookieName;

浏览器下次请求时将带上Cookie:uid=cookieName字段。

参考流程 🔜 完整的http请求流程

导航流程

用户发出 URL 请求到页面开始解析的这个过程,就叫做导航。浏览器的导航过程涵盖了从用户发起请求到提交文档给渲染进程的中间所有阶段。 url到页面

1. 用户输入

  • 用户从浏览器进程里输入请求信息;
  • 判断输入是 url 还是搜索内容

2. 网络进程发起请求

  • DNS 解析
  • 查找缓存 ? 缓存 : 发起请求
  • 建立 TCP 链接
  • 构建请求行、请求头
  • 发送请求头
  • 服务端根据请求生成响应数据发送给网络进程
  • 网络进程解析响应头
    • 状态码
      • 200 继承处理请求
      • 301、302 重定向到 Location 字段中的地址,流程重新开始
      • 403,404
      • 500
    • 响应类型
      • Content-Type:application/octet-stream 交给下载管理器,导航流程结束
      • Content-Type:text/html 继续页面渲染

3. 准备渲染进程

  • 通常情况下,打开新的页面都会使用单独的渲染进程;
  • 如果从 A 页面打开 B 页面,且 A 和 B 都属于同一站点的话,那么 B 页面复用 A 页面的渲染进程;如果是其他情况,浏览器进程则会为 B 创建一个新的渲染进程。
  • 同一站点(same-site)指根域名(例如,vuejs.org)加上协议(例如,https:// 或者 http://)一致

4. 提交文档

  • 此时文档(响应体数据)还在网络进程中,还没有上车(渲染进程)
  • “提交文档”的消息是由浏览器进程发出的,渲染进程接收到“提交文档”的消息后,会和网络进程建立传输数据的“管道”。
  • 等文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程。
  • 浏览器进程在收到“确认提交”的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新 Web 页面。

5. 渲染

导航流程结束,浏览器停止标签图标上的加载动画,即将进入渲染阶段。

渲染流程

1. 翻译翻译:什么 tmd 是惊喜

  • HTML 构建 DOM 树、CSS 转换成 styleSheets、CSS 属性标准化

2. 样式计算:DOM 节点的样式

最终输出的内容是每个 DOM 节点的样式,并被保存在 ComputedStyle 的结构内

  • 继承
  • 层叠:如何合并来自多个源的属性值的算法

3. 布局:DOM 节点的位置

  • 创建布局树
    • 遍历 DOM 树中的所有可见节点,并把这些节点加到布局中
    • 而不可见的节点会被布局树忽略掉,如 head 标签下面的全部内容,设置 display:none 样式的元素也没有被包进布局树
  • 布局计算
    • 计算布局树节点的坐标位置

4. 分层:布局树上的每一个节点都会直接或者间接地从属于一个层

  • 拥有层叠上下文属性的元素会被提升为单独的一层
  • 需要剪裁(clip)的地方也会被创建为图层

5. 栅格化(raster)

  • 合成线程会将图层划分为图块(tile),实际生成位图的操作是由栅格化来执行的。
  • 将图块转换为位图,图块是栅格化执行的最小单位
  • 一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。

6. 合成显示

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

渲染流程

流程优化——重排

div.style.height = 100px

通过 JavaScript 或者 CSS 修改元素的几何位置属性,例如改变元素的宽度、高度等,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排。发生在上图 👆 的Style样式计算流程。

重排需要更新完整的渲染流水线,所以开销也是最大的

流程优化——重绘

div.style.background = red;

如果修改了元素的背景颜色,那么布局阶段将不会被执行,因为并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程就叫重绘。相较于重排操作,重绘省去了布局(Layout)和分层(Layer)阶段,所以执行效率会比重排操作要高一些。

流程优化——合成

transform: translate(xxx, xxx);

使用 CSS 的 transform 来实现动画效果,这可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作。这样的效率是最高的,因为是在非主线程上合成,并没有占用主线程的资源,另外也避开了布局、分层和绘制(Paint)阶段,所以相对于重绘和重排,合成[0]能大大提升绘制效率。

页面显示

DOM 解析

  • 遇到 JavaScript 脚本,DOM 解析器是如何处理的?
  • DOM 解析器是如何处理跨站点资源的?

DOM 是表述 HTML 的内部数据结构,它会将 Web 页面和 JavaScript 脚本连接起来,并过滤一些不安全的内容。

在渲染引擎内部,有一个叫 HTML 解析器(HTMLParser)的模块,它的职责就是负责将 HTML 字节流转换为 DOM 结构。

  • 字节流转换为 DOM
    • 第一个阶段,通过分词器将字节流转换为 Token。 分为 Tag Token 和文本 Token Tag Token 又分 StartTag 和 EndTag
    • 第二个和第三个阶段 将 Token 解析为 DOM 节点,并将 DOM 节点添加到 DOM 树中。
  • JavaScript 文件加载,遇到 JS 文件资源需要先下载这段 JavaScript 代码
    • Chrome 主要的优化是预解析操作,当渲染引擎收到字节流之后,会开启一个预解析线程,用来分析 HTML 文件中包含的 JavaScript、CSS 等相关文件,解析到相关文件之后,预解析线程会提前下载这些文件
    • JavaScript 脚本设置为异步加载, 渲染引擎在遇到 JavaScript 脚本时,不管该脚本是否操纵了 CSSOM[1],都会执行 CSS 文件下载,解析操作,再执行 JavaScript 脚本。 JavaScript 脚本是依赖样式表的,这又多了一个阻塞过程

JavaScript 会阻塞 DOM 生成,而样式文件又会阻塞 JavaScript 的执行渲染流水线

页面白屏

  • URL 请求到提交数据阶段,这时页面展示出来的还是之前页面的内容。
  • 提交数据之后渲染进程会创建一个空白页面,我们通常把这段时间称为解析白屏,执行上图渲染流水线过程
  • 等首次渲染完成之后,就开始进入完整页面的生成阶段了,然后页面会一点点被绘制出来

可以看出性能指标First Paint的瓶颈主要在第二阶段,对应策略就是尽量减少 JS、CSS 文件的大小,压缩、部分加载(媒体查询)、异步加载(defer、sync)

页面合成

分层

在 Chrome 的渲染流水线中,分层体现在生成布局树之后,渲染引擎会根据布局树的特点将其转换为层树(Layer Tree),层树中的每个节点都对应着一个图层,下一步的绘制阶段就依赖于层树中的节点。

绘制阶段其实并不是真正地绘出图片,而是将绘制指令组合成一个列表,比如一个图层要设置的背景为黑色,并且还要在中间画一个圆形,那么绘制过程会生成 |Paint BackGroundColor:Black | Paint Circle|这样的绘制指令列表,绘制过程就完成了。

合成

有了绘制列表之后,就需要进入光栅化阶段了,光栅化就是按照绘制列表中的指令生成图片。每一个图层都对应一张图片,合成线程有了这些图片之后,会将这些图片合成为“一张”图片,并最终将生成的图片发送到后缓冲区。

需要重点关注的是,合成操作是在合成线程上完成的,这也就意味着在执行合成操作时,是不会影响到主线程执行的。这就是为什么经常主线程卡住了,但是 CSS 动画依然能执行的原因。

分块

页面的内容往往比屏幕大得多,显示一个页面时,如果等待所有的图层都生成完毕,再进行合成的话,会让合成图片的时间变得更久。

因此,合成线程会将每个图层分割为大小固定的图块,然后优先绘制靠近视口的图块,这样就可以大大加速页面的显示速度。不过有时候, 即使只绘制那些优先级最高的图块,也要耗费不少的时间,因为涉及到一个很关键的因素——纹理上传,这是因为从计算机内存上传到 GPU 内存的操作会比较慢。

为了解决这个问题,Chrome 又采取了一个策略:在首次合成图块的时候使用一个低分辨率的图片。比如可以是正常分辨率的一半,分辨率减少一半,纹理就减少了四分之三。在首次显示页面内容的时候,将这个低分辨率的图片显示出来,然后合成器继续绘制正常比例的网页内容,当正常比例的网页内容绘制完成后,再替换掉当前显示的低分辨率内容。这种方式尽管会让用户在开始时看到的是低分辨率的内容,但是也比用户在开始时什么都看不到要好。

will-change

.box {
  will-change: transform, opacity;
}

提前告诉渲染引擎 box 元素将要做几何变换和透明度变换操作,这时候渲染引擎会将该元素单独实现一帧,等这些变换发生时,渲染引擎会通过合成线程直接去处理变换,这些变换并没有涉及到主线程,这样就大大提升了渲染的效率。这也是 CSS 动画比 JavaScript 动画高效的原因。

页面性能

当我们谈性能优化的时候,到底在谈什么?

我们所谈论的页面优化,其实就是要让页面更快地显示和响应。要讨论页面优化,就要分析一个页面生存周期的不同阶段。

  • 加载阶段,是指从发出请求到渲染出完整页面的过程,影响到这个阶段的主要因素有网络和 JavaScript 脚本。
  • 交互阶段,主要是从页面加载完成到用户交互的整合过程,影响到这个阶段的主要因素是 JavaScript 脚本。
  • 关闭阶段,主要是用户发出关闭指令后页面所做的一些清理操作

加载阶段

影响页面首次渲染的关键资源核心因素

  1. 个数,对于 CSS,如果不是在构建页面之前加载的,则可以添加媒体取消阻止显现的标志。当 JavaScript 标签加上了 sync 或者 defer

  2. 大小,压缩 CSS 和 JavaScript 资源,移除 HTML、CSS、JavaScript 文件中一些注释内容

  3. RTT(Round Trip Time),数据并不是一次传输到服务端的,而是需要拆分成一个个数据包来回多次进行传输的。RTT 就是这里的往返时延。它是网络中一个重要的性能指标,表示从发送端发送数据开始,到发送端收到来自接收端的确认,总共经历的时延。通常 1 个 HTTP 的数据包在 14KB 左右,所以 1 个 0.1M 的页面就需要拆分成 8 个包来传输了,也就是说需要 8 个 RTT。

    通过减少关键资源的个数和减少关键资源的大小搭配来实现。除此之外,还可以使用 CDN 来减少每次 RTT 时长。

交互阶段

大的原则就是让单个帧的生成速度变快。

  1. 减少 JavaScript 脚本执行时间
  2. 避免强制同步布局

在加载阶段,核心的优化原则是:优化关键资源的加载速度,减少关键资源的个数,降低关键资源的 RTT 次数。 在交互阶段,核心的优化原则是:尽量减少一帧的生成时间。可以通过减少单次 JavaScript 的执行时间、避免强制同步布局、避免布局抖动、尽量采用 CSS 的合成动画、避免频繁的垃圾回收等方式来减少一帧生成的时长。

VirtualDOM

技术

过 JavaScript 操纵 DOM 是会影响到整个渲染流水线, DOM 的不当操作还有可能引发强制同步布局和布局抖动的问题,这些操作都会大大降低渲染效率。 对于一些复杂的页面 DOM 结构复杂,所生成的页面结构也会很复杂,对于这些复杂的页面,执行一次重排或者重绘操作都是非常耗时的,这就给我们带来了真正的性能问题。 在虚拟 DOM 收集到足够的改变时,再把这些变化一次性应用到真实的 DOM 上。

双缓存

可以把虚拟 DOM 看成是 DOM 的一个 buffer,和图形显示一样,它会在完成一次完整的操作之后,再把结果应用到 DOM 上,这样就能减少一些不必要的更新,同时还能保证 DOM 的稳定输出。

MVC

MVC 的整体结构比较简单,由模型、视图和控制器组成,其核心思想就是将数据和视图分离,也就是说视图和模型之间是不允许直接通信的,它们之间的通信都是通过控制器来完成的。

学会从架构层面看框架:

  • 图中的控制器是用来监控 DOM 的变化,一旦 DOM 发生变化,控制器便会通知模型,让其更新数据;
  • 模型数据更新好之后,控制器会通知视图,告诉它模型的数据发生了变化;
  • 视图接收到更新消息之后,会根据模型所提供的数据来生成新的虚拟 DOM;
  • 新的虚拟 DOM 生成好之后,就需要与之前的虚拟 DOM 进行比较,找出变化的节点;
  • 比较出变化的节点之后,React 将变化的虚拟节点应用到 DOM 上,这样就会触发 DOM 节点的更新;
  • DOM 节点的变化又会触发后续一系列渲染流水线的变化,从而实现页面的更新

在实际工程项目中,你需要学会分析出这各个模块,并梳理出它们之间的通信关系,这样对于任何框架你都能轻松上手了。

补充

漫谈 Chrome 渲染open in new window

CSSOM

渲染引擎同样无法直接理解 CSS 文件内容,所以需要将其解析成渲染引擎能够理解的结构,这个结构就是 CSSOM,体现在 DOM 中就是 document.styleSheets。

  • 第一个是提供给 JavaScript 操作样式表的能力,
  • 第二个是为布局树的合成提供基础的样式信息。

小结

  • TCP 为什么三次握手?又为什么要四次挥手?
  • IP 地址和端口号如何获取?
  • 301、302 重定向还会读取本地缓存吗?
  • 为什么减少重绘、重排能优化 Web 性能?那又有那些具体的实践方法能减少重绘、重排呢?
  • 为什么要 will change,为什么要 css 动画?