This commit is contained in:
louzefeng
2024-07-09 18:38:56 +00:00
parent 8bafaef34d
commit bf99793fd0
6071 changed files with 1017944 additions and 0 deletions

View File

@@ -0,0 +1,215 @@
<audio id="audio" title="01 | Chrome架构仅仅打开了1个页面为什么有4个进程" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/91/fc/910d08530719470e929a34ab57d281fc.mp3"></audio>
无论你是想要设计高性能Web应用还是要优化现有的Web应用你都需要了解浏览器中的网络流程、页面渲染过程JavaScript执行流程以及Web安全理论而这些功能是分散在浏览器的各个功能组件中的比较多、比较散要怎样学习才能掌握呢通过浏览器的多进程架构的学习你就可以把这些分散的知识点串起来组成一张网从而让自己能站在更高的维度去理解Web应用。
因此,**学习浏览器的多进程架构是很有必要的**。需要说明的是,**在本专栏中我所有的分析都是基于Chrome浏览器的**。那么多浏览器为什么偏偏选择Chrome浏览器呢因为Chrome、微软的Edge以及国内的大部分主流浏览器都是基于Chromium二次开发而来而Chrome是Google的官方发行版特性和Chromium基本一样只存在一些产品层面差异再加上Chrome是目前世界上使用率最高的浏览器所以**Chrome最具代表性**。
在开始之前我们一起看下Chrome打开一个页面需要启动多少进程你可以点击Chrome浏览器右上角的“选项”菜单选择“更多工具”子菜单点击“任务管理器”这将打开Chrome的任务管理器的窗口如下图
<img src="https://static001.geekbang.org/resource/image/ce/9e/ce7f8cfe212bec0f53360422e3b03a9e.png" alt="">
和Windows任务管理器一样Chrome任务管理器也是用来展示运行中Chrome使用的进程信息的。从图中可以看到Chrome启动了4个进程你也许会好奇只是打开了1个页面为什么要启动这么多进程呢
在解答这个问题之前,我们需要了解一下进程的概念,不过由于好多人容易把进程和线程的概念混淆,从而影响后续其他概念的理解,所以这里我就将这两个概念以及它们之间的关系一并为你讲解下。
## 进程和线程
不过,在介绍进程和线程之前,我需要先讲解下什么是并行处理,因为如果你理解了并行处理的概念,那么再理解进程和线程之间的关系就会变得轻松许多。
### 什么是并行处理
计算机中的并行处理就是同一时刻处理多个任务,比如我们要计算下面这三个表达式的值,并显示出结果。
```
A = 1+2
B = 20/5
C = 7*8
```
在编写代码的时候,我们可以把这个过程拆分为四个任务:
- **任务1 **是计算A=1+2
- **任务2 **是计算B=20/5
- **任务3 **是计算C=7*8
- **任务4 **是显示最后计算的结果。
正常情况下程序可以使用**单线程**来处理,也就是分四步按照顺序分别执行这四个任务。
如果采用**多线程**,会怎么样呢?我们只需分“两步走”:第一步,使用三个线程同时执行前三个任务;第二步,再执行第四个显示任务。
通过对比分析,你会发现用单线程执行需要四步,而使用多线程只需要两步。因此,**使用并行处理能大大提升性能**。
### 线程 VS 进程
多线程可以并行处理任务,但是**线程是不能单独存在的,它是由进程来启动和管理的**。那什么又是进程呢?
**一个进程就是一个程序的运行实例**。详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫**进程**。
为了让你更好地理解上述计算过程,我画了下面这张对比图:
<img src="https://static001.geekbang.org/resource/image/33/da/3380f0a16c323deda5d3a300804b95da.png" alt="">
从图中可以看到,**线程是依附于进程的,而进程中使用多线程并行处理能提升运算效率**。
总结来说进程和线程之间的关系有以下4个特点。
**1. 进程中的任意一线程执行出错,都会导致整个进程的崩溃。**
我们可以模拟以下场景:
```
A = 1+2
B = 20/0
C = 7*8
```
我把上述三个表达式稍作修改在计算B的值的时候我把表达式的分母改成0当线程执行到B = 20/0时由于分母为0线程会执行出错这样就会导致整个进程的崩溃当然另外两个线程执行的结果也没有了。
**2. 线程之间共享进程中的数据。**
如下图所示,线程之间可以对进程的公共数据进行读写操作。
<img src="https://static001.geekbang.org/resource/image/d0/9e/d0efacd7f299ed99e776cb97da2a799e.png" alt="">
从上图可以看出线程1、线程2、线程3分别把执行的结果写入A、B、C中然后线程2继续从A、B、C中读取数据用来显示执行结果。
**3. 当一个进程关闭之后,操作系统会回收进程所占用的内存。**
当一个进程退出时,操作系统会回收该进程所申请的所有资源;即使其中任意线程因为操作不当导致内存泄漏,当进程退出时,这些内存也会被正确回收。
比如之前的IE浏览器支持很多插件而这些插件很容易导致内存泄漏这意味着只要浏览器开着内存占用就有可能会越来越多但是当关闭浏览器进程时这些内存就都会被系统回收掉。
**4. 进程之间的内容相互隔离。**
进程隔离是为保护操作系统中进程互不干扰的技术每一个进程只能访问自己占有的数据也就避免出现进程A写入数据到进程B的情况。正是因为进程之间的数据是严格隔离的所以一个进程如果崩溃了或者挂起了是不会影响到其他进程的。如果进程之间需要进行数据的通信这时候就需要使用用于进程间通信IPC的机制了。
## 单进程浏览器时代
在了解了进程和线程之后,我们再来一起看下单进程浏览器的架构。顾名思义,**单进程浏览器是指浏览器的所有功能模块都是运行在同一个进程里**这些模块包含了网络、插件、JavaScript运行环境、渲染引擎和页面等。其实早在2007年之前市面上浏览器都是单进程的。单进程浏览器的架构如下图所示
<img src="https://static001.geekbang.org/resource/image/6d/ca/6ddad2419b049b0eb2a8036f3dfff1ca.png" alt="">
如此多的功能模块运行在一个进程里,是导致单进程浏览器**不稳定**、**不流畅**和**不安全**的一个主要因素。下面我就来一一分析下出现这些问题的原因。
### 问题1不稳定
早期浏览器需要借助于**插件**来实现诸如Web视频、Web游戏等各种强大的功能但是插件是最容易出问题的模块并且还运行在浏览器进程之中所以一个插件的意外崩溃会引起整个浏览器的崩溃。
除了插件之外,**渲染引擎模块**也是不稳定的通常一些复杂的JavaScript代码就有可能引起渲染引擎模块的崩溃。和插件一样渲染引擎的崩溃也会导致整个浏览器的崩溃。
### 问题2不流畅
从上面的“单进程浏览器架构示意图”可以看出所有页面的渲染模块、JavaScript执行环境以及插件都是运行在同一个线程中的这就意味着同一时刻只能有一个模块可以执行。
比如,下面这个无限循环的脚本:
```
function freeze() {
while (1) {
console.log(&quot;freeze&quot;);
}
}
freeze();
```
如果让这个脚本运行在一个单进程浏览器的页面里,你感觉会发生什么?
因为这个脚本是无限循环的,所以当其执行时,它会独占整个线程,这样导致其他运行在该线程中的模块就没有机会被执行。因为浏览器中所有的页面都运行在该线程中,所以这些页面都没有机会去执行任务,这样就会导致整个浏览器失去响应,变卡顿。这块内容要继续往深的地方讲就到页面的事件循环系统了,具体相关内容我会在后面的模块中为你深入讲解。
除了上述**脚本**或者**插件**会让单进程浏览器变卡顿外,**页面的内存泄漏**也是单进程变慢的一个重要原因。通常浏览器的内核都是非常复杂的,运行一个复杂点的页面再关闭页面,会存在内存不能完全回收的情况,这样导致的问题是使用时间越长,内存占用越高,浏览器会变得越慢。
### 问题3不安全
这里依然可以从插件和页面脚本两个方面来解释该原因。
插件可以使用C/C++等代码编写,通过插件可以获取到操作系统的任意资源,当你在页面运行一个插件时也就意味着这个插件能完全操作你的电脑。如果是个恶意插件,那么它就可以释放病毒、窃取你的账号密码,引发安全性问题。
至于页面脚本,它可以通过浏览器的漏洞来获取系统权限,这些脚本获取系统权限之后也可以对你的电脑做一些恶意的事情,同样也会引发安全问题。
以上这些就是当时浏览器的特点,不稳定,不流畅,而且不安全。这是一段不堪回首的过去,也许你没有经历过,不过你可以想象一下这样的场景:当你正在用浏览器打开多个页面时,突然某个页面崩溃了或者失去响应,随之而来的是整个浏览器的崩溃或者无响应,然后你发现你给老板写的邮件页面也随之消失了,这时你的心情会不会和页面一样崩溃呢?
## 多进程浏览器时代
好在现代浏览器已经解决了这些问题,是如何解决的呢?这就得聊聊我们这个“多进程浏览器时代”了。
### 早期多进程架构
你可以先看看下面这张图这是2008年Chrome发布时的进程架构。
<img src="https://static001.geekbang.org/resource/image/cd/60/cdc9215e6c6377fc965b7fac8c3ec960.png" alt="">
从图中可以看出Chrome的页面是运行在单独的渲染进程中的同时页面里的插件也是运行在单独的插件进程之中而进程之间是通过IPC机制进行通信如图中虚线部分
**我们先看看如何解决不稳定的问题。**由于进程是相互隔离的,所以当一个页面或者插件崩溃时,影响到的仅仅是当前的页面进程或者插件进程,并不会影响到浏览器和其他页面,这就完美地解决了页面或者插件的崩溃会导致整个浏览器崩溃,也就是不稳定的问题。
**接下来再来看看不流畅的问题是如何解决的。**同样JavaScript也是运行在渲染进程中的所以即使JavaScript阻塞了渲染进程影响到的也只是当前的渲染页面而并不会影响浏览器和其他页面因为其他页面的脚本是运行在它们自己的渲染进程中的。所以当我们再在Chrome中运行上面那个死循环的脚本时没有响应的仅仅是当前的页面。
对于内存泄漏的解决方法那就更简单了,因为当关闭一个页面时,整个渲染进程也会被关闭,之后该进程所占用的内存都会被系统回收,这样就轻松解决了浏览器页面的内存泄漏问题。
**最后我们再来看看上面的两个安全问题是怎么解决的**。采用多进程架构的额外好处是可以使用**安全沙箱**你可以把沙箱看成是操作系统给进程上了一把锁沙箱里面的程序可以运行但是不能在你的硬盘上写入任何数据也不能在敏感位置读取任何数据例如你的文档和桌面。Chrome把插件进程和渲染进程锁在沙箱里面这样即使在渲染进程或者插件进程里面执行了恶意程序恶意程序也无法突破沙箱去获取系统权限。
好了分析完早期的Chrome浏览器后相信你已经了解了浏览器采用多进程架构的必要性。
### 目前多进程架构
不过Chrome的发展是滚滚向前的相较之前目前的架构又有了很多新的变化。我们先看看最新的Chrome进程架构你可以参考下图
<img src="https://static001.geekbang.org/resource/image/b6/fc/b61cab529fa31301bde290813b4587fc.png" alt="">
从图中可以看出最新的Chrome浏览器包括1个浏览器Browser主进程、1个 GPU 进程、1个网络NetWork进程、多个渲染进程和多个插件进程。
下面我们来逐个分析下这几个进程的功能。
- **浏览器进程**。主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
- **渲染进程**。核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页排版引擎Blink和JavaScript引擎V8都是运行在该进程中默认情况下Chrome会为每个Tab标签创建一个渲染进程。出于安全考虑渲染进程都是运行在沙箱模式下。
- **GPU进程**。其实Chrome刚开始发布的时候是没有GPU进程的。而GPU的使用初衷是为了实现3D CSS的效果只是随后网页、Chrome的UI界面都选择采用GPU来绘制这使得GPU成为浏览器普遍的需求。最后Chrome在其多进程架构上也引入了GPU进程。
- **网络进程**。主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
- **插件进程**。主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。
讲到这里现在你应该就可以回答文章开头提到的问题了仅仅打开了1个页面为什么有4个进程因为打开1个页面至少需要1个网络进程、1个浏览器进程、1个GPU进程以及1个渲染进程共4个如果打开的页面有运行插件的话还需要再加上1个插件进程。
不过凡事都有两面性,虽然多进程模型提升了浏览器的稳定性、流畅性和安全性,但同样不可避免地带来了一些问题:
- **更高的资源占用**。因为每个进程都会包含公共基础结构的副本如JavaScript运行环境这就意味着浏览器会消耗更多的内存资源。
- **更复杂的体系架构**。浏览器各模块之间耦合性高、扩展性差等问题,会导致现在的架构已经很难适应新的需求了。
对于上面这两个问题Chrome团队一直在寻求一种弹性方案既可以解决资源占用高的问题也可以解决复杂的体系架构的问题。
### 未来面向服务的架构
为了解决这些问题在2016年Chrome官方团队使用“**面向服务的架构**”Services Oriented Architecture简称**SOA**的思想设计了新的Chrome架构。也就是说 Chrome 整体架构会朝向现代操作系统所采用的“面向服务的架构” 方向发展原来的各种模块会被重构成独立的服务Service每个服务Service都可以在独立的进程中运行访问服务Service必须使用定义好的接口通过IPC来通信从而**构建一个更内聚、松耦合、易于维护和扩展的系统**,更好实现 Chrome 简单、稳定、高速、安全的目标。如果你对面向服务的架构感兴趣,你可以去网上搜索下资料,这里就不过多介绍了。
Chrome最终要把UI、数据库、文件、设备、网络等模块重构为基础服务类似操作系统底层服务下面是Chrome“面向服务的架构”的进程模型图
<img src="https://static001.geekbang.org/resource/image/32/2a/329658fe821252db47b0964037a1de2a.png" alt="">
目前Chrome正处在老的架构向服务化架构过渡阶段这将是一个漫长的迭代过程。
Chrome正在逐步构建Chrome基础服务Chrome Foundation Service如果你认为Chrome是“便携式操作系统”那么Chrome基础服务便可以被视为该操作系统的“基础”系统服务层。
同时Chrome还提供灵活的弹性架构在强大性能设备上会以多进程的方式运行基础服务但是如果在资源受限的设备上如下图Chrome会将很多服务整合到一个进程中从而节省内存占用。
<img src="https://static001.geekbang.org/resource/image/a9/76/a9ba86d7b03263fa3997d3733d958176.png" alt="">
## 总结
好了,今天就到这里,下面我来简要梳理并总结今天的内容。
本文我主要是从Chrome进程架构的视角分析了浏览器的进化史。
最初的浏览器都是单进程的它们不稳定、不流畅且不安全之后出现了Chrome创造性地引入了多进程架构并解决了这些遗留问题。随后Chrome试图应用到更多业务场景如移动设备、VR、视频等为了支持这些场景Chrome的架构体系变得越来越复杂这种架构的复杂性倒逼Chrome开发团队必须进行架构的重构最终Chrome团队选择了面向服务架构SOA形式这也是Chrome团队现阶段的一个主要任务。
鉴于目前架构的复杂性要完整过渡到面向服务架构估计还需要好几年时间才能完成。不过Chrome开发是一个渐进的过程新的特性会一点点加入进来这也意味着我们随时能看到Chrome新的变化。
总体说来,**Chrome是以一个非常快速的速度在进化越来越多的业务和应用都逐渐转至浏览器来开发身为开发人员我们不能坐视不管而应该紧跟其步伐收获这波技术红利**。
## 思考时间
最后,给你留个思考题:回顾浏览器的进化路线,你认为推动浏览器发展的主要动力是什么?
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。

