一文搞懂从输入URL到页面展示全过程

前段时间因为各种事情一直在忙,最近总算是告一段落了,学点东西记录一下。

URL从输入到解析再到展示的全过程是一个非常经典的面试题,也是非常考验一个程序员基本能力的问题,以此为轴,可以串出很多深层次的知识点。

之前我对这个问题的了解非常浅层次,后来参阅了很多资料之后,感觉是将它整理出来的时候了。它可能不是最细的,但我会尽力完善。

“从一个网址从浏览器中输入并跳转起,到页面展示在你的屏幕上,这中间发生了什么?”接下来我们就开始一探究竟。

流程概览

整个解析到显示的过程可以这样简单描述:

  • 浏览器首先使用HTTP协议或HTTPS协议,向服务端请求页面;
  • 服务端响应浏览器请求,返回HTML代码等资源;
  • 浏览器将HTML代码构建为DOM树,计算DOM树上的CSS属性;
  • 根据CSS属性对元素逐个进行渲染,得到存放于内存中的位图,该步骤中的位图可以进行合成以加快绘制速度;
  • 将最终合成的结果绘制在页面上。

概览看起来简单,但其中每个环节展开讲解都会牵扯到很多问题。

如果用一个稍微复杂点的流程图来描述,那就是这样的:

这个过程需要牵扯到三个进程,浏览器进程、网络进程和渲染进程,它们分别的职责是:

  • 浏览器进程:主要负责用户交互、子进程管理和文件储存等功能。
  • 网络进程:面向渲染进程和浏览器进程等提供网络下载功能。
  • 渲染进程:把从网络下载的 HTML、JavaScript、CSS、图片等资源解析为可以显示和交互的页面。

也可以将这个图稍微简化一下:

接下来我将过程简单分为三个阶段:网络阶段构建阶段渲染阶段三个部分进行描述。

网络阶段

网络阶段是指从用户输入起直到获取到HTTP响应报文后的这个阶段,这是整个流程的起点。

首先用户需要输入URL,当然实际上,用户输入的也不一定是URL,浏览器首先需要判断用户的输入,从而确定用户输入的到底是URL还是一些其他内容。现在很多浏览器的地址栏都会集成一个搜索引擎,用户可以直接在地址栏输入搜索关键词,浏览器则将根据搜索引擎的URL规则生成一个URL并准备发起请求。而当用户输入的是一个准确的URL时,浏览器应当对此URL进行完整性检查,确认格式后再准备发起请求。这一步很容易理解,比如我想访问百度主页,我只需要输入baidu.com,浏览器即可自动帮我将前面的协议名称等内容补全,最终获得一个请求URL为https://www.baidu.com/。而假如浏览器判断用户输入的可能是搜索关键词,则自动根据默认搜索引擎构造URL,比如我在地址栏输入shawnzhou并按回车,我最终请求的URL是https://cn.bing.com/search?q=shawnzhou&PC=U316&FORM=CHROMN

此时在浏览器进程上,URL已经构造完毕,可以准备发起请求了。但因为发起请求是网络进程要做的事情,所以这一步还需要通过进程间通信(IPC)将URL传入网络进程,由网络进程负责建立TCP连接并发送HTTP或HTTPS请求。

在上面那个比较复杂的图中,我们现在的位置是:

网络进程拿到URL之后并不会直接去向外发送请求,它会首先寻找本地缓存中是否有这个URL对应的资源(具体策略请看文末扩展知识),如果有,而且没有比如过期等异常情况,那就不需要再耗费网络资源进行加载了,直接从本地把资源调出来准备渲染即可。这里我们就假设没有缓存,必须要进行请求。

请求即试图通过网络与互联网上的其他服务器建立通信。每个服务器都有自己的ip地址作为公网上识别自己的名称,但是想要人去记这些毫无规律杂乱无章的ip地址也太费劲了。所以人们通过比如baidu.com这样的域名,使用某个形象的名字去对应某个ip地址(也就是对应某个服务器)。但现在还有一个问题,人能看明白域名的意思,但机器不一定,所以此时需要有个翻译官来帮忙把各种域名翻译成对应的ip地址,这个翻译官就是DNS服务器。

