代码视界

Hanpeng Chen的个人博客

「高频面试题」浏览器从输入url到页面展示中间发生了什么

本文于 1107 天之前发表,文中内容可能已经过时。

“在浏览器中,从输入URL到页面展示,中间发生了什么?”这是一道经典的高频面试题,其中涉及了网络、操作系统、web等一系列的知识。接下来我们一起来看看要怎么回答这道题。

整个过程可以大致描述为:

URL解析

用户输入URL,浏览器会根据用户输入的信息判断是搜索内容还是请求的URL。如果是搜索内容,就将搜索内容+默认搜索引擎来合成新的带搜索关键字的URL。如果判断输入内容符合URL规则,地址栏会根据规则,把内容加上协议,合成为完整的URL。

地址解析

协议 默认端口号 传输协议
http 80 超文本传输协议
https 443 在http的基础上进行了安全设置(SSL/TSL)证书认证
ftp 21 主要用于客户端电脑和服务器端的文件传输

URL的编码

因为网络标准规定了URL只能是字母和数字,还有一些特殊符号(-_.~ ! * ‘ ( ) ; : @ & = + $ , / ? # [ ],

对于一些特殊字符,在客户端和服务端传递的时候,需要进行编码和解码。

  • encodeURI、decodeURI:对中文、空格等编码解码,适合给URL本身编码

  • encodeURIComponent、decodeURIComponent:对中文、空格 : /等编码解码,编码范围更广,适合给参数编码。

  • escape unescape 主要用于客户端不同页面之间的数据传输的时候,信息的编码解码(如:cookie)

检查缓存

接下来就进入页面资源请求过程。

这时,浏览器进程会通过进程间通信把URL请求发送至网络进程,网络进程接收到URL请求后,会在这里发起真正的URL请求流程。

首先,网络进程会先查找本地缓存是否缓存了该资源。如果有缓存资源且缓存在有效期内,直接返回资源给浏览器进程;如果在缓存中没有找到该资源,那么直接进入网络请求流程。

这一步中的缓存是指强缓存,具体内容可以参考:一文彻底掌握HTTP缓存

发起网络请求的第一步就是要进行DNS解析。

DNS解析

DNS解析:就是根据浏览器识别出来的URL地址中的域名,到DNS服务器上,查找服务器外网IP的过程。

DNS解析也是有缓存的:浏览器解析过一次,一般就会在本地记录一下解析记录。所以每一次DNS解析:本地DNS服务器解析(递归)、根/顶级/权威域名服务器解析(迭代)

DNS解析过程

DNS的域名查找:客户端和浏览器,本地DNS之间的查询方式是递归查询;本地DNS服务器和根域及子域之间的查询方式是迭代查询。

在客户端输入URL后,会有一个递归查找的过程,从浏览器缓存中查找 -> 本地的hosts文件查找 -> 本地DNS解析器缓存查找 -> 本地DNS服务器查找,在这个过程中任何一步找到了都会结束查找流程。

递归查找过程如下图所示:

如果在本地DNS服务器中也没有查询到,则根据本地DNS服务器设置的转发器进行查询。若未用转发模式,则迭代查找过程如下图:

优化

  • 减少DNS请求次数:页面中尽可能少用过多域名。HTTP有并发性,但它的并发性是受到源的限制的,同一个源一次最多并发4-7个。
  • DNS预获取(DNS Prefetch):在不能减少DNS解析记录的情况下,可以把DNS预解析。处理方法如下:
    1
    <link rel="dns-prefetch" href="//g.alicdn.com"/>

建立TCP连接,三次握手

DNS解析获取到服务器的IP+端口后,接下来就是要建立TCP链接,这里就涉及到我们常说的“三次握手”。

首先,判断是不是https,如果是,则HTTPS其实是HTTP + SSL/TLS 两部分组成,也就是在HTTP基础上又加了一层处理加密信息的模块。服务端和客户端的信息传输都会通过TLS进行加密,所以传输的数据都是加密后的数据。

进行三次握手,建立TCP连接:

  • 第一次握手:建立连接。客户端发送连接请求报文段,将SYN位置为1,Sequence Number为x;然后,客户端进入SYN_SEND状态,等待服务器的确认;
  • 第二次握手:服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认,设置Acknowledgment Number为 x+1;同时,自己还要发送SYN请求信息,将SYN位置为1,Sequence Number为y;服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN_RECV状态;
  • 第三次握手:客户端收到服务器的SYN+ACK报文段。然后将Acknowledgment Number设置为y+1,向服务器发送ACK报文段,这个报文段发送完毕以后,客户端和服务器端都进入ESTAB_LISHED状态,完成TCP三次握手。

  • seq序号:用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记
  • ack确认序号:只有ACK标志位为1,确认序号字段才有效,ack=seq+1

标识位:

  • ACK:确认标识,用于表示对数据包的成功接收
  • SYN:同步标识,表示TCP连接已初始化,发起一个新连接
  • FIN:完成标识,释放一个连接,用于拆除上一个SYN标识。一个完整的TCP连接过程一定有SYN和FIN包。

数据传输

TCP 连接建立之后,浏览器端会构建请求行、 请求头等信息,并把和该域名相关的 Cookie 等数据附加到请求头中,然后向服务器发送构建的请求信息。如果是 HTTPS,还需要进行 TSL 协商。

服务器接收到请求,就解析请求头,如果头部有缓存相关信息如if-none-match与if-modified-since,则验证缓存是否有效,若有效则返回状态码为304;若无效则重新返回资源,状态码为200。

这里会涉及HTTP的协商缓存,具体内容可以参考:一文彻底掌握HTTP缓存

关闭TCP连接,四次挥手

  • 第一次挥手:主机1(可以是客户端,也可以是服务器端),设置Sequence Number和Acknowledgment Number,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态,表示主机1没有数据要发送给主机2了;

  • 第二次挥手:主机2接收到主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment Number为Sequence Number + 1;主机1进入 FIN_WAIT_2状态;主机2告诉主机1,我同意你的关闭请求;

  • 第三次挥手:主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入LAST_ACK状态;

  • 第四次挥手:主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段后,就关闭连接;此时,主机1等待主机2 MSL后依然没有收到回复,则证明主机2已正常关闭,那好,主机1也可以关闭连接了。

浏览器渲染

浏览器拿到资源会根据资源类型进行处理,比如是 gzip 压缩后的文件则进行解压缩,如果响应头 Content-type 是 text/html,则开始解析HTML。

按照渲染的时间顺序,流水线可以分为下面几个子阶段:构建DOM树、样式计算、布局阶段、分层、栅格化和显示。

1、渲染进程将HTML内容转换为浏览器能够读懂的DOM树结构。

2、渲染引擎将CSS样式表转化为浏览器可以理解的styleSheets,计算出DOM节点的样式。

3、创建布局树,并计算元素的布局信息。

4、对布局树进行分层,并生成分层树。

5、为每个图层生成绘制列表,并将其提交到合成线程。

6、合成线程将图层分图块,并栅格化将图块转换成位图。

7、合成线程发送绘制图块命令给浏览器进程。浏览器进程根据指令生成页面,并显示到显示器上。

构建DOM树

浏览器从网络或硬盘中获得HTML字节数据后,会先将HTML的原始字节数据转换为文件指定编码的字符,然后浏览器会根据HTML规范来将字符串转换为各种令牌标签,如html、body等,最终解析成一个树状的对象模型,就是DOM树。

具体步骤如下:

  • 转码:读取接收到的HTML二进制数据,按指定编码格式将字节转换为HTML字符串
  • Tokens化:解析HTML,将HTML字符串转换为结构清晰的Tokens,每个Token都有特殊的含义,同时又自己的一套规则
  • 构建Nodes:每一个Node都添加特定的属性或属性访问器,通过指针能够确定Node的父、子、兄弟关系和所属treeScope
  • 构建DOM树:最重要的工作就是建立起每个节点的父子兄弟关系。

解析过程中遇到图片、link、script会启动下载。

script标签会阻塞 DOM 树的构建,所以一般将 script 放在底部,或者添加 async 、defer 标识。

css 下载时异步,不会阻塞浏览器构建 DOM 树,但是会阻塞渲染,布局阶段会等待 css 下载解析完毕后才进行。

样式计算

渲染引擎将CSS样式表转化为浏览器可以理解的styleSheets,计算出DOM节点的样式。

样式计算过程主要有两步:

  • 属性值标准化:先将所有值转换为渲染引擎容易理解的、标准化的计算值。在CSS文本中有很多属性值,如em、blue、bold等等,这些类型值不容易被渲染引擎理解,所以需要进行转换。

  • 处理样式的继承和层叠,计算出DOM树中每个节点的具体样式。

CSS样式来源主要有3种:

  • 通过link引用的外部css文件
  • style标签内的css
  • 元素的style属性内嵌的css

布局阶段

布局过程,即排除script、meta等功能化、非视觉节点,排除 display:none 的节点,计算元素的位置信息,确定元素的位置,构建一棵只包含可见元素布局树。

分层

页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。

并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。

图层绘制

渲染引擎实现图层的绘制:把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表。

绘制列表中的指令非常简单,就是让其执行一个简单的绘制操作,比如绘制粉色矩形或者黑色的线等。

栅格化

合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。

通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做视口(viewport)。在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。

显示

最后,合成线程发送绘制图块命令给浏览器进程。浏览器进程根据指令生成页面,并显示到显示器上,渲染过程完成。

页面渲染优化:

  • HTML文档结构层次尽量少,最好不深于六层;
  • 脚本尽量后放,放在前即可;
  • 少量首屏样式内联放在标签内
  • 样式结构层次尽量简单
  • 在脚本中尽量减少DOM操作,尽量缓存访问DOM的样式信息,避免过度触发回流;
  • 减少通过JavaScript代码修改元素样式,尽量使用修改class名方式操作样式或动画;
  • 动画尽量使用在绝对定位或固定定位的元素上;
  • 隐藏在屏幕外,或在页面滚动时,尽量停止动画;
  • 尽量缓存DOM查找,查找器尽量简洁;
  • 涉及多域名的网站,可以开启域名预解析。

如果你觉得这篇内容对你有帮助的话:

1、点赞支持下吧,让更多的人也能看到这篇内容

2、关注公众号:前端极客技术,我们一起学习一起进步。

欢迎关注微信公众号: 『前端极客技术』『代码视界』
支付宝打赏 微信打赏

赞赏是不耍流氓的鼓励