乐趣区

关于前端:穷追猛打阿里二面问了我30分钟从URL输入到渲染

当面试官问出这个题后,大部分人听到都是心田窃喜:早就背下这篇八股文。

然而稍等,上面几个问题你能答出来吗:

  1. 浏览器对 URL 为什么要解析?URL 参数用的是什么字符编码?那 encodeURI 和 encodeURIComponent 有什么区别?
  2. 浏览器缓存的 disk cache 和 memory cache 是什么?
  3. 预加载 prefetch、preload 有什么差异?
  4. JS 脚本的 async 和 defer 有什么区别?
  5. TCP 握手为什么要三次,挥手为什么要四次?
  6. HTTPS 的握手有理解过吗?

同样的问题,能够拿来招聘 P5 也能够是 P7,只是深度不同。所以我重新整理了一遍整个流程,本文较长,倡议先珍藏。

概述

在进入正题之前,先简略理解一下浏览器的架构作为前置常识。浏览器是多过程的工作的,“从 URL 输出到渲染”会次要波及到的,是浏览器过程、网络过程和渲染过程这三个:

  1. 浏览器过程负责解决、响应用户交互,比方点击、滚动;
  2. 网络过程负责解决数据的申请,提供下载性能;
  3. 渲染过程负责将获取到的 HTML、CSS、JS 解决成能够看见、能够交互的页面;

“从 URL 输出到页面渲染”整个过程能够分成网络申请和浏览器渲染两个局部,别离由网络过程和渲染过程去解决。

网络申请

网络申请局部进行了这几项工作:

  1. URL 的解析
  2. 查看资源缓存
  3. DNS 解析
  4. 建设 TCP 连贯
  5. TLS 协商密钥
  6. 发送申请 & 接管响应
  7. 敞开 TCP 连贯

接下来会一一开展。

URL 解析

浏览器首先会判断输出的内容是一个 URL 还是搜寻关键字。

如果是 URL,会把不残缺的 URL 合成残缺的 URL。一个残缺的 URL 应该是:协定 + 主机 + 端口 + 门路[+ 参数][+ 锚点]。比方咱们在地址栏输出www.baidu.com,浏览器最终会将其拼接成https://www.baidu.com/, 默认应用 443 端口。

如果是搜寻关键字,会将其拼接到默认搜索引擎的参数局部去搜寻。这个流程须要对输出的不平安字符编码进行本义(平安字符指的是数字、英文和多数符号)。因为 URL 的参数是不能有中文的,也不能有一些特殊字符,比方 = ? &,否则当我搜寻1+1=2,如果不加以本义,url 会是/search?q=1+1=2&source=chrome,和 URL 自身的分隔符= 产生了歧义。

URL 对非平安字符本义时,应用的编码叫百分号编码,因为它应用百分号加上两位的 16 进制数示意。这两位 16 进制数来自 UTF- 8 编码,将每一个中文转换成 3 个字节,比方我在 google 地址栏输出“中文”,url 会变成/search?q=%E4%B8%AD%E6%96%87,一共 6 个字节。

咱们在写代码时常常会用的 encodeURIencodeURIComponent正是起这个作用的,它们的规定根本一样,只是 = ? & ; / 这类 URI 组成符号,这些在 encodeURI 中不会被编码,但在 encodeURIComponent 中通通会。因为 encodeURI 是编码整个 URL,而 encodeURIComponent 编码的是参数局部,须要更加严格把关。

查看缓存

查看缓存肯定是在发动真正的申请之前进行的,只有这样缓存的机制才会失效。如果发现有对应的缓存资源,则去查看缓存的有效期。

  1. 在有效期内的缓存资源间接应用,称之为强缓存,从 chrome 网络面板看到这类申请间接返回 200,size 是 memory cache 或者 disk cachememory cache 是指从资源从内存中被取出,disk cache是指从磁盘中被取出;从内存中读取比从磁盘中快很多,但资源能不能调配到内存要取决于当下的零碎状态。通常来说,刷新页面会应用内存缓存,敞开后从新关上会应用磁盘缓存。
  2. 超过有效期的,则携带缓存的资源标识向服务端发动申请,校验是否能持续应用,如果服务端通知咱们,能够持续应用本地存储,则返回 304,并且不携带数据;如果服务端通知咱们须要用更新的资源,则返回 200,并且携带更新后的资源和资源标识缓存到本地,不便下一次应用。