可是,全世界每天的解析量大到几乎无法估算,只用一台DNS服务器怕不是能给他累到冒烟,而且万一哪天这个服务器挂掉了,那全世界的网络岂不就和瘫痪没什么区别了?所以DNS的需求就决定了它必须是个分布式的。总之,DNS会按照一定的解析顺序对URL进行解析,然后给出目标服务器的IP地址,这个具体的解析策略在文末的扩展知识中。

拿到服务器的IP地址之后,现在就可以与服务器进行TCP连接了。TCP是在客户端和服务端之间传输数据的桥梁,通过请求-响应的形式进行可靠的传输连接。HTTP请求即是建立在TCP连接之上的,要想建立TCP连接,需要经过一个三次握手的过程,那为什么必须要三次握手而不是两次或者四次?

实际上,TCP的三次握手是为了做到保证数据的可靠传输和尽可能提高传输效率而规定的。少于三次,则可能会出现信息丢失从而无法保证可靠传输,多于三次,则可以简化为三次。TCP需要seq序列号来做可靠重传或接收,而避免连接复用时无法分辨出 seq 是延迟的或者是旧的 seq,但seq没有绑定到整个网络的全局时钟上,因此无法判断这个包是不是迟到了,所以需要三次握手来约定确定双方的初始 seq 。假如只有两次握手,客户端首先向服务端发起第一次握手,服务端确认后向客户端发起第二次握手,但此时客户端到底是否确认收到是不确定的,这就造成了服务端向客户端发送的数据不一定可靠,同理,一次握手的话更加不可靠了。而如果是四次甚至更多次握手,则会造成确认重复,浪费网络资源。

OK,走到现在这一步,TCP连接是建立起来了,接下来就是HTTP或HTTPS进行请求的阶段了。这里以HTTP为例,HTTPS多出的步骤放在了扩展知识中。首先浏览器需要构造一个请求,这个请求包含了请求头和请求体,请求头携带了一些配置等信息,请求体可能有也可能没有,里面包含的是请求的具体内容。假如是按照现在常用的RESTful风格的API,它具有很多比如GET,PUT之类的方式,获取的内容一般也不相同,比如需要获得一个html文件或者需要下载一个东西,如何去界定返回的内容到底是什么东西呢?通过在请求头设定Content-type,就可以确定到底是什么类型了。比如需要返回html,则需要text/html,需要下载文件,则需要字节流,则需要application/octet-stream,如果是前后端分离开发需要json格式的数据,则需要application/json。请求头可以写的有很多,根据不同的请求可以携带不同的内容,还可以带cookie。

浏览器发出请求,服务器接收到请求后根据服务端的业务逻辑执行对应代码,然后构造一个响应并返回。响应也包括响应头和响应体,有一个核心内容是响应状态码,这个在我以前的文章里有写,我附在了扩展知识中。浏览器接收到来自服务端的响应后则开始解析,首先会从响应头开始解析,判断状态码是多少,如果需要重定向,则会读取重定向地址并重新发出HTTP请求,如果响应200,说明成功,则会继续处理本次请求。解析完请求头,可以继续的话,就解析请求体。用户发出 URL 请求到页面开始解析的过程,叫做导航。

上面这些操作都是在网络进程中完成的,如果要渲染页面,在网络进程读取到这会是一个网页之后,浏览器渲染进程将开始准备,但渲染进程准备好后还不能立即开始解析,因为此时html代码文档还在网络进程,所以网络进程此时需要通过IPC提交文档,由浏览器进程再确定文档确实被提交。具体流程如下:

  • 首先,浏览器进程接收到网络进程的响应头数据,然后向渲染进程发送提交文档的消息;

  • 渲染进程接收到提交文档的消息后,会和网络进程建立传输数据的管道,即IPC;

  • 管道建立,文档开始传输,传输完成之后,渲染进程会返回确认提交的消息给浏览器进程;

  • 浏览器进程在收到确认提交的消息后,会更新浏览器界面状态,包括安全状态、地址栏的 URL、前进后退的历史状态,并渲染HTML然后更新。

这同时也解释了为什么在浏览器的地址栏里面输入了一个地址后,如果加载速度比较慢,之前的页面不会立马消失,而是需要等待加载一会才能把页面内容更新。

默认情况下,Chrome 会为每个页面分配一个渲染进程,也就是说,每打开一个新页面就会配套创建一个新的渲染进程。

再来标注一下我们现在的位置:

此时这个流程已经几乎快走完了,最后看起来只剩下了解析DOM树和渲染的过程,但其实,这个过程还是很复杂的。

构建阶段

在构建阶段,浏览器已经获得了需要的HTML字节流代码文件,但是不能直接把代码打出来,那样不是网页,可是浏览器也没法自己读代码,这可咋整?有办法,渲染进程在这一步将负责解析获得的字节流,并先构建DOM树。

在说DOM树之前,先说说DOM的作用吧。从页面视角来看,DOM是生成页面的基础数据结构;从JavaScript视角来看 ,DOM提供给JavaScript操作的API,从安全视角来看,DOM是一道安全线,会在解析阶段将不安全的内容剔除。DOM树即是由各种DOM结点组成的一棵树。在渲染引擎内部,有一个叫做HTML Parser的模块,它的职责就是负责将HTML字节流转化为DOM结构。

DOM的具体生成流程示意图:

在HTML解析器中,有一个叫做分词器的东西,它将字节流分为很多Token,规定了最小语义单元。分词器对字节流进行处理,将HTML代码拆分为标签和文本,即找出组成HTML页面的单元,然后生成结点,最后将它们组合起来就生成了DOM。上图所示的HTML代码经过分词器区分之后的的结构如下:

好了,现在能够把标签区分开来了,但是要如何区分结点的父子关系呢?毕竟我们要做的是DOM树,既然是树结构那就免不了要讨论结点之间的父子关系,这里采用的是Token栈的方式进行生成,Token栈可以用入栈和退栈操作完成父子关系的计算。

简单描述一下Token栈的算法过程:循环遍历每个Token直到所有的Token都解析完,当遇到开始标签时,将起标签压入Token栈,在DOM树中绘制一个同名结点。在栈中上下相邻的两个Token在绘制时表示为父子关系,栈中靠顶为子,靠底为父。如果遇到文本结点,则会直接将它绘制到DOM中,但不会压入Token栈。当遇到结束标签时,栈顶弹出,代表此结点的内容已经放入DOM。

到了现在这个阶段,我们已经拿到DOM树结构了,实际上,DOM树结构此时还是一个对象,它还不能直接展示在页面上,此时还缺少CSS的加持,有了样式之后,就可以进行渲染了。要加入样式,则需要计算一个叫做CSSOM的东西,它判断每个DOM结点都有哪些CSS参与了修饰。对于CSSOM的介绍,我放在了扩展知识里。

拿到的CSS样式表首先需要过一次标准化,即将属性值统一,比如rem转化为px,color写颜色名的转化为RGB值,同时还需要考虑CSS的继承,层叠等概念,比较复杂。但这时候的CSS代码还是不能被浏览器识别的,需要一步转化,将CSS样式转化成document.styleSheets,这里我们可以先暂时把这些算好的属性叫做ComputedStyle,即计算样式。现在有了DOM树,有了CSSOM,接下来就是将它们结合起来,生成布局树的步骤了。

创建布局树时,需要将DOM树和计算样式结合起来,流程如下:

  • 对DOM树进行剔除操作,也就是剔除掉比如head这种不可见的结点,它们不需要被渲染。
  • 查看ComputedStyle中的结点,判断是否带有如display: none这样的令其不可见属性,这样的不可见结点也不会放在布局树中。
  • 最终将留下来的结点的DOM和CSSOM结合起来,生成布局树。

题外话:要是因为JS或者其他原因,导致布局需要随时变动,如何优化性能?这里有重绘和重排两个概念,我在扩展知识中说明了这一部分。

总之现在是有了布局树,下面该准备将它渲染了。

渲染阶段

渲染是什么?将页面布局显示在屏幕上就是渲染。说到屏幕就不得不先提一些它的相关知识。

每个显示器都有其固定的刷新频率,比如60Hz的普通屏幕,144Hz,240Hz的高刷新率屏幕。那这个单位Hz到底代表什么?它是频率的单位,代表每秒钟的周期性变动的重复次数,也就是说,一块60Hz的屏幕每秒钟刷新60次,再通俗点说,刷新频率就是屏幕每秒画面被刷新的次数,表示每秒钟显示屏上的“图片”被重新绘制了多少次。

我们知道,显卡是负责图形计算和图形输出的硬件,显卡中有个部分叫做前缓冲区,它负责将计算好的画面输出到屏幕,如果刷新率为60Hz,则每秒钟内,显示器从前缓冲区读取60张合成的图像,然后输出到屏幕上。显卡合成的图像会临时保存在后缓冲区,当前缓冲区的图像已经被输送走的时候,后缓冲区的图像将被立即送入前缓冲区,等待下一次显示,而后面输出的图像又会放在后缓冲区待命,这样循环往复,便实现了屏幕图像的刷新。

还有一个概念叫做帧和帧率,它一般出现于使用动画时,比如滑动,缩放等,一般的网页刷新帧率是60帧/秒,也就是渲染引擎每秒要输送60张计算好的图片发送到显卡的后缓冲区,有时候会感觉到页面卡到掉帧,也就是说帧率出现了下降,即渲染引擎的计算量增大,每秒钟没办法输送正好60张图片,这就会导致帧率下降。比如60Hz的屏幕,代表1000ms内刷新60次,所以每次计算的时间限制大约只有16.67ms,如果某个组件在16.67ms内无法计算完成,则帧率就会下降,计算时间越长下降越多。当帧率下降严重时,你就会感觉到卡。这启示我们,生成布局树时应当让DOM尽量少,尽量少触发重排和重绘,这样就会提升显示性能。

这样,一次从URL输入到加载的全过程就算说完啦~呼,好久没有写过长文了,偶尔记录记录还是很有成就感的。

扩展知识

网络与通信相关

  • HTTP标准

https://tools.ietf.org/html/rfc2616

https://tools.ietf.org/html/rfc7234

https://tools.ietf.org/html/rfc2818

https://tools.ietf.org/html/rfc7540

  • HTTP请求类型

https://www.runoob.com/http/http-methods.html

  • HTTP请求状态码

https://shawnzhou.world/2021/04/07/HTTP-status-code/

  • OSI七层模型

  • IPC示意图:

image-20210530162723558

浏览器缓存策略

详情参阅https://www.jiqizhixin.com/articles/2020-07-24-12

缓存是一种请求资源的副本,可以存储各种URL请求要用到的东西,比如HTML,CSS,JS代码,以及图片,视频,数据等资源。缓存会根据进来的请求保存输出内容的副本;当下一个请求来到的时候,如果是相同的 URL,缓存会根据缓存机制决定是直接使用副本响应访问请求,还是向源服务器再次发送请求。

本地和Web服务器都可以拥有缓存。使用缓存可以减少网络延迟,加快页面加载速度,减少带宽消耗和服务器压力。缓存的规则有新鲜度(过期机制判断是否陈旧)和校验值(验证机制判断是否修改),缓存分为强缓存(本地缓存)和协商缓存(弱缓存)。

在网络进程根据URL判断缓存是否存在时,会首先查看强缓存,如果命中则直接获取。如果未命中,则进入协商缓存阶段,此时将向浏览器发出请求判断是否命中,如果协商缓存命中,服务器会返回304,不带任何响应实体,此时可以从浏览器(客户端)中使用缓存。假如协商缓存也没有命中,则直接从服务器加载全部资源。

304 Not Modified 表示资源已找到,但是并不符合条件中的要求,此时服务器端资源并未改变,可以直接使用客户端未过期的缓存。

DNS解析策略

感谢工作室一位对计算机网络了如指掌的学长给我的耐心讲解Orz

就目前来说,一次DNS解析的顺序是如下面这样:

  • 首先寻找本地DNS缓存(包括浏览器缓存,系统缓存,路由器缓存),假如缓存不过期,合法,且命中,则直接从缓存拿出解析结果,这是最快的。
  • 如果缓存里没有,则会去寻找操作系统设定的hosts文件,如果在hosts文件里面有解析结果,则直接拿走结果,这个速度也很快。
  • 如果hosts里面也没有,那就去看看本地DNS服务器里有没有记录。本地DNS服务器一般都是由DHCP自动分配,当然也有特殊情况下需要手动分配的,比如需要在某个内网环境中过滤掉一些域名,那就需要搭建本地DNS服务器并自定义解析策略。
  • 如果上面这些都没有,那只能说明本地就彻底没有这个解析了。此时解析会向ISP-DNS缓存(互联网运营商提供)中查看有没有这个记录,有则返回这个记录,到这一步的解析其实可能会有点需要时间了,但这里能解析的域名也就是国内的绝大多数域名了,国内网站几乎没有到这一步还找不到记录的。
  • 假如你要访问的确实就是国内找不到的,那就还会继续向外延伸。解析会根据根域名服务器→顶级域名服务器→主域名服务器的顺序依次进行,直到找到记录后返回。
  • 到上一步还是找不到?你是不是域名写错了…

TCP的三次握手和四次挥手

详情参阅https://www.cnblogs.com/davina123/p/12978114.html

TCP的三次握手如下:

  • 第一次握手:建立连接时,客户端发送syn包(syn=x)到服务器,并进入SYN_SENT(Synchronize Sequence Numbers)状态,等待服务器确认;
  • 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(syn=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;
  • 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。

借用一张网上的图解释一下三次握手,上面的漫画形象来自《图解HTTP》,这本书很不错,推荐买来一读。

完成三次握手之后,TCP连接成功建立。

TCP的四次挥手如下:

  • 第一次挥手: 首先从客户端开始发出连接释放报文,并且停止发送数据,这客户端进入到终止等待1状态。

  • 第二次挥手:从服务器到客户端 。服务器收到连接释放报文,发出确认报文且带上自己的序列号,这时服务器端进入了关闭等待状态。这时TCP服务器要通知高层的应用进程,客户端向服务器释放,这时服务器处于半关闭状态。如果服务器还有数据要发送,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据

  • 第三次挥手:服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,由于在半关闭状态,服务器很可能又发送了一些数据,服务器就进入了最后确认状态,等待客户端的确认。

  • 第四次挥手:客户端收到服务器释放报文后,发出确认此时,客户端就进入了时间等待状态。因为此时TCP连接还没有释放,必须经过一段时间2∗MSL(最长报文段寿命),因为网络是不可靠的,有可以最后一个ACK丢失。所以时间状态就是用来重发可能丢失的ACK报文的。当客户端撤销相应的TCB后,才进入CLOSED状态。

服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。服务器结束TCP连接的时间要比客户端早一些。所以我们可以看到当服务器端收到客户端的SYN连接请求报文后,可以直接的发送请求连接和确认连接,但在关闭的时候,服务器收么到关闭信息后,很可能不会立即关闭,它需要时间来反应,只能先回复一个确认,只有当服务器端所有的报文都发送完后,服务器端才能发送希望断开连接。所以需要挥手需要四步来完成。

img

浏览器如何处理HTTPS协议

详情参阅https://segmentfault.com/a/1190000012196642https://www.jianshu.com/p/ecbae815baf2

HTTPS是一种安全的,加密的协议,由于HTTP协议使用明文,有被窃听,伪装,篡改的可能性,它并不适合安全传输,使用HTTPS则可以解决这个问题。

HTTP加上加密处理、认证机制、以及完整性保护后的就是HTTPS。

需要知道的是,HTTPS并非是应用层的一种新的协议。只是HTTP通信接口部分用SSL或TLS协议代替而已。也就是说,所谓的HTTPS,其实就是身披SSL协议外壳的HTTP。HTTPS中使用了SSL和TLS这两个协议,TLS是以SSL为原型开发的协议,有时候会统称该协议为SSL。HTTPS在加密过程中使用了非对称加密技术和对称加密技术,公钥加密后的密文只能通过对应的私钥来解密,而私钥加密的密文却可以通过对应的公钥来解密,使用证书保证公钥的正确性,非对称加密算法加密的是证书的数字签名。

img

CSSOM

详情参阅https://zhuanlan.zhihu.com/p/23569241https://www.cnblogs.com/mcad/p/10753212.html

CSSOM 是 CSS 的对象模型,在 W3C 标准中,它包含两个部分:

  • 描述样式表和规则等 CSS 的模型部分(CSSOM)
  • 跟元素视图相关的 View 部分(CSSOM View)

最后渲染出CSSOM树大概是这个样子:

img

重绘和重排

详情参阅https://www.cnblogs.com/cencenyue/p/7646718.htmlcnblogs.com/yadongliang/p/10677589.html

重绘是指一个元素外观的改变所触发的浏览器行为,浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。触发重绘的条件是改变元素的外观属性,比如字体颜色和背景颜色。重排又称回流,当渲染树中的一部分或全部,因为元素的规模尺寸,布局,隐藏等改变而需要重新构建时,就称为回流。每个页面至少需要一次回流,就是在页面第一次加载的时候。

重绘和重排的关系:在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘。所以,重排必定会引发重绘,但重绘不一定会引发重排。

重绘和重排会导致性能下降,浏览器本身带有一个优化策略:浏览器会维护一个队列,把所有会引起重排,重绘的操作放入这个队列,等队列中的操作到一定数量或者到了一定时间间隔,浏览器就会flush队列,进行一批处理,这样多次重排,重绘变成一次重排重绘。

减少重绘和重排的操作有以下方法:

  • 不要一条一条地修改 DOM 的样式。可以先定义好 css 的 class,然后修改 DOM 的 className。
  • 不要把 DOM 结点的属性值放在一个循环里当成循环里的变量。
  • 为动画的 HTML 元件使用 fixed 或 absoult 的 position,那么修改他们的 CSS 是不会 reflow 的。
  • 不要使用 table 布局。因为可能很小的一个小改动会造成整个 table 的重新布局。(table及其内部元素除外,它可能需要多次计算才能确定好其在渲染树中节点的属性,通常要花3倍于同等元素的时间。这也是为什么要避免使用table做布局的一个原因)
  • 不要在布局信息改变的时候做查询(会导致渲染队列强制刷新)
  • 不要经常访问浏览器的flush队列属性;如果一定要访问,可以利用缓存。将访问的值存储起来,接下来使用就不会再引发回流
  • 先设置元素为display:none然后进行页面布局等操作;设置完成后将元素设置为display:block这样的话就只引发两次重绘和重排
  • 将需要多次重排的元素,position属性设为absolute或fixed,元素脱离了文档流,它的变化不会影响到其他元素
打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2018-2021 Shawn Zhou
  • Hexo 框架强力驱动 | 主题 - Ayer
  • 访问人数: | 浏览次数:

感谢打赏~

支付宝
微信