mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2026-05-11 04:04:34 +08:00
del
This commit is contained in:
@@ -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("freeze");
|
||||
}
|
||||
}
|
||||
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是以一个非常快速的速度在进化,越来越多的业务和应用都逐渐转至浏览器来开发,身为开发人员,我们不能坐视不管,而应该紧跟其步伐,收获这波技术红利**。
|
||||
|
||||
## 思考时间
|
||||
|
||||
最后,给你留个思考题:回顾浏览器的进化路线,你认为推动浏览器发展的主要动力是什么?
|
||||
|
||||
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。
|
||||
|
||||
|
||||
@@ -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页面性能的时候有一个重要的指标叫“**FP(First 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了。**TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议**。相对于UDP,TCP有下面两个特点:
|
||||
|
||||
- 对于数据包丢失的情况,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的关系?
|
||||
|
||||
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。
|
||||
|
||||
|
||||
@@ -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="">
|
||||
|
||||
首先浏览器会向服务器发送**请求行**,它包括了**请求方法、请求URI(Uniform 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:"4f80f-13c-3a1xb12a"
|
||||
|
||||
```
|
||||
|
||||
服务器收到请求头后,会根据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请求的各个阶段,如果一个页面的网络加载时间过久,你是如何分析卡在哪个阶段的?
|
||||
|
||||
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。
|
||||
@@ -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到页面展示,这中间发生了什么?”这个问题,你知道怎么回答了吗?可以用你自己的语言组织下,就当为你的面试做准备。
|
||||
|
||||
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。
|
||||
@@ -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内容。比如上面的`<p>`标签是告诉浏览器在这里的内容需要创建一个新段落,中间的文本就是段落中需要显示的内容。
|
||||
|
||||
如果需要改变HTML的字体颜色、大小等信息,就需要用到CSS。CSS又称为**层叠样式表,是由选择器和属性组成**,比如图中的p选择器,它会把HTML里面`<p>`标签的内容选择出来,然后再把选择器的属性值应用到`<p>`标签内容上。选择器里面有个color属性,它的值是red,这是告诉渲染引擎把`<p>`标签的内容显示为红色。
|
||||
|
||||
至于**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("p")[0].innerText = "black"
|
||||
|
||||
```
|
||||
|
||||
这行代码的作用是把第一个`<p>`标签的内容修改为black,具体执行结果你可以参考下图:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e7/74/e730aa1d73c1151c588e2f8c7e22c274.png" alt="">
|
||||
|
||||
从图中可以看出,在执行了一段修改第一个`<p>`标签的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文件
|
||||
- `<style>`标记内的 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被解析成了32px,red被解析成了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个区域中点击对应的元素,就可以在下面的区域查看该元素的样式了。比如这里我们选择的元素是`<p>`标签,位于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树的合成吗?会阻塞页面的显示吗?
|
||||
|
||||
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。
|
||||
@@ -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代码:
|
||||
|
||||
```
|
||||
<style>
|
||||
div {
|
||||
width: 200;
|
||||
height: 200;
|
||||
overflow:auto;
|
||||
background: gray;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<div >
|
||||
<p>所以元素有了层叠上下文的属性或者需要被剪裁,那么就会被提升成为单独一层,你可以参看下图:</p>
|
||||
<p>从上图我们可以看到,document层上有A和B层,而B层之上又有两个图层。这些图层组织在一起也是一颗树状结构。</p>
|
||||
<p>图层树是基于布局树来创建的,为了找出哪些元素需要在哪些层中,渲染引擎会遍历布局树来创建层树(Update LayerTree)。</p>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
```
|
||||
|
||||
在这里我们把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性能吗?那又有那些具体的实践方法能减少重绘、重排呢?
|
||||
|
||||
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。
|
||||
Reference in New Issue
Block a user