DNS 解析

如果没有胜利应用本地缓存,则须要发动网络申请了。首先要做的是 DNS 解析。

会顺次搜寻:

  1. 浏览器的 DNS 缓存;
  2. 操作系统的 DNS 缓存;
  3. 路由器的 DNS 缓存;
  4. 向服务商的 DNS 服务器查问;
  5. 向寰球 13 台根域名服务器查问;

为了节省时间,能够在 HTML 头部去做 DNS 的预解析:

<link rel="dns-prefetch" href="http://www.baidu.com" />

为了保障响应的及时,DNS 解析应用的是 UDP 协定

建设 TCP 连贯

咱们发送的申请是基于 TCP 协定的,所以要先进行连贯建设。建设连贯的通信是打电话,单方都在线;无连贯的通信是发短信,发送方不论接管方,本人说本人的。

这个确认接管方在线的过程就是通过 TCP 的三次握手实现的。

  1. 客户端发送建设连贯申请;
  2. 服务端发送建设连贯确认,此时服务端为该 TCP 连贯分配资源;
  3. 客户端发送建设连贯确认的确认,此时客户端为该 TCP 连贯分配资源;

为什么要三次握手才算建设连贯实现?

能够先假如建设连贯只有两次会产生什么。把下面的状态图稍加批改,看起来一切正常。


但如果这时服务端收到一个生效的建设连贯申请,咱们会发现服务端的资源被节约了——此时客户端并没有想给它传送数据,但它却筹备好了内存等资源始终期待着。

所以说,三次握手是为了保障客户端存活, 避免服务端在收到生效的超时申请造成资源节约。

协商加密密钥——TLS 握手

为了保障通信的平安,咱们应用的是 HTTPS 协定,其中的 S 指的就是 TLS。TLS 应用的是一种非对称 + 对称的形式进行加密。

对称加密就是两边领有雷同的秘钥,两边都晓得如何将密文加密解密。这种加密形式速度很快,然而问题在于如何让单方晓得秘钥。因为
传输数据都是走的网络,如果将秘钥通过网络的形式传递的话,秘钥被截获,就失去了加密的意义。

非对称加密,每个人都有一把公钥和私钥,公钥所有人都能够晓得,私钥只有本人晓得,将数据用公钥加密,解密必须应用私钥。这种加密形式就能够完满解决对称加密存在的问题,毛病是速度很慢。

咱们采取非对称加密的形式协商出一个对称密钥,这个密钥只有发送方和接管方晓得的密钥,流程如下:

  1. 客户端发送一个随机值以及须要的协定和加密形式;
  2. 服务端收到客户端的随机值,发送本人的数字证书,附加上本人产生一个随机值,并依据客户端需要的协定和加密形式应用对应的形式;
  3. 客户端收到服务端的证书并验证是否无效,验证通过会再生成一个随机值,通过服务端证书的公钥去加密这个随机值并发送给服务端;
  4. 服务端收到加密过的随机值并应用私钥解密取得第三个随机值,这时候两端都领有了三个随机值,能够通过这三个随机值依照之前约定的加密形式生成密钥,接下来的通信就能够通过该对称密钥来加密解密;

通过以上步骤可知,在 TLS 握手阶段,两端应用非对称加密的形式来通信,然而因为非对称加密损耗的性能比对称加密大,所以在正式传输数据时,两端应用对称加密的形式。

发送申请 & 接管响应

HTTP 的默认端口是 80,HTTPS 的默认端口是 443。

申请的根本组成是 申请行 + 申请头 + 申请体

POST /hello HTTP/1.1
User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
Host: www.example.com
Accept-Language: en, mi

name=niannian

响应的根本组成是 响应行 + 响应头 + 响应体

HTTP/1.1 200 OK
Content-Type:application/json
Server:apache

{password:'123'}

敞开 TCP 连贯

等数据传输结束,就要敞开 TCP 连贯了。敞开连贯的被动方能够是客户端,也能够是服务端,这里以客户端为例,整个过程有四次握手:

  1. 客户端申请开释连贯,仅示意客户端不再发送数据了;
  2. 服务端确认连贯开释,但这时可能还有数据须要解决和发送;
  3. 服务端申请开释连贯,服务端这时不再须要发送数据时;
  4. 客户端确认连贯开释;

为什么要有四次挥手

TCP 是能够双向传输数据的,每个方向都须要一个申请和一个确认。因为在第二次握手完结后,服务端还有数据传输,所以没有方法把第二次确认和第三次合并。

被动方为什么会期待 2MSL

客户端在发送完第四次的确认报文段后会期待 2MSL 才正真敞开连贯,MSL 是指数据包在网络中最大的生存工夫。目标是确保服务端收到了这个确认报文段,

假如服务端没有收到第四次握手的报文,试想一下会产生什么?在客户端发送第四次握手的数据包后,服务端首先会期待,在 1 个 MSL 后,它发现超过了网络中数据包的最大生存工夫,然而本人还没有收到数据包,于是服务端认为这个数据包曾经失落了,它决定把第三次握手的数据包从新给客户端发送一次,这个数据包最多破费一个 MSL 会达到客户端。

一来一去,一共是 2MSL,所以客户端在发送完第四次握手数据包后,期待 2MSL 是一种兜底机制,如果在 2MSL 内没有收到其余报文段,客户端则认为服务端曾经胜利承受到第四次挥手,连贯正式敞开。

浏览器渲染

下面讲完了网络申请局部,当初浏览器拿到了数据,剩下须要渲染过程工作了。浏览器渲染次要实现了一下几个工作:

  1. 构建 DOM 树;
  2. 款式计算;
  3. 布局定位;
  4. 图层分层;
  5. 图层绘制;
  6. 显示;

构建 DOM 树

HTML 文件的构造没法被浏览器了解,所以先要把 HTML 中的标签变成一个能够给 JS 应用的构造。

在控制台能够尝试打印 document,这就是解析进去的 DOM 树。

款式计算

CSS 文件一样没法被浏览器间接了解,所以首先把 CSS 解析成样式表。
这三类款式都会被解析:

  • 通过 link 援用的内部 CSS 文件
  • <style>标签内的款式
  • 元素的 style 属性内嵌的 CSS

在控制台打印document.styleSheets,这就是解析出的样式表。

利用这份样式表,咱们能够计算出 DOM 树中每个节点的款式。之所以叫计算,是因为每个元素要继承其父元素的属性。

<style>
    span {color: red}
    div {font-size: 30px}
</style>
<div>
    <span> 年年 </span>
</div>

比方下面的 年年,不仅要承受 span 设定的款式,还要继承 div 设置的。

DOM 树中的节点有了款式,当初被叫做渲染树。

为什么要把 CSS 放在头部,js 放在 body 的尾部

在解析 HTML 的过程中,遇到须要加载的资源特点如下:

  • CSS 资源异步下载,下载和解析都不会阻塞构建 dom 树<link href='./style.css' rel='stylesheet'/>
  • JS 资源同步下载,下载和执行都会阻塞构建 dom 树<script src='./index.js'/>

因为这样的个性,往往举荐将 CSS 样式表放在 head 头部,js 文件放在 body 尾部,使得渲染能尽早开始。

CSS 会阻塞 HTML 解析吗

上文提到页面渲染是渲染过程的工作,这个渲染过程中又细分为 GUI 渲染线程和 JS 线程。

解析 HTML 生成 DOM 树,解析 CSS 生成样式表以及前面去生成布局树、图层树都是由 GUI 渲染线程去实现的,这个线程能够一边解析 HTML,一边解析 CSS,这两个是不会抵触的,所以也提倡把 CSS 在头部引入。

然而在 JS 线程执行时,GUI 渲染线程没有方法去解析 HTML,这是因为 JS 能够操作 DOM,如果两者同时进行可能引起抵触。如果这时 JS 去批改了款式,那此时 CSS 的解析和 JS 的执行也没法同时进行了,会先等 CSS 解析实现,再去执行 JS,最初再去解析 HTML。

从这个角度来看,CSS 有可能阻塞 HTML 的解析。

预加载扫描器是什么

下面提到的外链资源,不论是同步加载 JS 还是异步加载 CSS、图片等,都要到 HTML 解析到这个标签能力开始,这仿佛不是一种很好的形式。实际上,从 2008 年开始,浏览器开始逐渐实现了预加载扫描器:在拿到 HTML 文档的时候,先扫描整个文档,把 CSS、JS、图片和 web 字体等提前下载。

js 脚本引入时 async 和 defer 有什么差异

预加载扫描器解决了 JS 同步加载阻塞 HTML 解析的问题,然而咱们还没有解决 JS 执行阻塞 HTML 解析的问题。所有有了 async 和 defer 属性。

  • 没有 defer 或 async,浏览器会立刻加载并执行指定的脚本
  • async 属性示意异步执行引入的 JavaScript,经加载好,就会开始执行
  • defer 属性示意提早到 DOM 解析实现,再执行引入的 JS

在加载多个 JS 脚本的时候,async 是无程序的执行,而 defer 是有程序的执行

preload、prefetch 有什么区别

之前提到过预加载扫描器,它能提前加载页面须要的资源,但这一性能只对特定写法的外链失效,并且咱们没有方法依照本人的想法给重要的资源一个更高的优先级,所以有了 preload 和 prefetch。

  1. preload:以高优先级为以后页面加载资源;
  2. prefetch:以低优先级为前面的页面加载将来须要的资源,只会在闲暇时才去加载;

无论是 preload 还是 prefetch,都只会加载,不会执行,如果预加载的资源被服务器设置了能够缓存 cache-control 那么会进入磁盘,反之只会被保留在内存中。

具体应用如下:

<head>
    <!-- 文件加载 -->
    <link rel="preload" href="main.js" as="script">
    <link rel="prefetch" href="news.js" as="script">
</head>

<body>
    <h1>hello world!</h1>
    <!-- 文件文件执行 -->
    <script src="main.js" defer></script>
</body>

为了保障资源正确被预加载,应用时须要留神:

  1. preload 的资源应该在以后页面立刻应用,如果不加上 script 标签执行预加载的资源,控制台中会显示正告,提醒预加载的资源在以后页面没有被援用;
  2. prefetch 的目标是取将来会应用的资源,所以当用户从 A 页面跳转到 B 页面时,进行中的 preload 的资源会被中断,而 prefetch 不会;
  3. 应用 preload 时,应配合 as 属性,示意该资源的优先级,应用 as="style" 属性将取得最高的优先级,as ="script"将取得低优先级或中优先级,其余能够取的值有font/image/audio/video
  4. preload 字体时要加上 crossorigin 属性,即便没有跨域,否则会反复加载:

    <link rel="preload href="font.woff"as="font" crossorigin>

此外,这两种预加载资源不仅能够通过 HTML 标签设置,还能够通过 js 设置

var res = document.createElement("link"); 
res.rel = "preload"; 
res.as = "style"; 
res.href = "css/mystyles.css"; 
document.head.appendChild(res); 

以及 HTTP 响应头:

Link: </uploads/images/pic.png>; rel=prefetch

布局定位

下面具体的讲述了 HTML 和 CSS 加载、解析过程,当初咱们的渲染树中的节点有了款式,然而不晓得要画在哪个地位。所以还须要另外一颗布局树确定元素的几何定位。

布局树只取渲染树中的可见元素,意味着 head 标签,display:none的元素不会被增加。

图层分层

当初咱们有了布局树,但仍旧不能间接开始绘制,在此之前须要分层,生成一棵对应的图层树。浏览器的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面。

因为页面中有很多简单的成果,如一些简单的 3D 变换、页面滚动,或者应用 z-index 做 z 轴排序等,咱们心愿能更加不便地实现这些成果。

并不是布局树的每个节点都能生成一个图层,如果一个节点没有本人的层,那么这个节点就从属于父节点的图层

通常满足上面两点中任意一点的元素就能够被晋升为独自的一个图层。

1、领有层叠上下文属性的元素会被晋升为独自的一层:明确定位属性 position 的元素、定义通明属性 opacity 的元素、应用 CSS 滤镜 filter 的元素等,都领有层叠上下文属性。

2、须要剪裁(clip)的中央也会被创立为图层overflow

在 chrome 的开发者工具:更多选项 - 更多工具 -Layers能够看到图层的分层状况。

图层绘制

在实现图层树的构建之后,接下来终于到对每个图层进行绘制。
首先会把图层拆解成一个一个的绘制指令,排布成一个绘制列表,在上文提到的开发者工具的 Layers 面板中,点击 detail 中的 profiler 能够看到绘制列表。

至此,渲染过程中的主线程——GUI 渲染线程曾经实现了它所有工作,接下来交给渲染过程中的合成现成。

合成线程接下来会把视口拆分成图快,把图块转换成位图。

至此,渲染过程的工作全副实现,接下来会把生成的位图还给浏览器过程,最初在页面上显示。

性能优化,还能够做些什么

本篇不专讲性能优化,只是在这个命题下补充一些常见伎俩。

预解析、预渲染

除了上文提到的应用 preload、prefetch 去提前加载,还能够应用DNS PrefetchPrerenderPreconnect

  1. DNS Prefetch:DNS 预解析;

     <link rel="dns-prefetch" href="//fonts.googleapis.com">
  2. preconnect:在一个 HTTP 申请正式发给服务器前事后执行一些操作,这包含 DNS 解析,TLS 协商,TCP 握手;

    <link href="https://cdn.domain.com" rel="preconnect" crossorigin>

  3. Prerender: 获取下个页面所有的资源,在闲暇时渲染整个页面;

    <link rel="prerender" href="https://www.keycdn.com">

    缩小回流和重绘

回流是指浏览器须要从新计算款式、布局定位、分层和绘制,回流又被叫重排;

触发回流的操作:

  • 增加或删除可见的 DOM 元素
  • 元素的地位发生变化
  • 元素的尺寸发生变化
  • 浏览器的窗口尺寸变动

重绘是只从新像素绘制,当元素款式的扭转不影响布局时触发。

回流 = 计算款式 + 布局 + 分层 + 绘制;重绘 = 绘制。故回流对性能的影响更大

所以应该尽量避免回流和重绘。比方利用 GPU 减速来实现款式批改,transform/opacity/filters这些属性的批改都不是在主线程实现的,不会重绘,更不会回流。

结语

把“URL 输出到渲染”整个过程讲完,回到结尾几个比拟刁钻的问题,在文中都不难找到答案:

  1. 浏览器将输出内容解析后,拼接成残缺的 URL,其中的参数应用的是 UTF- 8 编码,也就是咱们开发时会罕用的 encodeURI 和 encodeURIComponent 两个函数,其中 encodeURI 是对残缺 URL 编码,encodeURIComponent 是对 URL 参数局部编码,要求会更严格;
  2. 浏览器缓存的 disk cache 和 memory cache 别离是从磁盘读取和从内存中读取,通常刷新页面会间接从内存读,而敞开 tab 后从新关上是从磁盘读;
  3. 预加载 prefetch 是在闲暇工夫,以低优先级加载后续页面用到的资源;而 preload 是以高优先级提前加载以后页面须要的资源;
  4. 脚本的 async 是指异步加载,实现加载立即执行,defer 是异步加载,实现 HTML 解析后再执行;
  5. TCP 握手须要三次的三次是为了保障客户端的存活,避免服务端资源的节约,挥手要四次是因为 TCP 是双工通信,每一个方向的连贯开释、应答各须要一次;
  6. HTTPS 的握手是为了协商出一个对称密钥,单方一共发送三个随机数,利用这三个随机数计算出只有单方晓得的密钥,正式通信的内容都是用这个密钥进行加密的;

如果这篇文章对你有帮忙,帮我点个赞呗~这对我很重要

退出移动版