View File

@@ -0,0 +1,113 @@
<audio id="audio" title="02 | TCP协议如何保证页面文件能被完整送达浏览器" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/98/ab/989fd02dfaccd7c08555afd3ee2138ab.mp3"></audio>
在衡量Web页面性能的时候有一个重要的指标叫“**FPFirst Paint**”,是**指从页面加载到首次开始绘制的时长**。这个指标直接影响了用户的跳出率更快的页面响应意味着更多的PV、更高的参与度以及更高的转化率。那什么影响FP指标呢其中一个重要的因素是**网络加载速度**。
要想优化Web页面的加载速度你需要对网络有充分的了解。而理解网络的关键是要对网络协议有深刻的认识不管你是使用HTTP还是使用WebSocket它们都是基于TCP/IP的如果你对这些原理有足够了解也就清楚如何去优化Web性能或者能更轻松地定位Web问题了。此外TCP/IP的设计思想还有助于拓宽你的知识边界从而在整体上提升你对项目的理解和解决问题的能力。
因此,在这篇文章中,我会给你**重点介绍在Web世界中的TCP/IP是如何工作的**。当然协议并不是本专栏的重点这篇文章我会从我的角度结合HTTP来分析网络请求的核心路径如果你想对网络协议有更深入的理解那我推荐你学习刘超老师的《趣谈网络协议》专栏以及陶辉老师的《Web协议详解与抓包实战》视频课程。
好,接下来我们回到正题,开始今天的内容。在网络中,一个文件通常会被拆分为很多数据包来进行传输,而数据包在传输过程中又有很大概率丢失或者出错。**那么如何保证页面文件能被完整地送达浏览器呢?**
这篇文章将站在数据包的视角,给出问题答案。
## 一个数据包的“旅程”
下面我将分别从“数据包如何送达主机”“主机如何将数据包转交给应用”和“数据是如何被完整地送达应用程序”这三个角度来为你讲述数据的传输过程。
**互联网,实际上是一套理念和协议组成的体系架构**。其中,协议是一套众所周知的规则和标准,如果各方都同意使用,那么它们之间的通信将变得毫无障碍。
互联网中的数据是通过数据包来传输的。如果发送的数据很大,那么该数据就会被拆分为很多小数据包来传输。比如你现在听的音频数据,是拆分成一个个小的数据包来传输的,并不是一个大的文件一次传输过来的。
### 1. IP把数据包送达目的主机
数据包要在互联网上进行传输,就要符合**网际协议**Internet Protocol简称**IP**)标准。互联网上不同的在线设备都有唯一的地址,地址只是一个数字,这和大部分家庭收件地址类似,你只需要知道一个家庭的具体地址,就可以往这个地址发送包裹,这样物流系统就能把物品送到目的地。
**计算机的地址就称为IP地址访问任何网站实际上只是你的计算机向另外一台计算机请求信息。**
如果要想把一个数据包从主机A发送给主机B那么在传输之前数据包上会被附加上主机B的IP地址信息这样在传输过程中才能正确寻址。额外地数据包上还会附加上主机A本身的IP地址有了这些信息主机B才可以回复信息给主机A。这些附加的信息会被装进一个叫IP头的数据结构里。IP头是IP数据包开头的信息包含IP版本、源IP地址、目标IP地址、生存时间等信息。如果你要详细了解IP头信息可以参考[该链接](https://zh.wikipedia.org/wiki/%E4%BC%A0%E8%BE%93%E6%8E%A7%E5%88%B6%E5%8D%8F%E8%AE%AE)。
为了方便理解,我先把网络简单分为三层结构,如下图:
<img src="https://static001.geekbang.org/resource/image/00/4d/00d9bcad0bda1fdb43ead428e89ae74d.png" alt="">
下面我们一起来看下一个数据包从主机A到主机B的旅程
- 上层将含有“极客时间”的数据包交给网络层;
- 网络层再将IP头附加到数据包上组成新的 **IP数据包**,并交给底层;
- 底层通过物理网络将数据包传输给主机B
- 数据包被传输到主机B的网络层在这里主机B拆开数据包的IP头信息并将拆开来的数据部分交给上层
- 最终含有“极客时间”信息的数据包就到达了主机B的上层了。
### 2. UDP把数据包送达应用程序
IP是非常底层的协议只负责把数据包传送到对方电脑但是对方电脑并不知道把数据包交给哪个程序是交给浏览器还是交给王者荣耀因此需要基于IP之上开发能和应用打交道的协议最常见的是“**用户数据包协议**User Datagram Protocol简称**UDP**。
UDP中一个最重要的信息是**端口号**端口号其实就是一个数字每个想访问网络的程序都需要绑定一个端口号。通过端口号UDP就能把指定的数据包发送给指定的程序了所以**IP通过IP地址信息把数据包发送给指定的电脑而UDP通过端口号把数据包分发给正确的程序**。和IP头一样端口号会被装进UDP头里面UDP头再和原始数据包合并组成新的UDP数据包。UDP头中除了目的端口还有源端口号等信息。
为了支持UDP协议我把前面的三层结构扩充为四层结构在网络层和上层之间增加了传输层如下图所示
<img src="https://static001.geekbang.org/resource/image/3e/ea/3edb673a43f23d84253c52124ce447ea.png" alt="">
下面我们一起来看下一个数据包从主机A旅行到主机B的路线
- 上层将含有“极客时间”的数据包交给传输层;
- 传输层会在数据包前面附加上**UDP头**组成新的UDP数据包再将新的UDP数据包交给网络层
- 网络层再将IP头附加到数据包上组成新的IP数据包并交给底层
- 数据包被传输到主机B的网络层在这里主机B拆开IP头信息并将拆开来的数据部分交给传输层
- 在传输层数据包中的UDP头会被拆开**并根据UDP中所提供的端口号把数据部分交给上层的应用程序**
- 最终含有“极客时间”信息的数据包就旅行到了主机B上层应用程序这里。
在使用UDP发送数据时有各种因素会导致数据包出错虽然UDP可以校验数据是否正确但是对于错误的数据包UDP并不提供重发机制只是丢弃当前的包而且UDP在发送之后也无法知道是否能达到目的地。
虽说**UDP不能保证数据可靠性但是传输速度却非常快**所以UDP会应用在一些关注速度、但不那么严格要求数据完整性的领域如在线视频、互动游戏等。
### 3. TCP把数据完整地送达应用程序
对于浏览器请求或者邮件这类要求数据传输可靠性reliability的应用如果使用UDP来传输会存在**两个问题**
- 数据包在传输过程中容易丢失;
- 大文件会被拆分成很多小的数据包来传输这些小的数据包会经过不同的路由并在不同的时间到达接收端而UDP协议并不知道如何组装这些数据包从而把这些数据包还原成完整的文件。
基于这两个问题我们引入TCP了。**TCPTransmission Control Protocol传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议**。相对于UDPTCP有下面两个特点:
- 对于数据包丢失的情况TCP提供重传机制
- TCP引入了数据包排序机制用来保证把乱序的数据包组合成一个完整的文件。
和UDP头一样TCP头除了包含了目标端口和本机端口号外还提供了用于排序的序列号以便接收端通过序号来重排数据包。
下面看看TCP下的单个数据包的传输流程
<img src="https://static001.geekbang.org/resource/image/94/32/943ac29f7d5b45a8861b0cde5da99032.png" alt="">
通过上图你应该可以了解一个数据包是如何通过TCP来传输的。TCP单个数据包的传输流程和UDP流程差不多不同的地方在于通过TCP头的信息保证了一块大的数据传输的完整性。
下面我们再看下**完整的TCP连接过程**通过这个过程你可以明白TCP是如何保证重传机制和数据包的排序功能的。
从下图可以看出一个完整的TCP连接的生命周期包括了“**建立连接**”“**传输数据**”和“**断开连接**”三个阶段。
<img src="https://static001.geekbang.org/resource/image/44/44/440ee50de56edc27c6b3c992b3a25844.png" alt="">
- **首先,建立连接阶段**。这个阶段是通过“三次握手”来建立客户端和服务器之间的连接。TCP 提供面向连接的通信传输。**面向连接**是指在数据通信开始之前先做好两端之间的准备工作。所谓**三次握手**是指在建立一个TCP连接时客户端和服务器总共要发送三个数据包以确认连接的建立。
- **其次,传输数据阶段**。在该阶段,**接收端需要对每个数据包进行确认操作**也就是接收端在接收到数据包之后需要发送确认数据包给发送端。所以当发送端发送了一个数据包之后在规定时间内没有接收到接收端反馈的确认消息则判断为数据包丢失并触发发送端的重发机制。同样一个大的文件在传输过程中会被拆分成很多小的数据包这些数据包到达接收端后接收端会按照TCP头中的序号为其排序从而保证组成完整的数据。
- **最后,断开连接阶段**。数据传输完毕之后,就要终止连接了,涉及到最后一个阶段“四次挥手”来保证双方都能断开连接。
到这里你应该就明白了TCP为了保证数据传输的可靠性牺牲了数据包的传输速度因为“三次握手”和“数据包校验机制”等把传输过程中的数据包的数量提高了一倍。
## 总结
好了,这一节就到这里,下面我来做一个简单的总结。
- 互联网中的数据是通过数据包来传输的,数据包在传输过程中容易丢失或出错。
- IP负责把数据包送达目的主机。
- UDP负责把数据包送达具体应用。
- 而TCP保证了数据完整地传输它的连接可分为三个阶段建立连接、传输数据和断开连接。
其实了解TCP协议是为了全方位了解HTTP包括其实际功能和局限性之后才会更加深刻地理解为什么要推出HTTP/2以及为什么要推出QUIC协议也就是未来的HTTP/3。这是一个由浅入深、循序渐进的过程我希望你能稳扎稳打学好这每一步、每一个协议后面“水到自然渠成”。
## 思考时间
今天这篇文章我没有讲HTTP协议但是相信你应该听说过HTTP协议是基于TCP协议的那么今天我留给你的问题是你怎么理解HTTP和TCP的关系
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。

View File

@@ -0,0 +1,238 @@
<audio id="audio" title="03 | HTTP请求流程为什么很多站点第二次打开速度会很快" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/12/c1/12a2874ebd1664a4d9a790fdbcf1d6c1.mp3"></audio>
在[上一篇文章](https://time.geekbang.org/column/article/113550)中我介绍了TCP协议是如何保证数据完整传输的相信你还记得一个TCP连接过程包括了建立连接、传输数据和断开连接三个阶段。
而HTTP协议正是建立在TCP连接基础之上的。**HTTP是一种允许浏览器向服务器获取资源的协议是Web的基础**通常由浏览器发起请求用来获取不同类型的文件例如HTML文件、CSS文件、JavaScript文件、图片、视频等。此外**HTTP也是浏览器使用最广的协议**所以要想学好浏览器就要先深入了解HTTP。
不知道你是否有过下面这些疑问:
1. 为什么通常在第一次访问一个站点时,打开速度很慢,当再次访问这个站点时,速度就很快了?
1. 当登录过一个网站之后,下次再访问该站点,就已经处于登录状态了,这是怎么做到的呢?
这一切的秘密都隐藏在HTTP的请求过程中。所以在今天这篇文章中我将通过分析一个HTTP请求过程中每一步的状态来带你了解完整的HTTP请求过程希望你看完这篇文章后能够对HTTP协议有个全新的认识。
## 浏览器端发起HTTP请求流程
如果你在浏览器地址栏里键入极客时间网站的地址:[http://time.geekbang.org/index.html](http://time.geekbang.org/index.html%EF%BC%8C) 那么接下来,浏览器会完成哪些动作呢?下面我们就一步一步详细“追踪”下。
### 1. 构建请求
首先,浏览器构建**请求行**信息(如下所示),构建好后,浏览器准备发起网络请求。
```
GET /index.html HTTP1.1
```
### 2. 查找缓存
在真正发起网络请求之前,浏览器会先在浏览器缓存中查询是否有要请求的文件。其中,**浏览器缓存是一种在本地保存资源副本,以供下次请求时直接使用的技术**。
当浏览器发现请求的资源已经在浏览器缓存中存有副本,它会拦截请求,返回该资源的副本,并直接结束请求,而不会再去源服务器重新下载。这样做的好处有:
- 缓解服务器端压力,提升性能(获取资源的耗时更短了);
- 对于网站来说,缓存是实现快速资源加载的重要组成部分。
当然,如果缓存查找失败,就会进入网络请求过程了。
### 3. 准备IP地址和端口
不过先不急在了解网络请求之前我们需要先看看HTTP和TCP的关系。因为浏览器使用**HTTP协议作为应用层协议**,用来封装请求的文本信息;并使用**TCP/IP作传输层协议**将它发到网络上所以在HTTP工作开始之前浏览器需要通过TCP与服务器建立连接。也就是说**HTTP的内容是通过TCP的传输数据阶段来实现的**,你可以结合下图更好地理解这二者的关系。
<img src="https://static001.geekbang.org/resource/image/12/80/1277f342174b23f9442d3b27016d7980.png" alt="">
那接下来你可以思考这么“一连串”问题:
- HTTP网络请求的第一步是做什么呢结合上图看是和服务器建立TCP连接。
- 那建立连接的信息都有了吗?[上一篇文章](https://time.geekbang.org/column/article/113550)中我们讲到建立TCP连接的第一步就是需要准备IP地址和端口号。
- 那怎么获取IP地址和端口号呢这得看看我们现在有什么我们有一个URL地址那么是否可以利用URL地址来获取IP和端口信息呢
在[上一篇文章](https://time.geekbang.org/column/article/113550)中我们介绍过数据包都是通过IP地址传输给接收方的。由于IP地址是数字标识比如极客时间网站的IP是39.106.233.176, 难以记忆但使用极客时间的域名time.geekbang.org就好记多了所以基于这个需求又出现了一个服务负责把域名和IP地址做一一映射关系。这套域名映射为IP的系统就叫做“**域名系统**”,简称**DNS**Domain Name System
所以,这样一路推导下来,你会发现在**第一步浏览器会请求DNS返回域名对应的IP**。当然浏览器还提供了**DNS数据缓存服务**,如果某个域名已经解析过了,那么浏览器会缓存解析的结果,以供下次查询时直接使用,这样也会减少一次网络请求。
拿到IP之后接下来就需要获取端口号了。通常情况下如果URL没有特别指明端口号那么HTTP协议默认是80端口。
### 4. 等待TCP队列
现在已经把端口和IP地址都准备好了那么下一步是不是可以建立TCP连接了呢
答案依然是“不行”。Chrome有个机制同一个域名同时最多只能建立6个TCP连接如果在同一个域名下同时有10个请求发生那么其中4个请求会进入排队等待状态直至进行中的请求完成。
当然如果当前请求数量少于6会直接进入下一步建立TCP连接。
### 5. 建立TCP连接
排队等待结束之后终于可以快乐地和服务器握手了在HTTP工作开始之前浏览器通过TCP与服务器建立连接。而TCP的工作方式我在[上一篇文章](https://time.geekbang.org/column/article/113550)中已经做过详细介绍了,如果有必要,你可以自行回顾下,这里我就不再重复讲述了。
### 6. 发送HTTP请求
一旦建立了TCP连接浏览器就可以和服务器进行通信了。而HTTP中的数据正是在这个通信过程中传输的。
你可以结合下图来理解,浏览器是如何发送请求信息给服务器的。
<img src="https://static001.geekbang.org/resource/image/b8/d7/b8993c73f7b60feb9b8bd147545c47d7.png" alt="">
首先浏览器会向服务器发送**请求行**,它包括了**请求方法、请求URIUniform Resource Identifier和HTTP版本协议**。
发送请求行,就是告诉服务器浏览器需要什么资源,最常用的请求方法是**Get**。比如直接在浏览器地址栏键入极客时间的域名time.geekbang.org这就是告诉服务器要Get它的首页资源。
另外一个常用的请求方法是**POST**它用于发送一些数据给服务器比如登录一个网站就需要通过POST方法把用户信息发送给服务器。如果使用POST方法那么浏览器还要准备数据给服务器这里准备的数据是通过**请求体**来发送。
在浏览器发送请求行命令之后,还要以**请求头**形式发送其他一些信息把浏览器的一些基础信息告诉服务器。比如包含了浏览器所使用的操作系统、浏览器内核等信息以及当前请求的域名信息、浏览器端的Cookie信息等等。
## 服务器端处理HTTP请求流程
历经千辛万苦HTTP的请求信息终于被送达了服务器。接下来服务器会根据浏览器的请求信息来准备相应的内容。
### 1. 返回请求
一旦服务器处理结束便可以返回数据给浏览器了。你可以通过工具软件curl来查看返回请求数据具体使用方法是在命令行中输入以下命令
```
curl -i https://time.geekbang.org/
```
注意这里加上了`-i`是为了返回响应行、响应头和响应体的数据,返回的结果如下图所示,你可以结合这些数据来理解服务器是如何响应浏览器的。
<img src="https://static001.geekbang.org/resource/image/3e/76/3e30476a4bbda49fd7cd4fd0ea09f076.png" alt="">
首先服务器会返回**响应行**,包括协议版本和状态码。
但并不是所有的请求都可以被服务器处理的,那么一些无法处理或者处理出错的信息,怎么办呢?服务器会通过请求行的**状态码**来告诉浏览器它的处理结果,比如:
- 最常用的状态码是200表示处理成功
- 如果没有找到页面,则会返回**404**。
状态码类型很多,这里我就不过多介绍了,网上有很多资料,你可以自行查询和学习。
随后,正如浏览器会随同请求发送请求头一样,服务器也会随同响应向浏览器发送**响应头**。响应头包含了服务器自身的一些信息比如服务器生成返回数据的时间、返回的数据类型JSON、HTML、流媒体等类型以及服务器要在客户端保存的Cookie等信息。
发送完响应头后,服务器就可以继续发送**响应体**的数据通常响应体就包含了HTML的实际内容。
以上这些就是服务器响应浏览器的具体过程。
### 2. 断开连接
通常情况下,一旦服务器向客户端返回了请求数据,它就要关闭 TCP 连接。不过如果浏览器或者服务器在其头信息中加入了:
```
Connection:Keep-Alive
```
那么TCP连接在发送后将仍然保持打开状态这样浏览器就可以继续通过同一个TCP连接发送请求。**保持TCP连接可以省去下次请求时需要建立连接的时间提升资源加载速度**。比如一个Web页面中内嵌的图片就都来自同一个Web站点如果初始化了一个持久连接你就可以复用该连接以请求其他资源而不需要重新再建立新的TCP连接。
### 3. 重定向
到这里似乎请求流程快结束了不过还有一种情况你需要了解下比如当你在浏览器中打开geekbang.org后你会发现最终打开的页面地址是 [https://www.geekbang.org](https://www.geekbang.org)。
这两个URL之所以不一样是因为涉及到了一个**重定向操作**。跟前面一样你依然可以使用curl来查看下请求geekbang.org 会返回什么内容?
在控制台输入如下命令:
```
curl -I geekbang.org
```
注意这里输入的参数是`-I`,和`-i`不一样,`-I`表示只需要获取响应头和响应行数据,而不需要获取响应体的数据,最终返回的数据如下图所示:
<img src="https://static001.geekbang.org/resource/image/28/43/28d5796c6ab7faa619ed8f1bd17b0843.jpg" alt="">
从图中你可以看到响应行返回的状态码是301状态301就是告诉浏览器我需要重定向到另外一个网址而需要重定向的网址正是包含在响应头的Location字段中接下来浏览器获取Location字段中的地址并使用该地址重新导航这就是一个完整重定向的执行流程。这也就解释了为什么输入的是 geekbang.org最终打开的却是 [https://www.geekbang.org](https://www.geekbang.org) 了。
不过也不要认为这种跳转是必然的。如果你打开 [https://12306.cn](https://12306.cn)你会发现这个站点是打不开的。这是因为12306的服务器并没有处理跳转所以必须要手动输入完整的 [https://www.12306.cn](https://www.12306.cn) 才能打开页面。
## 问题解答
说了这么多相信你现在已经了解了HTTP的请求流程那现在我们再回过头来看看文章开头提出的问题。
### 1. 为什么很多站点第二次打开速度会很快?
如果第二次页面打开很快,主要原因是第一次加载页面过程中,缓存了一些耗时的数据。
那么,哪些数据会被缓存呢?从上面介绍的核心请求路径可以发现,**DNS缓存**和**页面资源缓存**这两块数据是会被浏览器缓存的。其中DNS缓存比较简单它主要就是在浏览器本地把对应的IP和域名关联起来这里就不做过多分析了。
我们重点看下浏览器资源缓存,下面是缓存处理的过程:
<img src="https://static001.geekbang.org/resource/image/5f/08/5fc2f88a04ee0fc41a808f3481287408.png" alt="">
首先,我们看下服务器是通过什么方式让浏览器缓存数据的?
从上图的第一次请求可以看出,当服务器返回**HTTP响应头**给浏览器时,浏览器是**通过响应头中的Cache-Control字段来设置是否缓存该资源**。通常我们还需要为这个资源设置一个缓存过期时长而这个时长是通过Cache-Control中的Max-age参数来设置的比如上图设置的缓存过期时间是2000秒。
```
Cache-Control:Max-age=2000
```
这也就意味着,在该缓存资源还未过期的情况下, 如果再次请求该资源,会直接返回缓存中的资源给浏览器。
但如果缓存过期了,浏览器则会继续发起网络请求,并且在**HTTP请求头**中带上:
```
If-None-Match:&quot;4f80f-13c-3a1xb12a&quot;
```
服务器收到请求头后会根据If-None-Match的值来判断请求的资源是否有更新。
- 如果没有更新就返回304状态码相当于服务器告诉浏览器“这个缓存可以继续使用这次就不重复发送数据给你了。”
- 如果资源有更新,服务器就直接返回最新资源给浏览器。
关于缓存的细节内容特别多,具体细节你可以参考这篇 [HTTP缓存](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Caching_FAQ),在这里我就不赘述了。
简要来说很多网站第二次访问能够秒开是因为这些网站把很多资源都缓存在了本地浏览器缓存直接使用本地副本来回应请求而不会产生真实的网络请求从而节省了时间。同时DNS数据也被浏览器缓存了这又省去了DNS查询环节。
### 2. 登录状态是如何保持的?
通过上面的介绍,你已经了解了缓存是如何工作的。下面我们再一起看下登录状态是如何保持的。
- 用户打开登录页面在登录框里填入用户名和密码点击确定按钮。点击按钮会触发页面脚本生成用户登录信息然后调用POST方法提交用户登录信息给服务器。
- 服务器接收到浏览器提交的信息之后查询后台验证用户登录信息是否正确如果正确的话会生成一段表示用户身份的字符串并把该字符串写到响应头的Set-Cookie字段里如下所示然后把响应头发送给浏览器。
```
Set-Cookie: UID=3431uad;
```
- 浏览器在接收到服务器的响应头后开始解析响应头如果遇到响应头里含有Set-Cookie字段的情况浏览器就会把这个字段信息保存到本地。比如把`UID=3431uad`保持到本地。
- 当用户再次访问时浏览器会发起HTTP请求但在发起请求之前浏览器会读取之前保存的Cookie数据并把数据写进请求头里的Cookie字段里如下所示然后浏览器再将请求头发送给服务器。
```
Cookie: UID=3431uad;
```
- 服务器在收到HTTP请求头数据之后就会查找请求头里面的“Cookie”字段信息当查找到包含`UID=3431uad`的信息时,服务器查询后台,并判断该用户是已登录状态,然后生成含有该用户信息的页面数据,并把生成的数据发送给浏览器。
- 浏览器在接收到该含有当前用户的页面数据后,就可以正确展示用户登录的状态信息了。
好了通过这个流程你可以知道浏览器页面状态是通过使用Cookie来实现的。Cookie流程可以参考下图
<img src="https://static001.geekbang.org/resource/image/d9/b3/d9d6cefe8d3d6d84a37a626687c6ecb3.png" alt="">
简单地说,如果服务器端发送的响应头内有 Set-Cookie 的字段,那么浏览器就会将该字段的内容保持到本地。当下次客户端再往该服务器发送请求时,客户端会自动在请求头中加入 Cookie 值后再发送出去。服务器端发现客户端发送过来的Cookie后会去检查究竟是从哪一个客户端发来的连接请求然后对比服务器上的记录最后得到该用户的状态信息。
## 总结
本篇文章的内容比较多、比较碎,但是非常重要,所以我先来总结下今天的主要内容。
为了便于你理解我画了下面这张详细的“HTTP请求示意图”用来展现浏览器中的HTTP请求所经历的各个阶段。
<img src="https://static001.geekbang.org/resource/image/1b/6c/1b49976aca2c700883d48d927f48986c.png" alt="">
从图中可以看到浏览器中的HTTP请求从发起到结束一共经历了如下八个阶段构建请求、查找缓存、准备IP和端口、等待TCP队列、建立TCP连接、发起HTTP请求、服务器处理请求、服务器返回请求和断开连接。
然后我还通过HTTP请求路径解答了两个经常会碰到的问题一个涉及到了Cache流程另外一个涉及到如何使用Cookie来进行状态管理。
通过今天系统的讲解想必你已经了解了一个HTTP完整的工作流程相信这些知识点之于你以后的学习或工作会很有帮助。
另外,你应该也看出来了本篇文章是有很多分析问题的思路在里面的。所以在学习过程中,你也要学会提问,通过最终要做什么和现在有什么,去一步步分析并提出一些问题,让疑问带领着你去学习,抓住几个本质的问题就可以学透相关知识点,让你能站在更高维度去查看整体框架。希望它能成为你的一个学习技巧吧!
## 思考时间
最后还是留给你个思考题结合今天所讲HTTP请求的各个阶段如果一个页面的网络加载时间过久你是如何分析卡在哪个阶段的
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。

View File

@@ -0,0 +1,203 @@
<audio id="audio" title="04 | 导航流程从输入URL到页面展示这中间发生了什么" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3d/8d/3d4d948d10d7b1dbe4e9add4e29a538d.mp3"></audio>
“在浏览器里从输入URL到页面展示这中间发生了什么 ”这是一道经典的面试题能比较全面地考察应聘者知识的掌握程度其中涉及到了网络、操作系统、Web等一系列的知识。所以我在面试应聘者时也必问这道题但遗憾的是大多数人只能回答其中部分零散的知识点并不能将这些知识点串联成线无法系统而又全面地回答这个问题。
那么今天我们就一起来探索下这个流程下图是我梳理出的“从输入URL到页面展示完整流程示意图”
<img src="https://static001.geekbang.org/resource/image/92/5d/92d73c75308e50d5c06ad44612bcb45d.png" alt="">
从图中可以看出,**整个过程需要各个进程之间的配合**,所以在开始正式流程之前,我们还是先来快速回顾下浏览器进程、渲染进程和网络进程的主要职责。
- 浏览器进程主要负责用户交互、子进程管理和文件储存等功能。
- 网络进程是面向渲染进程和浏览器进程等提供网络下载功能。
- 渲染进程的主要职责是把从网络下载的HTML、JavaScript、CSS、图片等资源解析为可以显示和交互的页面。因为渲染进程所有的内容都是通过网络获取的会存在一些恶意代码利用浏览器漏洞对系统进行攻击所以运行在渲染进程里面的代码是不被信任的。这也是为什么Chrome会让渲染进程运行在安全沙箱里就是为了保证系统的安全。
当然,你也可以先回顾下前面的[《01 | Chrome架构仅仅打开了1个页面为什么有4个进程](https://time.geekbang.org/column/article/113513)这篇文章,来全面了解浏览器多进程架构。
回顾了浏览器的进程架构后,我们再结合上图来看下这个完整的流程,可以看出,整个流程包含了许多步骤,我把其中几个核心的节点用蓝色背景标记出来了。这个过程可以大致描述为如下。
- 首先浏览器进程接收到用户输入的URL请求浏览器进程便将该URL转发给网络进程。
- 然后在网络进程中发起真正的URL请求。
- 接着网络进程接收到了响应头数据,便解析响应头数据,并将数据转发给浏览器进程。
- 浏览器进程接收到网络进程的响应头数据之后,发送“提交导航(CommitNavigation)”消息到渲染进程;
- 渲染进程接收到“提交导航”的消息之后便开始准备接收HTML数据接收数据的方式是直接和网络进程建立数据管道
- 最后渲染进程会向浏览器进程“确认提交”,这是告诉浏览器进程:“已经准备好接受和解析页面数据了”。
- 浏览器进程接收到渲染进程“提交文档”的消息之后,便开始移除之前旧的文档,然后更新浏览器进程中的页面状态。
这其中,**用户发出URL请求到页面开始解析的这个过程就叫做导航**。
## 从输入URL到页面展示
现在我们知道了浏览器几个主要进程的职责,还有在导航过程中需要经历的几个主要的阶段,下面我们就来详细分析下这些阶段,同时也就解答了开头所说的那道经典的面试题。
### 1. 用户输入
当用户在地址栏中输入一个查询关键字时,地址栏会判断输入的关键字是**搜索内容**,还是**请求的URL**。
- 如果是搜索内容地址栏会使用浏览器默认的搜索引擎来合成新的带搜索关键字的URL。
- 如果判断输入内容符合URL规则比如输入的是 time.geekbang.org那么地址栏会根据规则把这段内容加上协议合成为完整的URL如 [https://time.geekbang.org](https://time.geekbang.org)。
当用户输入关键字并键入回车之后这意味着当前页面即将要被替换成新的页面不过在这个流程继续之前浏览器还给了当前页面一次执行beforeunload事件的机会beforeunload事件允许页面在退出之前执行一些数据清理操作还可以询问用户是否要离开当前页面比如当前页面可能有未提交完成的表单等情况因此用户可以通过beforeunload事件来取消导航让浏览器不再执行任何后续工作。
当前页面没有监听beforeunload事件或者同意了继续后续流程那么浏览器便进入下图的状态
<img src="https://static001.geekbang.org/resource/image/fa/30/fad33fc7c5f2bdf4e20cac7691484130.png" alt="">
从图中可以看出,当浏览器刚开始加载一个地址之后,标签页上的图标便进入了加载状态。但此时图中页面显示的依然是之前打开的页面内容,并没立即替换为极客时间的页面。因为需要等待提交文档阶段,页面内容才会被替换。
### 2. URL请求过程
接下来便进入了页面资源请求过程。这时浏览器进程会通过进程间通信IPC把URL请求发送至网络进程网络进程接收到URL请求后会在这里发起真正的URL请求流程。那具体流程是怎样的呢
首先网络进程会查找本地缓存是否缓存了该资源。如果有缓存资源那么直接返回资源给浏览器进程如果在缓存中没有查找到资源那么直接进入网络请求流程。这请求前的第一步是要进行DNS解析以获取请求域名的服务器IP地址。如果请求协议是HTTPS那么还需要建立TLS连接。
接下来就是利用IP地址和服务器建立TCP连接。连接建立之后浏览器端会构建请求行、请求头等信息并把和该域名相关的Cookie等数据附加到请求头中然后向服务器发送构建的请求信息。
服务器接收到请求信息后,会根据请求信息生成响应数据(包括响应行、响应头和响应体等信息),并发给网络进程。等网络进程接收了响应行和响应头之后,就开始解析响应头的内容了。(为了方便讲述,下面我将服务器返回的响应头和响应行统称为响应头。)
**1重定向**
在接收到服务器返回的响应头后网络进程开始解析响应头如果发现返回的状态码是301或者302那么说明服务器需要浏览器重定向到其他URL。这时网络进程会从响应头的Location字段里面读取重定向的地址然后再发起新的HTTP或者HTTPS请求一切又重头开始了。
比如,我们在终端里输入以下命令:
```
curl -I http://time.geekbang.org/
```
`curl -I + URL`的命令是接收服务器返回的响应头的信息。执行命令后,我们看到服务器返回的响应头信息如下:
<img src="https://static001.geekbang.org/resource/image/65/7e/655cbf32dd4bb6f9decc5c7f9a535a7e.png" alt="">
从图中可以看出极客时间服务器会通过重定向的方式把所有HTTP请求转换为HTTPS请求。也就是说你使用HTTP向极客时间服务器请求时服务器会返回一个包含有301或者302状态码响应头并把响应头的Location字段中填上HTTPS的地址这就是告诉了浏览器要重新导航到新的地址上。
下面我们再使用HTTPS协议对极客时间发起请求看看服务器的响应头信息是什么样子的。
```
curl -I https://time.geekbang.org/
```
我们看到服务器返回如下信息:
<img src="https://static001.geekbang.org/resource/image/0c/43/0c4987fe5d05646fa8245d8cc50d1a43.png" alt="">
从图中可以看出服务器返回的响应头的状态码是200这是告诉浏览器一切正常可以继续往下处理该请求了。
好了,以上是重定向内容的介绍。现在你应该理解了,**在导航过程中如果服务器响应行的状态码包含了301、302一类的跳转信息浏览器会跳转到新的地址继续导航如果响应行是200那么表示浏览器可以继续处理该请求**。
**2响应数据类型处理**
在处理了跳转信息之后我们继续导航流程的分析。URL请求的数据类型有时候是一个下载类型有时候是正常的HTML页面那么浏览器是如何区分它们呢
答案是Content-Type。**Content-Type是HTTP头中一个非常重要的字段 它告诉浏览器服务器返回的响应体数据是什么类型**然后浏览器会根据Content-Type的值来决定如何显示响应体的内容。
这里我们还是以极客时间为例看看极客时间官网返回的Content-Type值是什么。在终端输入以下命令
```
curl -I https://time.geekbang.org/
```
返回信息如下图:
<img src="https://static001.geekbang.org/resource/image/89/1c/8951e161b5f44a73e52c16b631a63e1c.png" alt="">
从图中可以看到响应头中的Content-type字段的值是text/html这就是告诉浏览器服务器返回的数据是**HTML格式**。
接下来我们再来利用curl来请求极客时间安装包的地址如下所示
```
curl -I https://res001.geekbang.org/apps/geektime/android/2.3.1/official/geektime_2.3.1_20190527-2136_offical.apk
```
请求后返回的响应头信息如下:
<img src="https://static001.geekbang.org/resource/image/59/3b/595902748d7d4c2f9c1d4783962ae43b.png" alt="">
从返回的响应头信息来看其Content-Type的值是application/octet-stream显示数据是**字节流类型**的,通常情况下,浏览器会按照**下载类型**来处理该请求。
需要注意的是如果服务器配置Content-Type不正确比如将text/html类型配置成application/octet-stream类型那么浏览器可能会曲解文件内容比如会将一个本来是用来展示的页面变成了一个下载文件。
所以不同Content-Type的后续处理流程也截然不同。如果Content-Type字段的值被浏览器判断为**下载类型那么该请求会被提交给浏览器的下载管理器同时该URL请求的导航流程就此结束**。但如果是**HTML那么浏览器则会继续进行导航流程**。由于Chrome的页面渲染是运行在渲染进程中的所以接下来就需要准备渲染进程了。
### 3. 准备渲染进程
默认情况下Chrome会为每个页面分配一个渲染进程也就是说每打开一个新页面就会配套创建一个新的渲染进程。但是也有一些例外在某些情况下浏览器会让多个页面直接运行在同一个渲染进程中。
比如我从极客时间的首页里面打开了另外一个页面——算法训练营我们看下图的Chrome的任务管理器截图
<img src="https://static001.geekbang.org/resource/image/d8/28/d8fe2afbd8ea2d4a8d8cc4bb14c50f28.png" alt="">
从图中可以看出打开的这三个页面都是运行在同一个渲染进程中进程ID是23601。
**那什么情况下多个页面会同时运行在一个渲染进程中呢?**
要解决这个问题我们就需要先了解下什么是同一站点same-site。具体地讲我们将“**同一站点**”定义为**根域名**例如geekbang.org加上**协议**例如https:// 或者http://),还包含了该根域名下的所有子域名和不同的端口,比如下面这三个:
```
https://time.geekbang.org
https://www.geekbang.org
https://www.geekbang.org:8080
```
它们都是属于**同一站点**因为它们的协议都是HTTPS而且根域名也都是geekbang.org。
Chrome的默认策略是每个标签对应一个渲染进程。但**如果从一个页面打开了另一个新页面,而新页面和当前页面属于同一站点的话,那么新页面会复用父页面的渲染进程**。官方把这个默认策略叫process-per-site-instance。
那若新页面和当前页面不属于同一站点情况又会发生什么样的变化呢比如我通过极客邦页面里的链接打开InfoQ的官网[https://www.infoq.cn/](https://www.infoq.cn/) 因为infoq.cn和geekbang.org不属于同一站点所以infoq.cn会使用一个新的渲染进程你可以参考下图
<img src="https://static001.geekbang.org/resource/image/fb/74/fba1dd05f0aeba93a5cb25f305971274.png" alt="">
从图中任务管理器可以看出:由于极客邦和极客时间的标签页拥有**相同的协议和根域名**,所以它们属于**同一站点**并运行在同一个渲染进程中而infoq.cn的根域名不同于geekbang.org也就是说InfoQ和极客邦不属于同一站点因此它们会运行在两个不同的渲染进程之中。
总结来说,打开一个新页面采用的**渲染进程策略**就是:
- 通常情况下,打开新的页面都会使用单独的渲染进程;
- 如果从A页面打开B页面且A和B都属于**同一站点**的话那么B页面复用A页面的渲染进程如果是其他情况浏览器进程则会为B创建一个新的渲染进程。
渲染进程准备好之后,还不能立即进入文档解析状态,因为此时的文档数据还在网络进程中,并没有提交给渲染进程,所以下一步就进入了提交文档阶段。
### 4. 提交文档
所谓提交文档就是指浏览器进程将网络进程接收到的HTML数据提交给渲染进程具体流程是这样的
- 首先当浏览器进程接收到网络进程的响应头数据之后,便向渲染进程发起“提交文档”的消息;
- 渲染进程接收到“提交文档”的消息后,会和网络进程建立传输数据的“管道”;
- 等文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程;
- 浏览器进程在收到“确认提交”的消息后会更新浏览器界面状态包括了安全状态、地址栏的URL、前进后退的历史状态并更新Web页面。
其中,当渲染进程**确认提交**之后,更新内容如下图所示:
<img src="https://static001.geekbang.org/resource/image/d3/b8/d3c5a6188b09b5b57af439005ae7dfb8.png" alt="">
这也就解释了为什么在浏览器的地址栏里面输入了一个地址后,之前的页面没有立马消失,而是要加载一会儿才会更新页面。
到这里,一个完整的导航流程就“走”完了,这之后就要进入渲染阶段了。
### 5. 渲染阶段
一旦文档被提交,渲染进程便开始页面解析和子资源加载了,关于这个阶段的完整过程,我会在下一篇文章中来专门介绍。这里你只需要先了解一旦页面生成完成,渲染进程会发送一个消息给浏览器进程,浏览器接收到消息后,会停止标签图标上的加载动画。如下所示:
<img src="https://static001.geekbang.org/resource/image/be/58/bef45eb5b01c34e328486004feedd658.png" alt="">
至此一个完整的页面就生成了。那文章开头的“从输入URL到页面展示这中间发生了什么”这个过程及其“串联”的问题也就解决了。
## 总结
好了,今天就到这里,下面我来简单总结下这篇文章的要点:
- 服务器可以根据响应头来控制浏览器的行为,如跳转、网络数据类型判断。
- Chrome默认采用每个标签对应一个渲染进程但是如果两个页面属于同一站点那这两个标签会使用同一个渲染进程。
- 浏览器的导航过程涵盖了从用户发起请求到提交文档给渲染进程的中间所有阶段。
导航流程很重要,它是网络加载流程和渲染流程之间的一座桥梁,如果你理解了导航流程,那么你就能完整串起来整个页面显示流程,这对于你理解浏览器的工作原理起到了点睛的作用。
## 思考时间
最后还是留给你个小作业在上一篇文章中我们介绍了HTTP请求过程在本文我们又介绍了导航流程那么如果再有面试官问你“从输入URL到页面展示这中间发生了什么”这个问题你知道怎么回答了吗可以用你自己的语言组织下就当为你的面试做准备。
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。

View File

@@ -0,0 +1,212 @@
<audio id="audio" title="05 | 渲染流程HTML、CSS和JavaScript是如何变成页面的" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/41/1e/41c879102cba3e316ce662ad34df421e.mp3"></audio>
在[上一篇文章](https://time.geekbang.org/column/article/117637)中我们介绍了导航相关的流程那导航被提交后又会怎么样呢就进入了渲染阶段。这个阶段很重要了解其相关流程能让你“看透”页面是如何工作的有了这些知识你可以解决一系列相关的问题比如能熟练使用开发者工具因为能够理解开发者工具里面大部分项目的含义能优化页面卡顿问题使用JavaScript优化动画流程通过优化样式表来防止强制同步布局等等。
既然它的功能这么强大,那么今天,我们就来好好聊聊**渲染流程**。
通常我们编写好HTML、CSS、JavaScript等文件经过浏览器就会显示出漂亮的页面如下图所示但是你知道它们是如何转化成页面的吗这背后的原理估计很多人都答不上来。
<img src="https://static001.geekbang.org/resource/image/2b/79/2b08a85c63bee68c6fd95dabb648fd79.png" alt="">
从图中可以看出左边输入的是HTML、CSS、JavaScript数据这些数据经过中间渲染模块的处理最终输出为屏幕上的像素。
这中间的**渲染模块**就是我们今天要讨论的主题。为了能更好地理解下文你可以先结合下图快速抓住HTML、CSS和JavaScript的含义
<img src="https://static001.geekbang.org/resource/image/31/e6/31cd7172f743193d682d088a60cb44e6.png" alt="">
从上图可以看出,**HTML的内容是由标记和文本组成**。标记也称为**标签**每个标签都有它自己的语义浏览器会根据标签的语义来正确展示HTML内容。比如上面的`&lt;p&gt;`标签是告诉浏览器在这里的内容需要创建一个新段落,中间的文本就是段落中需要显示的内容。
如果需要改变HTML的字体颜色、大小等信息就需要用到CSS。CSS又称为**层叠样式表,是由选择器和属性组成**比如图中的p选择器它会把HTML里面`&lt;p&gt;`标签的内容选择出来,然后再把选择器的属性值应用到`&lt;p&gt;`标签内容上。选择器里面有个color属性它的值是red这是告诉渲染引擎把`&lt;p&gt;`标签的内容显示为红色。
至于**JavaScript简称为JS使用它可以使网页的内容“动”起来**比如上图中可以通过JavaScript来修改CSS样式值从而达到修改文本颜色的目的。
搞清楚HTML、CSS和JavaScript的含义后那么接下来我们就正式开始分析渲染模块了。
由于渲染机制过于复杂所以渲染模块在执行过程中会被划分为很多子阶段输入的HTML经过这些子阶段最后输出像素。我们把这样的一个处理流程叫做**渲染流水线**,其大致流程如下图所示:
<img src="https://static001.geekbang.org/resource/image/92/e8/9259f8732ddad472e5e08a633ad46de8.png" alt="">
按照渲染的时间顺序流水线可分为如下几个子阶段构建DOM树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成。内容比较多我会用两篇文章来为你详细讲解这各个子阶段。接下来在介绍每个阶段的过程中你应该重点关注以下三点内容
- 开始每个子阶段都有其**输入的内容**
- 然后每个子阶段有其**处理过程**
- 最终每个子阶段会生成**输出内容**。
理解了这三部分内容,能让你更加清晰地理解每个子阶段。
## 构建DOM树
为什么要构建DOM树呢**这是因为浏览器无法直接理解和使用HTML所以需要将HTML转换为浏览器能够理解的结构——DOM树**。
这里我们还需要简单介绍下什么是**树结构**,为了更直观地理解,你可以参考下面我画的几个树结构:
<img src="https://static001.geekbang.org/resource/image/fc/38/fcad0a4e3e73c796f00d6120284a3638.png" alt="">
从图中可以看出,树这种结构非常像我们现实生活中的“树”,其中每个点我们称为**节点**,相连的节点称为**父子节点**。树结构在浏览器中的应用还是比较多的,比如下面我们要介绍的渲染流程,就在频繁地使用树结构。
接下来咱们还是言归正传来看看DOM树的构建过程你可以参考下图
<img src="https://static001.geekbang.org/resource/image/12/79/125849ec56a3ea98d4b476c66c754f79.png" alt="">
从图中可以看出构建DOM树的**输入内容**是一个非常简单的HTML文件然后经由HTML解析器解析最终输出树状结构的DOM。
为了更加直观地理解DOM树你可以打开Chrome的“开发者工具”选择“Console”标签来打开控制台然后在控制台里面输入“document”后回车这样你就能看到一个完整的DOM树结构如下图所示
<img src="https://static001.geekbang.org/resource/image/47/73/47f57c3eee749dd838939bfe5dd64573.png" alt="">
图中的document就是DOM结构你可以看到DOM和HTML内容几乎是一样的但是和HTML不同的是DOM是保存在内存中树状结构可以通过JavaScript来查询或修改其内容。
那下面就来看看如何通过JavaScript来修改DOM的内容在控制台中输入
```
document.getElementsByTagName(&quot;p&quot;)[0].innerText = &quot;black&quot;
```
这行代码的作用是把第一个`&lt;p&gt;`标签的内容修改为black具体执行结果你可以参考下图
<img src="https://static001.geekbang.org/resource/image/e7/74/e730aa1d73c1151c588e2f8c7e22c274.png" alt="">
从图中可以看出,在执行了一段修改第一个`&lt;p&gt;`标签的JavaScript代码后DOM的第一个p节点的内容成功被修改同时页面中的内容也被修改了。
好了现在我们已经生成DOM树了但是DOM节点的样式我们依然不知道要让DOM节点拥有正确的样式这就需要样式计算了。
## 样式计算Recalculate Style
样式计算的目的是为了计算出DOM节点中每个元素的具体样式这个阶段大体可分为三步来完成。
### 1. 把CSS转换为浏览器能够理解的结构
那CSS样式的来源主要有哪些呢你可以先参考下图
<img src="https://static001.geekbang.org/resource/image/bc/7c/bc93df7b8d03b2675f21e1d9e4e1407c.png" alt="">
从图中可以看出CSS样式来源主要有三种
- 通过link引用的外部CSS文件
- `&lt;style&gt;`标记内的 CSS
- 元素的style属性内嵌的CSS
和HTML文件一样浏览器也是无法直接理解这些纯文本的CSS样式所以**当渲染引擎接收到CSS文本时会执行一个转换操作将CSS文本转换为浏览器可以理解的结构——styleSheets**。
为了加深理解你可以在Chrome控制台中查看其结构只需要在控制台中输入document.styleSheets然后就看到如下图所示的结构
<img src="https://static001.geekbang.org/resource/image/8e/ab/8ec7d5ecfadcd05b3f1ec762223a9aab.png" alt="">
从图中可以看出这个样式表包含了很多种样式已经把那三种来源的样式都包含进去了。当然样式表的具体结构不是我们今天讨论的重点你只需要知道渲染引擎会把获取到的CSS文本全部转换为styleSheets结构中的数据并且该结构同时具备了查询和修改功能这会为后面的样式操作提供基础。
### 2. 转换样式表中的属性值,使其标准化
现在我们已经把现有的CSS文本转化为浏览器可以理解的结构了那么**接下来就要对其进行属性值的标准化操作**。
要理解什么是属性值标准化你可以看下面这样一段CSS文本
```
body { font-size: 2em }
p {color:blue;}
span {display: none}
div {font-weight: bold}
div p {color:green;}
div {color:red; }
```
可以看到上面的CSS文本中有很多属性值如2em、blue、bold这些类型数值不容易被渲染引擎理解所以**需要将所有值转换为渲染引擎容易理解的、标准化的计算值**,这个过程就是属性值标准化。
那标准化后的属性值是什么样子的?
<img src="https://static001.geekbang.org/resource/image/12/60/1252c6d3c1a51714606daa6bdad3a560.png" alt="">
从图中可以看到2em被解析成了32pxred被解析成了rgb(255,0,0)bold被解析成了700……
### 3. 计算出DOM树中每个节点的具体样式
现在样式的属性已被标准化了接下来就需要计算DOM树中每个节点的样式属性了如何计算呢
**这就涉及到CSS的继承规则和层叠规则了。**
首先是CSS继承。**CSS继承就是每个DOM节点都包含有父节点的样式**。这么说可能有点抽象我们可以结合具体例子看下面这样一张样式表是如何应用到DOM节点上的。
```
body { font-size: 20px }
p {color:blue;}
span {display: none}
div {font-weight: bold;color:red}
div p {color:green;}
```
这张样式表最终应用到DOM节点的效果如下图所示
<img src="https://static001.geekbang.org/resource/image/fe/b4/fe9a0ea868dc02a3c4a59f6080aa80b4.png" alt="">
从图中可以看出所有子节点都继承了父节点样式。比如body节点的font-size属性是20那body节点下面的所有节点的font-size都等于20。
为了加深你对CSS继承的理解你可以打开Chrome的“开发者工具”选择第一个“element”标签再选择“style”子标签你会看到如下界面
<img src="https://static001.geekbang.org/resource/image/88/b2/88a3aac427cc7c09361eac01a85fc7b2.png" alt="">
这个界面展示的信息很丰富,大致可描述为如下。
- 首先,可以选择要查看的**元素的样式位于图中的区域2中**在图中的第1个区域中点击对应的元素就可以在下面的区域查看该元素的样式了。比如这里我们选择的元素是`&lt;p&gt;`标签位于html.body.div.这个路径下面。
- 其次,可以从**样式来源位于图中的区域3中**中查看样式的具体来源信息看看是来源于样式文件还是来源于UserAgent样式表。**这里需要特别提下UserAgent样式它是浏览器提供的一组默认样式如果你不提供任何样式默认使用的就是UserAgent样式**。
- 最后可以通过区域2和区域3来查看样式继承的具体过程。
以上就是CSS继承的一些特性样式计算过程中会根据DOM节点的继承关系来合理计算节点样式。
样式计算过程中的第二个规则是样式层叠。**层叠是CSS的一个基本特征它是一个定义了如何合并来自多个源的属性值的算法。它在CSS处于核心地位CSS的全称“层叠样式表”正是强调了这一点**。关于层叠的具体规则这里就不做过多介绍了,网上资料也非常多,你可以自行搜索学习。
总之样式计算阶段的目的是为了计算出DOM节点中每个元素的具体样式在计算过程中需要遵守CSS的继承和层叠两个规则。这个阶段最终输出的内容是每个DOM节点的样式并被保存在ComputedStyle的结构内。
如果你想了解每个DOM元素最终的计算样式可以打开Chrome的“开发者工具”选择第一个“element”标签然后再选择“Computed”子标签如下图所示
<img src="https://static001.geekbang.org/resource/image/d8/46/d87415b0187e3860404bf963f1c3d646.png" alt="">
上图红色方框中显示了html.body.div.p标签的ComputedStyle的值。你想要查看哪个元素点击左边对应的标签就可以了。
## 布局阶段
现在我们有DOM树和DOM树中元素的样式但这还不足以显示页面因为我们还不知道DOM元素的几何位置信息。**那么接下来就需要计算出DOM树中可见元素的几何位置我们把这个计算过程叫做布局**。
Chrome在布局阶段需要完成两个任务创建布局树和布局计算。
### 1. 创建布局树
你可能注意到了DOM树还含有很多不可见的元素比如head标签还有使用了display:none属性的元素。所以**在显示之前,我们还要额外地构建一棵只包含可见元素布局树**。
我们结合下图来看看布局树的构造过程:
<img src="https://static001.geekbang.org/resource/image/8e/0e/8e48b77dd48bdc509958e73b9935710e.png" alt="">
从上图可以看出DOM树中所有不可见的节点都没有包含到布局树中。
为了构建布局树,浏览器大体上完成了下面这些工作:
- 遍历DOM树中的所有可见节点并把这些节点加到布局树中
- 而不可见的节点会被布局树忽略掉如head标签下面的全部内容再比如body.p.span这个元素因为它的属性包含 dispaly:none所以这个元素也没有被包进布局树。
### 2. 布局计算
现在我们有了一棵完整的布局树。那么接下来,就要计算布局树节点的坐标位置了。布局的计算过程非常复杂,我们这里先跳过不讲,等到后面章节中我再做详细的介绍。
在执行布局操作的时候会把布局运算的结果重新写回布局树中所以布局树既是输入内容也是输出内容这是布局阶段一个不合理的地方因为在布局阶段并没有清晰地将输入内容和输出内容区分开来。针对这个问题Chrome团队正在重构布局代码下一代布局系统叫LayoutNG试图更清晰地分离输入和输出从而让新设计的布局算法更加简单。
## 总结
好了,今天正文就到这里,我画了下面这张比较完整的渲染流水线,你可以结合这张图来回顾下今天的内容。
<img src="https://static001.geekbang.org/resource/image/a4/9a/a4a0ea4da58260aafc9aabdd37613f9a.png" alt="">
从图中可以看出本节内容我们介绍了渲染流程的前三个阶段DOM生成、样式计算和布局。要点可大致总结为如下
- 浏览器不能直接理解HTML数据所以第一步需要将其转换为浏览器能够理解的DOM树结构
- 生成DOM树后还需要根据CSS样式表来计算出DOM树所有节点的样式
- 最后计算DOM元素的布局信息使其都保存在布局树中。
到这里我们的每个节点都拥有了自己的样式和布局信息,那么后面几个阶段就要利用这些信息去展示页面了,由于篇幅限制,剩下的这些阶段我会在下一篇文章中介绍。
## 思考时间
最后给你留个思考题如果下载CSS文件阻塞了会阻塞DOM树的合成吗会阻塞页面的显示吗
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。

View File

@@ -0,0 +1,195 @@
<audio id="audio" title="06 | 渲染流程HTML、CSS和JavaScript是如何变成页面的" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/28/52/28e24a5972bef66fe8a572ba124cd252.mp3"></audio>
在[上篇文章](https://time.geekbang.org/column/article/118205)中,我们介绍了渲染流水线中的**DOM生成、样式计算**和**布局**三个阶段,那今天我们接着讲解渲染流水线后面的阶段。
这里还是先简单回顾下上节前三个阶段的主要内容在HTML页面内容被提交给渲染引擎之后渲染引擎首先将HTML解析为浏览器可以理解的DOM然后根据CSS样式表计算出DOM树所有节点的样式接着又计算每个元素的几何坐标位置并将这些信息保存在布局树中。
## 分层
现在我们有了布局树,而且每个元素的具体位置信息都计算出来了,那么接下来是不是就要开始着手绘制页面了?
答案依然是否定的。
因为页面中有很多复杂的效果如一些复杂的3D变换、页面滚动或者使用z-indexing做z轴排序等为了更加方便地实现这些效果**渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树**LayerTree。如果你熟悉PS相信你会很容易理解图层的概念正是这些图层叠加在一起构成了最终的页面图像。
要想直观地理解什么是图层你可以打开Chrome的“开发者工具”选择“Layers”标签就可以可视化页面的分层情况如下图所示
<img src="https://static001.geekbang.org/resource/image/e2/c0/e2c917edf5119cddfbec9481372f8fc0.png" alt="">
从上图可以看出,渲染引擎给页面分了很多图层,这些图层按照一定顺序叠加在一起,就形成了最终的页面,你可以参考下图:
<img src="https://static001.geekbang.org/resource/image/cd/78/cd6aac705501d48bda6e8eebca058b78.png" alt="">
现在你知道了**浏览器的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面**。下面我们再来看看这些图层和布局树节点之间的关系,如文中图所示:
<img src="https://static001.geekbang.org/resource/image/e8/61/e8a7e60a2a08e05239456284d2aa4061.png" alt="">
通常情况下,**并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层**。如上图中的span标签没有专属图层那么它们就从属于它们的父节点图层。但不管怎样最终每一个节点都会直接或者间接地从属于一个层。
那么需要满足什么条件,渲染引擎才会为特定的节点创建新的图层呢?通常满足下面两点中任意一点的元素就可以被提升为单独的一个图层。
**第一点,拥有层叠上下文属性的元素会被提升为单独的一层。**
页面是个二维平面但是层叠上下文能够让HTML元素具有三维概念这些HTML元素按照自身属性的优先级分布在垂直于这个二维平面的z轴上。你可以结合下图来直观感受下
<img src="https://static001.geekbang.org/resource/image/a0/19/a03eb12053aac1ac496b61a424f20119.png" alt="">
从图中可以看出明确定位属性的元素、定义透明属性的元素、使用CSS滤镜的元素等都拥有层叠上下文属性。
若你想要了解更多层叠上下文的知识,你可以[参考这篇文章](https://developer.mozilla.org/zh-CN/docs/Web/Guide/CSS/Understanding_z_index/The_stacking_context)。
**第二点需要剪裁clip的地方也会被创建为图层。**
不过首先你需要了解什么是剪裁结合下面的HTML代码
```
&lt;style&gt;
div {
width: 200;
height: 200;
overflow:auto;
background: gray;
}
&lt;/style&gt;
&lt;body&gt;
&lt;div &gt;
&lt;p&gt;所以元素有了层叠上下文的属性或者需要被剪裁,那么就会被提升成为单独一层,你可以参看下图:&lt;/p&gt;
&lt;p&gt;从上图我们可以看到document层上有A和B层而B层之上又有两个图层。这些图层组织在一起也是一颗树状结构。&lt;/p&gt;
&lt;p&gt;图层树是基于布局树来创建的为了找出哪些元素需要在哪些层中渲染引擎会遍历布局树来创建层树Update LayerTree。&lt;/p&gt;
&lt;/div&gt;
&lt;/body&gt;
```
在这里我们把div的大小限定为200 * 200像素而div里面的文字内容比较多文字所显示的区域肯定会超出200 * 200的面积这时候就产生了剪裁渲染引擎会把裁剪文字内容的一部分用于显示在div区域下图是运行时的执行结果
<img src="https://static001.geekbang.org/resource/image/6a/0c/6a583733735edc1e4d7946740eb6fc0c.png" alt="">
出现这种裁剪情况的时候,渲染引擎会为文字部分单独创建一个层,如果出现滚动条,滚动条也会被提升为单独的层。你可以参考下图:
<img src="https://static001.geekbang.org/resource/image/7b/97/7b6ceaab23c6c6d8e5930864ff9d7097.png" alt="">
所以说,元素有了层叠上下文的属性或者需要被剪裁,满足其中任意一点,就会被提升成为单独一层。
## 图层绘制
在完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行绘制,那么接下来我们看看渲染引擎是怎么实现图层绘制的?
试想一下,如果给你一张纸,让你先把纸的背景涂成蓝色,然后在中间位置画一个红色的圆,最后再在圆上画个绿色三角形。你会怎么操作呢?
通常,你会把你的绘制操作分解为三步:
1. 绘制蓝色背景;
1. 在中间绘制一个红色的圆;
1. 再在圆上绘制绿色三角形。
渲染引擎实现图层的绘制与之类似,会把一个图层的绘制拆分成很多小的**绘制指令**,然后再把这些指令按照顺序组成一个待绘制列表,如下图所示:
<img src="https://static001.geekbang.org/resource/image/40/08/40825a55214a7990bba6b9bec6e54108.png" alt="">
从图中可以看出,绘制列表中的指令其实非常简单,就是让其执行一个简单的绘制操作,比如绘制粉色矩形或者黑色的线等。而绘制一个元素通常需要好几条绘制指令,因为每个元素的背景、前景、边框都需要单独的指令去绘制。所以在图层绘制阶段,输出的内容就是这些待绘制列表。
你也可以打开“开发者工具”的“Layers”标签选择“document”层来实际体验下绘制列表如下图所示
<img src="https://static001.geekbang.org/resource/image/30/70/303515c26fcd4eaa9b9966ad7f190370.png" alt="">
在该图中区域1就是document的绘制列表拖动区域2中的进度条可以重现列表的绘制过程。
## 栅格化raster操作
绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。你可以结合下图来看下渲染主线程和合成线程之间的关系:
<img src="https://static001.geekbang.org/resource/image/46/41/46d33b6e5fca889ecbfab4516c80a441.png" alt="">
如上图所示,当图层的绘制列表准备好之后,主线程会把该绘制列表**提交commit**给合成线程,那么接下来合成线程是怎么工作的呢?
那我们得先来看看什么是视口,你可以参看下图:
<img src="https://static001.geekbang.org/resource/image/24/72/242225112f2a3ec97e736c960b88d972.png" alt="">
通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做**视口**viewport
在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。
基于这个原因,**合成线程会将图层划分为图块tile**这些图块的大小通常是256x256或者512x512如下图所示
<img src="https://static001.geekbang.org/resource/image/bc/52/bcc7f6983d5ece8e2dd716f431d0e052.png" alt="">
然后**合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图**。而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的,运行方式如下图所示:
<img src="https://static001.geekbang.org/resource/image/d8/20/d8d77356211e12b47bb9f508e2db8520.png" alt="">
通常栅格化过程都会使用GPU来加速生成使用GPU生成位图的过程叫快速栅格化或者GPU栅格化生成的位图被保存在GPU内存中。
相信你还记得GPU操作是运行在GPU进程中如果栅格化操作使用了GPU那么最终生成位图的操作是在GPU中完成的这就涉及到了跨进程操作。具体形式你可以参考下图
<img src="https://static001.geekbang.org/resource/image/a8/87/a8d954cd8e4722ee03d14afaa14c3987.png" alt="">
从图中可以看出渲染进程把生成图块的指令发送给GPU然后在GPU中执行生成图块的位图并保存在GPU的内存中。
## 合成和显示
一旦所有图块都被光栅化合成线程就会生成一个绘制图块的命令——“DrawQuad”然后将该命令提交给浏览器进程。
浏览器进程里面有一个叫viz的组件用来接收合成线程发过来的DrawQuad命令然后根据DrawQuad命令将其页面内容绘制到内存中最后再将内存显示在屏幕上。
到这里经过这一系列的阶段编写好的HTML、CSS、JavaScript等文件经过浏览器就会显示出漂亮的页面了。
## 渲染流水线大总结
好了我们现在已经分析完了整个渲染流程从HTML到DOM、样式计算、布局、图层、绘制、光栅化、合成和显示。下面我用一张图来总结下这整个渲染流程
<img src="https://static001.geekbang.org/resource/image/97/37/975fcbf7f83cc20d216f3d68a85d0f37.png" alt="">
结合上图,一个完整的渲染流程大致可总结为如下:
1. 渲染进程将HTML内容转换为能够读懂的**DOM树**结构。
1. 渲染引擎将CSS样式表转化为浏览器可以理解的**styleSheets**计算出DOM节点的样式。
1. 创建**布局树**,并计算元素的布局信息。
1. 对布局树进行分层,并生成**分层树**。
1. 为每个图层生成**绘制列表**,并将其提交到合成线程。
1. 合成线程将图层分成**图块**,并在**光栅化线程池**中将图块转换成位图。
1. 合成线程发送绘制图块命令**DrawQuad**给浏览器进程。
1. 浏览器进程根据DrawQuad消息**生成页面**,并**显示**到显示器上。
## 相关概念
有了上面介绍渲染流水线的基础,我们再来看看三个和渲染流水线相关的概念——**“重排”“重绘”和“合成”**。理解了这三个概念对于你后续Web的性能优化会有很大帮助。
### 1. 更新了元素的几何属性(重排)
你可先参考下图:
<img src="https://static001.geekbang.org/resource/image/b3/e5/b3ed565230fe4f5c1886304a8ff754e5.png" alt="">
从上图可以看出如果你通过JavaScript或者CSS修改元素的几何位置属性例如改变元素的宽度、高度等那么浏览器会触发重新布局解析之后的一系列子阶段这个过程就叫**重排**。无疑,**重排需要更新完整的渲染流水线,所以开销也是最大的**。
### 2. 更新元素的绘制属性(重绘)
接下来我们再来看看重绘比如通过JavaScript更改某些元素的背景颜色渲染流水线会怎样调整呢你可以参考下图
<img src="https://static001.geekbang.org/resource/image/3c/03/3c1b7310648cccbf6aa4a42ad0202b03.png" alt="">
从图中可以看出,如果修改了元素的背景颜色,那么布局阶段将不会被执行,因为并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程就叫**重绘**。相较于重排操作,**重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些**。
### 3. 直接合成阶段
那如果你更改一个既不要布局也不要绘制的属性,会发生什么变化呢?渲染引擎将跳过布局和绘制,只执行后续的合成操作,我们把这个过程叫做**合成**。具体流程参考下图:
<img src="https://static001.geekbang.org/resource/image/02/2c/024bf6c83b8146d267f476555d953a2c.png" alt="">
在上图中我们使用了CSS的transform来实现动画效果这可以避开重排和重绘阶段直接在非主线程上执行合成动画操作。这样的效率是最高的因为是在非主线程上合成并没有占用主线程的资源另外也避开了布局和绘制两个子阶段所以**相对于重绘和重排,合成能大大提升绘制效率**。
至于如何用这些概念去优化页面,我们会在后面相关章节做详细讲解的,这里你只需要先结合“渲染流水线”弄明白这三个概念及原理就行。
## 总结
通过本文的分析你应该可以看到Chrome的渲染流水线还是相当复杂晦涩且难以理解不过Chrome团队在不断添加新功能的同时也在不断地重构一些子阶段目的就是**让整体渲染架构变得更加简单和高效**,正所谓大道至简。
通过这么多年的生活和工作经验来看,无论是做架构设计、产品设计,还是具体到代码的实现,甚至处理生活中的一些事情,能够把复杂问题简单化的人都是具有大智慧的。所以,在工作或生活中,你若想要简化遇到的问题,就要刻意地练习,练就抓住问题本质的能力,把那些复杂的问题简单化,从而最终真正解决问题。
## 思考时间
在优化Web性能的方法中减少重绘、重排是一种很好的优化方式那么结合文中的分析你能总结出来为什么减少重绘、重排能优化Web性能吗那又有那些具体的实践方法能减少重绘、重排呢
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。