从输入URL到页面呈现,中间经历了什么?
“浏览器中,从输入URL到页面呈现出来,中间发生了什么?这篇文章将带你重新了解浏览器渲染一个完整页面的过程及原理。
“ 浏览器中,从输入URL到页面呈现出来,中间发生了什么? ”这是一道相当经典的面试题了,可以全面了解一个人知识的掌握程度,期中涉及到了网络、操作系统、浏览器、Web等一系列的知识。我之前的了解大概有下边这几点:
- DNS寻址:由域名(www.baidu.com)找到对应的ip地址(110.242.68.3)。
- 浏览器缓存:强制缓存、协商缓存。
- TCP三次握手。
- 发起HTTP请求。
- 渲染阶段:构建DOM Tree和CSS Tree。
- TCP四次挥手。
这道题基本上没有对错,只是看对知识的掌握程度,多数情况下我们只能回答例如上述这样的部分知识点,并不能将这些知识点串联起来,无法系统且全面的回答这个问题。
接下来我们就一起探索一下整个流程,下面是梳理出来的“从输入URL到页面呈现”完整示意图:
从图可以看出,整个过程需要各个进程之间的配合,现代浏览器是多进程架构的,主要分为:
- 浏览器主进程 :主要负责界面显示、用户交互、子进程管理,同事提供存储等功能。
- 渲染进程 :将HTML、CSS和JavaScript转换为用户可以与之交互的网页,排版引擎和JavaScript V8引擎都运行在该进程中,出于安全考虑,渲染进程是运行在沙箱模式下。
- 网络进程 :主要负责页面的网络资源加载。
- GPU进程 :Chrome开始是没有GPU进程的。GPU的初衷是为了实现3D CSS的效果,但是随后网页、Chrome的UI界面都采用GPU来绘制,是的GPU成为浏览器普遍的需求。Chrome在后续的架构中也加入了GPU进程。
- 插件进程 :主要负责插件的运行,由于插件容易崩溃,所以需要插件进程来隔离,确保插件进程崩溃不会对浏览器和页面造成影响。
正是这样的多进程架构导致我们仅仅打开一个页面,就会有至少四个必要的进程(插件进程除外),最新版本的Chrome又多了(Storage、Audio Service、备用渲染进程),这也是Chrome团队在推进的一种“ 面向服务的架构 ”,将原来的各种模块重构成独立的服务(Service),每个服务(Service)都可以在独立的进程中运行,访问Service都必须使用定好的接口,用过IPC来通信,从而构建一个 更内聚、松耦合、易于维护和扩展的系统 。
其中, 用户发出URL请求到页面开始解析的过程,叫做导航 。
从输入URL到页面展示
1.用户输入
当用户在地址栏中输入一个关键字时,地址栏会判断输入的关键字是 搜索内容 还是 请求的URL 。
- 如果是搜索内容,地址栏会使用浏览器默认的搜索引擎合成新的带搜索关键字的URL。
- 如果输入内容符合URL规则,比如输入的是www.baidu.com,那么地址栏会根据规则,把这段内容加上协议,合成完整的URL,如 https://www.baidu.com 。
当用户输入关键字并按下回车之后,这意味着当前页面即将要被替换成新的页面,不过这个流程之前,浏览器还给了当前页面一次执行beforeunload事件的机会,beforeunload事件允许页面在退出之前执行一些数据清理的操作,还可以询问用户是否要离开当前页面,比如当前页面可能有未提交完成的表单情况,因此用户可以通过beforeunload事件来取消导航,让浏览器不在执行后续的工作。
当前页面没有监听beforeunload事件或者同意了继续后续流程,那么浏览器便进入下图的状态:
图中可以看出,当浏览器刚开始加载一个地址后,标签页上的图标便进入了加载状态,但此时图中页面显示的依然是之前的页面内容,并没有替换为百度的页面,因为需要等待提交文档阶段完成,页面内容才会被替换。
2.URL请求过程
接下来,便进入了页面资源请求过程。这是,浏览器进程会通过进程间通信(IPC)把URL请求发送至网络进程,网络进程接收到URL请求后,会在这里发起真正的URL请求流程。
首先,网络进程会查找本地是否缓存了该资源。如果有缓存资源,那么直接返回键资源给浏览器进程;如果在缓存中没有查找到资源,那么直接进入网络请求流程。这请求前的第一步是要进行DNS解析,以获取请求域名的服务器IP地址,如果请求协议是HTTPS,那么还需要简历TLS连接。
接下来就是利用IP地址和服务器简历TCP连接。连接建立之后,浏览器会构建请求行、请求头信息,并把和该域名相关的Cookie等数据附加到请求头中,然后像服务器发送构建的请求信息。
服务器接收到请求信息后,会根据请求信息生成响应数据(包含响应行、响应头和响应体等信息),并发送给网络进程。等网络进程接收了响应行和响应头之后,就开始解析响应头的内容了。
2.1重定向
在接收到服务器返回的响应都之后,网络进程开始解析响应头,如果发现返回的状态码是301或者302,那么说明服务器需要浏览器重定向到其他URL。这时网络进程会从响应头的Location字段里面读取重定向的地址,然后发起新的HTTP或者HTTPS请求,一切又重头开始了。
比如我们在终端里输入一下命令:
已複製!curl -I http://www.baidu.com
curl -I + URL的命令是接受服务器返回的响应头信息。执行命令后,我们看到服务器返回的响应头信息入下:
图中看出,laughingzhu.cn服务器会通过重定向的方式把HTTP请求转换为HTTPS请求。也就是说你用HTTP向laughingzhu.cn服务器请求是,服务器会返回一个包含301或者302的状态码响应头,并把响应头的Location字段中填上HTTPS的地址,这就是告诉浏览器要重新导航到新的地址上。
接下来是通过HTTPS协议对即可时间发起请求,响应信息如下:
服务器返回的响应头的状态码是200,这是告诉浏览器一切正常,可以继续往下处理该请求。
在导航过程中,如果服务器响应的状态码包含了301,302等异类的重定向信息,浏览器会跳转到新的地址继续导航;如果响应行是200,表示浏览器可以继续处理该请求。
2.2.响应数据类型处理
在处理了跳转信息之后,继续导航流程的分析。URL请求的数据类型,有时候是一个下载类型,有时候是正常的HTML页面,浏览器通过Content-Type。 Content-Type是HTTP头中一个非常重要的字段,他告诉浏览器服务器返回的响应体数据是什么类型 ,然后浏览器会根据Content-Type的值来决定如何显示响应体的内容。
图中看到,响应头的Content-Type字段的值是text/html,即告诉浏览器,服务器返回的数据格式是HTML格式。
我们利用curl请求另一个链接地址:
图中响应头Content-Type的值是application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,为 office文件所对应类型 ,通常情况下浏览器会按照 下载类型 来处理该请求。
如果服务器设置Content-Type不正确,比如将text/html类型设置成application/octet-stream这种字节流类型,那么浏览器可能会曲解文件内容,比如会将一个本来是用来展示的页面,变成一个下载文件。
所以不同的Content-Type的后续处理流程不同。如果Content-Type字段的值被浏览器判断为下载类型,那么该请求会被提交给浏览器的下载管理器,同时该URL请求的导航流程就此结束。但如果是HTML,那么浏览器则会被继续进行导航流程。由于Chrome的页面渲染是在渲染进程中的,所以接下来就需要准备渲染进程了。
3.准备渲染进程
默认情况下,Chrome会为每个页面分配一个渲染进程,也就是说,每打开一个新页面就会配套创建一个新的渲染进程。但是也有一些列外,在某些请求下,浏览器会让多个页面直接运行在同一个渲染进程中。
比如从laughingzhu.cn中打开了多个详情页,如下图:
可以看出两个页面都是运行在同一个渲染进程中,进程ID是4179。
我们先了解一下什么是同一站点(same-site)。具体地讲,我们将“ 同一站点 ”定义为 根域名 (例如,laughingzhu.cn)加上 协议 (例如,https://或者http://),还包含了该根域名下的所有子域名和不同的端口,比如
已複製!https://ly.laughingzhu.cn https://www.laughingzhu.cn https://www.laughingzhu.cn:8080
它们都属于 同一站点 ,因为他们的协议都是HTTPS,而且根域名都是laughingzhu.cn。
Chrome的默认策略是,每个标签对应一个渲染进程。但 如果从一个页面打开了另一个新页面,而新页面和当前页面属于同一站点的话,那么新页面会复用父页面的渲染进程 。官方吧这个默认策略叫做process-per-site-instance,具体可参考多进程架构。
总的来说,打开一个新页面采用的 渲染进程策略 就是:
- 通常情况下,打开新的页面都会使用单独的渲染进程;
- 如果从A页面打开B页面,且A和B都属于 同一站点 的话,那么B页面复用A页面的渲染进程;如果是其他情况,浏览器进程则会为页面B创建一个新的渲染进程。
渲染进程准备好之后,还不能立即进入文档解析状态,因为此时的文档数据还在网络进程中,并没有提交给渲染进程,所有下一步就是提交文档阶段。
4.提交文档
所谓提交文档,就是指浏览器进程将网络进程接收到的HTML数据提交给渲染进程,具体流程是这样的:
- 首先当浏览器进程接收到网络进程的响应头数据之后,便向渲染进程发起“提交文档”的消息;
- 渲染进程接收到“提交文档”的消息后,会和网络进程简历传输数据的“管道”。
- 等文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程;
- 浏览器进程在收到“确认提交”的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的URL、前进后退的历史状态,并更新Web页面。
其中,当渲染进程 确认提交 之后,更新内容如下:
这也就解释了为什么在浏览器的地址栏里输入了一个地址后,之前的页面没有立马消失,而是要加载一会儿更新页面。
到这里,一个完整的导航流程就“走”完了,这之后就要进入渲染阶段了。
5.渲染阶段
文档一旦被提交,就进入了渲染阶段,了解其相关流程能让你“看透”页面是怎么工作的,有了这些知识,可以解决一些列的相关的问题,比如能熟练使用开发者工具,能够理解开发者工具里面大部分项目的含义,能优化页面卡顿问题,使用JavaSctipt优化动画流程,通过优化样式来防止强制同步布局等等。
通常,我们编写好HTML、CSS、JavaScript等文件,经过浏览器就会显示出漂亮的页面(如下图所示):
图中左边输入的是HTML、CSS、JavaScript数据,这些数据经过中间渲染模块的处理,最终输出为屏幕上我们看到的像素图像。为了更好的理解接下来的内容,可以结合下图简单立即HTML、CSS、JavaScript的含义:
HTML :HTML的内容是有标记和文本组成。标记也称为标签,每个标签都有它自己的语义,浏览器会根据标签的语义来展示HTML内容。 CSS :如果需要改变HTML的字体颜色、大小等信息,就需要用到CSS。 CSS又称为层叠样式表 ,是有选择器和属性组成。 JavaScript : 使用它可以使网页的内容“动”起来 。
渲染模块在执行过程中会被划分为很多子阶段,输入的HTML经过这些子阶段,最后输出像素。我们把这样的一个处理流程叫做 渲染流水线 ,其大致流程如下:
按照渲染的时间顺序,可以分为如下几个子阶段:构建DOM树,样式计算、布局阶段、分层、绘制、分块、光栅化和合成。
5.1构建DOM树(DOM)
为什么要构建DOM🌲呢?这是因为浏览器无法理解和使用HTML,所以需要将HTML转换为浏览器能够理解的结构--DOM树。
DOM树构建过程示意图
构建DOM树的 输入内容 是一个简单的HTML文件,然后经由HTML解析器解析,最终输出树状结构的DOM。现在DOM树生成了,但是DOM节点的样式不知道,要让DOM节点拥有正确的样式,就需要样式计算。
5.2样式结算(Style)
样式极端的目的是为了计算出DOM节点中每个元素的具体样式,这个阶段大体可分为三步完成。
(1)把CSS转换为浏览器能够理解的结构
HTML加载CSS的三种方式
和HTML文件一样,浏览器也无法直接理解这些纯文本的CSS样式,所以 当渲染引擎接收到CSS文本时,会执行一个转换操作,将CSS文本转换为浏览器可以理解的结构--styleSheets。
可以在控制台中输入document.styleSheets,结构大致如下:
(2)转换样式表中的属性值,使其标准化
通过上述的操作后我们已经将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这些类型的数值是无法被渲染引擎理解,所有 需要将所有值转换为渲染引擎容易理解的、标准化的计算值 ,这个过程就是属性值标准化。标准化后的属性值是什么样子的?
(3)计算出DOM树中每个节点的具体样式。
现在样式的属性已被标准化了,接下来就需要计算DOM树中每个节点的样式属性了,这里涉及到 CSS的继承规则和层叠规则 。
首先是CSS继承。CSS继承就是每个DOM节点都包含有父节点的样式。具体例子如下:
已複製!body { font-size: 20px } p {color:blue;} span {display: none} div {font-weight: bold;color:red} div p {color:green;}
计算后DOM样式
图中可以看出所有子节点都继承了父节点样式。比如body节点的font-size属性是20,那body节点下面的所有子节点的font-size默认都是20。
Chrome开发者工具中样式继承过程界面
样式计算过程中的第二个规则是样式层叠。层叠是CSS的一个基本特征,他是一个定义了如何合并来自多个源的属性值的算法。他在CSS处于核心地位,CSS的全程“层叠样式表”正式强调了这一点。 总的来说CSS选择器的优先级大概为: !important > 行内样式 > ID > 类、伪类、属性 > 标签名,伪元素 >通配符> 继承>浏览器默认属性。
总的来说,样式计算阶段的目的是为了计算出DOM节点中每个元素的具体样式,在计算过程中需要遵守CSS的继承和层叠两个规则。这个阶段最终输出的内容是每个DOM节点的样式,并被保存在ComputedStyle的结构内。
如果想了解每个DOM元素最终的计算样式,可以打开Chrome的“开发者工具”,选择一个element标签,然后选择“Computed”子标签,如图: DOM元素最终计算的样式
5.3布局阶段(Layout)
现在我们有DOM树和DOM树中元素的样式,但这还不足以显示页面,因为我们还不知道DOM元素的几何位置信息。那么接下来就需要 计算出DOM树中可见元素的几何位置,我们把这个计算过程叫做布局 。
Chrome在布局阶段需要完成两个任务:创建布局树和布局计算。
创建布局树
DOM树还含有许多不可见的元素,如head、使用了display: none属性的元素。所以 在显示之前,我们还要额外地构建一颗只包含可见元素布局树 。
布局树构造过程示意图
从图中可以看出,DOM树中所有不可见的节点都没有包含到布局树中。 为了构建布局树,浏览器大体完成了下面这些工作:
- 遍历DOM树中的所有可见节点,并把这些节点加到布局树中;
- 不可见的节点会被布局树忽略掉,如head标签下面的全部内容,再比如body.p.span这个元素,它的属性包含display: none,所以这个元素也没有被包含进布局树。
到这里,已经渲染进程流水线已经完成了一半的工作。
5.4分层(Layer)
现在我们有了布局树,每个元素的具体位置信息都计算出来了,但是页面还不能立即绘制,因为页面中有很多负责的效果。如一些复杂的3D变换、页面滚动、或者使用z-index做z轴排序等,为了更加方便地实现这些效果, 渲染引擎还需要为特定的节点生成专用的图层,并生成一颗对饮的图层树(LayerTree) 。如果玩过PS的大家就很容易理解图层的概念,正式这些图层叠加在一起构成了最终的页面图像。
更加直观的如下图,在Chrome的“开发者工具”。选择“Layers”标签,就可以可视化页面的分层情况:
渲染引擎给页面多图层示意图
渲染引擎给页面分了跟多图层,这些图层按照一定顺序叠加在一起,就形成了最终的页面: 图层叠加的最终效果
浏览器的页面实际上被分成了很多图层,这些图像叠加后合成了最终的页面。接下来再来看看图层和布局树节点的关系,如图:
布局树和图层树关系示意图
通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。如图中的span标签就没有专属图层,它就从属于父节点图层。但不管怎样,最终每一个节点都会直接或间接地从属于一个图层。
通常满足下面两点中的任意一点的元素就可以被提升为单独的一个图层。
第一点,拥有层叠上下文属性的元素会被提升为单独的一层。
页面是个二维的平面,但是层叠上下文能够让HTML元素具有三维概念,这些HTML元素按照自身属性的优先级分布在垂直于这个二维平面的z轴上。列入: 层叠上下文示意图
从图中可以看出,明确使用定位属性的元素、定义透明属性的元素、使用CSS滤镜的元素等,都拥有层叠上下文属性。可参考层叠上下文。
第二点,需要剪裁(clip)的地方也会被创建为图层。
这里结合下面代码简单了解以下是什么是剪裁:
已複製!<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 区域,下图是运行时的执行结果: 被剪裁的内容会出现在单独的一层
出现这中剪裁情况的时候,渲染引擎会为文字部分单独创建一个层,如果出现滚动条,滚动条也会被提升为单独的层。所以说元素有了层叠上下文的属性或者不要被剪裁,满足其中任意一点,就会被提升为单独一层。
5.5图层绘制(Paint)
渲染引擎实现图层的绘制与之类似,会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表,如下图所示:
从图中可以看出,绘制列表中的指令其实非常简单,就是让其执行一个简单的绘制操作,比如绘制粉色矩形或者黑色的线等。而绘制一个元素通常需要好几条绘制指令,因为每个元素的背景、前景、边框都需要单独的指令去绘制。所以在图层绘制阶段,输出的内容就是这些待绘制列表。
你也可以打开“开发者工具”的“Layers”标签,选择“document”层,来实际体验下绘制列表,如下图所示:
在该图中,区域 1 就是 document 的绘制列表,拖动区域 2 中的进度条可以重现列表的绘制过程。
5.6栅格化(Raster)
绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。你可以结合下图来看下渲染主线程和合成线程之间的关系: 渲染进程中的合成线层和主线程
如上图所示,当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程,那么接下来合成线程是怎么工作的呢?那我们得先来看看什么是视口,你可以参看下图:
通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部门叫做 视口(viewport) 。
在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。
基于这个原因,合成线程会将图层划分为图块(tile),这些图块的大小通常是 256x256 或者 512x512,如下图所示:
然后合**成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。**而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的,运行方式如下图所示:
合成线程提交图块给栅格化线程池
通常,栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。
上边提到GPU 操作是运行在 GPU 进程中,如果栅格化操作使用了 GPU,那么最终生成位图的操作是在 GPU 中完成的,这就涉及到了跨进程操作。具体形式你可以参考下图:
GPU栅格化
渲染进程把生成图块的指令发给GPU,然后在GPU中执行生成图块的位图,并保存在GPU的内容中。
合成和显示
一旦所有图块都被栅格化,合成线层就会生成一个绘制图块的命令--“DrawQuad”,然后将该命令提交给浏览器进程。
浏览器接收合成线程发送过来的DrawQuad命令,然后根据对应的指令将页面内容会知道内存中,最后将内存显示在屏幕上。
5.7渲染流程总结:
上边分析玩了整个渲染流程,从HTML到DOM、样式计算、布局、图层、绘制、光栅化、合成和显示。流程如图:
完整的渲染流水线示意图
结合上图,一个完整的渲染流程大致可总结如下:
- 渲染进程将HTML内容转换为能够读懂的 DOM树 结构。
- 渲染引擎将CSS样式表转化为浏览器可以理解的 styleSheets ,计算出DOM节点的样式。
- 创建 布局树 ,并计算元素的布局信息。
- 对布局树进行分层,并生成 分层树 。
- 为每个图层生成 绘制列表 ,并提交到合成线层。
- 合成线层将图层分为 图块 ,并在 光栅化线程池 中将图块转化成位图。
- 合成线层发送绘制图块命令 DrawQuad 给浏览器进程。
- 浏览器进程根据DrawQuad消息 生成页面 ,并 显示 到显示器上。
6.相关概念延伸
三个和渲染流水线相关的概念-- ”重排“、”重绘“、”合成“。
6.1重排(更新了元素的集合属性)
更新元素的集合属性
从上图可以看出,如果你通过JavaScript或者CSS修改元素的集合位置属性,例如改变元素的高度、宽度等,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫**重排。**无疑, 重排需要更新完整的渲染过程,所以开销也是最大的。
6.2重绘(更新元素的绘制属性)
通过JavaScript更改某些元素的北京颜色,渲染流水线会怎样调整呢?可以参考下图:
更新元素背景
图中看出,如果修改了元素的背景颜色,那么布局阶段将不会被执行,因为并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程叫做 重绘 。相对于重排, 重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些 。
6.3直接合成阶段
如果改一个既不要布局也不要绘制的属性,渲染引擎将跳过布局和绘制,只执行后续的合成操作,我们把这个过程叫做 合成 ,具体流程如下:
避开重绘和重排
在上图中,我们使用了 CSS 的 transform 来实现动画效果,这可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作。这样的效率是最高的,因为是在非主线程上合成,并没有占用主线程的资源,另外也避开了布局和绘制两个子阶段,所以 相对于重绘和重排,合成能大大提升绘制效率 。
6.4DNS系统
DNS 的核心系统是一个三层的树状、分布式服务,基本对应域名的结构。
- 根域名服务器(Root DNS Server):管理顶级域名服务器,返回“com”“net”“cn”等顶级域名服务器的 IP 地址;
- 顶级域名服务器(Top-level DNS Server):管理各自域名下的权威域名服务器,比如 com 顶级域名服务器可以返回 apple.com 域名服务器的 IP 地址;
- 权威域名服务器(Authoritative DNS Server):管理自己域名下主机的 IP 地址,比如 apple.com 权威域名服务器可以返回 www.apple.com 的 IP 地址;
参考链接: