知识点串联:输入URL 到页面加载过程详解

本文主要两个目的:

  • 第一,将前面涉及网络协议特别是TCP协议HTTP协议DNS解析等等知识点贯穿;
  • 第二,在此基础上介绍浏览器渲染过程,以及为Web优化提供基础。@pdai

地址栏输入URL

URL : Uniform / Universal Resource Locator , 即统一资源定位符。它实际上就是网站网址。浏览器就是靠URL来查找资源位置。

可以把URL分割成几个部分:协议、网络地址、资源路径。

  • 传送协议: URL包含协议部分,是浏览器和www万维网之间的沟通方式,它会告诉浏览器正确在网路上找到资源位置。最常见的网络传输协议的是HTTP协议(超文本传输协议)( https则是进行加密的网络传输);其他也还有ftp 、file、 https、mailto 、git 等。还有自定义的协议(私有协议),例如tencent。不同协议有不同的通讯内容格式。
  • 网络地址: 指示该连接网络上哪一台计算机,可以是域名或者IP地址,可以包括端口号;
  • 资源路径: 指示从服务器上获取哪一项资源。

例如: http://www.quaro.com/question/123456/

  • 协议部分:http
  • 网络地址:www.quaro.com
  • 资源路径:/question/123456/

DNS 域名解析IP

这里主要贯穿下思路,DNS涉及相关其它知识点可以参考:网络协议 - DNS 解析详解

基础概念

IP 地址:IP 协议为互联网上的每一个网络和每一台主机分配的一个逻辑地址。IP 地址如同门牌号码,通过 IP 地址才能确定一台主机位置。服务器本质也是一台主机,想要访问某个服务器,必须先知道它的 IP 地址。

域名 DN(domain name ):域名是为了识别主机名称和组织机构名称的一种具有分层的名称。 IP 地址由四个数字组成,中间用点号连接,在使用过程中难记忆且易输错,所以用我们熟悉的字母和数字组合来代替纯数字的 IP 地址,比如我们只会记住 www.baidu.com (百度域名) 而不是 220.181.112.244(百度的其中一个 IP 地址)。

计算机域名系统 DNS ( Domain Name System or Domain Name Service): 它是由域名解析器和域名服务器组成的。 域名服务器是指保存有该网络中所有主机的域名和对应IP地址,并具有将域名转换为IP地址功能的服务器。 每个域名都对应一个或多个提供相同服务的服务器的 IP 地址,只有知道服务器 IP 地址才能建立连接,所以需要通过 DNS 把域名解析成一个 IP 地址。

查找域名对应的IP地址

通过域名查找IP过程:浏览器缓存 -> 系统缓存 -> 本地DNS服务器缓存

  • 浏览器搜索自己的DNS缓存(维护一张域名与IP地址对应表)
  • 搜索操作系统中的DNS缓存(维护一张域名与IP地址对应表)
  • 搜索操作系统的hosts文件(windows环境下,维护一张域名与IP地址对应表)
  • 操作系统将域名发送到本地区域服务器(LNDS),进行查找,成功则返回结果(递归查询),失败则发起一个迭代DNS请求(迭代查询)// 迭代查询递归查询请参考下一节
  • 本地域名服务器LDNS将得到的IP地址返回给操作系统,同时也将IP地址缓存起来
  • 操作系统将IP地址返回给浏览器,同时将IP地址缓存起来

DNS迭代查询和递归查询

这里独立一节只是补充强调说明这两个重要的概念... 实际上是结合在一起的。

看下图即可:

  • 递归查询:客户端与服务器之间属于递归查询,即当客户机想DNS服务器发出请求后,若DNS服务器本身不能解析,会向另一个DNS服务器发出查询请求,最后将结果转交给客户端的过程。 服务器必须回答目标IP与域名的映射关系。

  • 迭代查询:DNS服务器之间属于迭代查询。服务器接收到一次迭代查询回复一次结果,这个结果不一定死目标IP与域名的映射关系,也可以是其他DNS服务器的地址。

请求和响应数据

包括:

  • TCP连接建立
  • 发送http 请求
  • 服务端处理
  • 返回http 结果
  • TCP连接关闭。

建立TCP连接(3次握手)

上一步找到IP之后,便可以开始建立TCP连接了,这里就是我们所说的TCP3次握手。详情可以参考:网络协议 - TCP 协议详解

发送HTTP请求

与服务器建立了连接后,就可以向服务器发起请求了。详情可以参考:网络协议 - HTTP 协议详解

请求报文结构如下:

服务器处理请求

服务器端收到请求后的由web服务器(准确说应该是http服务器)处理请求,诸如Apache、Ngnix、IIS等。web服务器解析用户请求,知道了需要调度哪些资源文件,再通过相应的这些资源文件处理用户请求和参数,并调用数据库信息,最后将结果通过web服务器返回给浏览器客户端。

详情可以参考:网络协议 - HTTP 协议详解

返回HTTP响应结果

服务器处理完请求后,就会发送响应结果。响应报文的结构如下:

响应结果中会有对应的HTTP状态码,可分为5类:

详情可以参考:网络协议 - HTTP 协议详解

关闭TCP连接(4次挥手)

为了避免服务器与客户端双方的资源占用和损耗,当双方没有请求或响应传递时,任意一方都可以发起关闭请求。与创建TCP连接的3次握手类似,关闭TCP连接,需要4次握手。

详情可以参考:网络协议 - TCP 协议详解

浏览器加载,解析和渲染

浏览器构成

浏览器内核也称渲染引擎,主要有3种:

  • Trident内核: IE
  • Webkit内核:Chrome,Safari
  • Gecko内核:FireFox

浏览器加载

加载过程如下

  • 当浏览器获得一个html文件时,会”自上而下“加载,并在加载过程中进行解析渲染。
  • 加载过程中遇到外部css文件,浏览器另外发出一个请求,来获取css文件。
  • 遇到图片资源,浏览器也会另外发出一个请求,来获取图片资源。这是异步请求,并不会影响html文档进行加载。
  • 但是当文档加载过程中遇到js文件,html文档会挂起渲染(加载解析渲染同步)的线程,不仅要等待文档中js文件加载完毕,还要等待解析执行完毕,才可以恢复html文档的渲染线程。

加载外联js和css的阻塞情况:一个不太严谨但方便记忆的口诀:JS 全阻塞,CSS 半阻塞

  • JS 会阻塞后续 DOM 解析以及其它资源(如 CSS,JS 或图片资源)的加载。
  • CSS不阻塞DOM的加载和解析(它只阻塞DOM的渲染呈现。这里谈加载),不会阻塞其它资源(如图片)的加载,但是会阻塞 后续JS 文件的执行(原因之一是,js执行代码可能会依赖到css样式。css只阻塞执行而不阻塞js的加载)。
  • 鉴于上面的特性,当css后面存在js的时候,css会间接地阻塞js后面资源的加载(css阻塞js,js阻塞其他资源 )。
  • 现代浏览器会进行 prefetch 优化,浏览器在获得 html 文档之后会对页面上引用的资源进行提前下载

外联js文件使用defer属性和asyn可以达到异步非阻塞加载的效果,由于现代浏览器都存在 prefetch,所以 defer, async 可能并没有太多的用途,可以作为了解扩展知识,仅仅将脚本文件放到 body 底部(但还是在</body>之前)就可以起到很不错的优化效果(遵循先解析再渲染再执行script这个顺序)。当把js放在最后的时候,其实浏览器将自动忽略</body>标签,从而自动在最后的最后补上</body>

浏览器解析和渲染

步骤如下:

  • 解析html,生成dom树
  • 解析css,生成cssom树
  • 将dom树和cssom树合并,生成渲染树
  • 遍历渲染树,开始布局和计算
  • 绘制渲染树,显示到屏幕

解析html,生成dom树

当浏览器接收到服务器响应来的HTML文档后,会自上而下扫描文档,开始解析,遍历文档节点,生成DOM树。

整个构建过程其实包括: 字节 -> 字符 -> 令牌 -> 节点对象 -> 对象模型,下面是示例代码和配图:

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
    <title>Critical Path</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg"></div>
  </body>
</html>

解析css,生成cssom树

  • 每个css文件都被分析成一个stylesheet对象,每个对象都包含CSS规则。
  • css规则对象包含对应于css语法的选择器和声明对象以及其他对象。

构建过程没有什么特别的差别,下面是示例代码和配图:

body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }

将dom树和cssom树合并,生成渲染树

  • 浏览器会先从dom树的根节点开始遍历每个可见节点,找到其适配的CSS样式规则并应用。
  • 将dom树与cssom树结合在一起,这就是渲染树。
  • 每一个渲染对象都对应着dom节点,但是非视觉(隐藏,不占位)dom元素不会插入渲染树,如<head>元素或声明display: none;的元素。
  • 渲染对象与dom节点不是简单的一对一的关系,一个dom可以对应一个渲染对象,但一个dom元素也可能对应多个渲染对象,因为有很多元素不止包含一个css盒子。(如当文本被折行时,会产生多个行盒,这些行会生成多个渲染对象;又如行内元素同时包含块元素和行内元素,则会创建一个匿名块级盒包含内部行内元素,此时一个dom对应多个渲染对象)

遍历渲染树,开始布局和计算

布局阶段会从渲染树的根节点开始遍历,然后确定每个节点对象在页面上的确切大小与位置。 布局阶段的输出是一个盒子模型,它会精确地捕获每个元素在屏幕内的确切位置与大小,所有相对的测量值也都会被转换为屏幕内的绝对像素值。

绘制渲染树,显示到屏幕

在绘制阶段,浏览器会立即发出Paint Setup与Paint事件,开始将渲染树绘制成像素,绘制所需的时间跟CSS样式的复杂度成正比,绘制完成后,用户就可以看到页面的最终呈现效果了。

Repaint和Reflow

当用户在浏览网页时进行交互或通过 js 脚本改变页面结构时,以上的部分操作有可能重复运行,此过程称为 Repaint 或 Reflow。

Repaint

当元素改变的时候,将不会影响元素在页面当中的位置(比如 background-color, border-color, visibility),浏览器仅仅会应用新的样式重绘此元素,此过程称为 Repaint。

Reflow

当元素改变的时候,将会影响文档内容或结构,或元素位置,此过程称为 Reflow。( HTML 使用的是 flow based layout ,也就是流式布局,所以,如果某元件的几何尺寸发生了变化,需要重新布局,也就叫 Reflow。)

Reflow 的成本比 Repaint 的成本高得多的多。我们应当尽量避免Reflow。

进而:如何优化浏览器渲染过程

针对html

  • html文档结构层次尽量少,最好不深于6层
  • 首屏html可以少量,主体结构动态插入
  • 尽量减少将 DOM 节点属性值放在循环当中,会导致大量读写此属性值。
  • 创建有效的 HTML 和 CSS ,不要忘记指定文档编码,比如<meta charset="utf-8">

针对css

  • 使用媒体查询,减少初次cssom树的构建量
  • 尽量用id和class,不要过渡层叠
  • 样式结构层次尽量简单
  • 尽可能的为产生动画的 HTML 元素使用 fixed 或 absolute 的 position ,那么修改他们的 CSS 是不会 Reflow 的。

针对js

  • 使用defer和async,避免对文档的阻塞
  • 可以的话,动态插入js,避免阻塞
  • 不要通过 JS 逐条修改 DOM 的样式,提前定义好 CSS 的 Class 进行操作。

针对引入位置

  • css放到head,让cssom树先行构建;js放到</body>前,保证dom树先行构建,不被阻塞 避免js文件的插入跟在css文件之后,避免css解析对js执行的延迟,造成阻塞

针对资源载入

  • 对页面资源进行压缩,对传输进行gzip压缩
  • 利用link标签的rel属性进行预解析,运用http缓存

参考文章

  • https://www.zhihu.com/question/263866883/answer/276139578
  • https://segmentfault.com/a/1190000008835506
  • https://segmentfault.com/a/1190000013522717
  • https://blog.csdn.net/qq_29311407/article/details/79988700
  • https://www.cnblogs.com/yongwunaci/p/10671999.html
  • https://www.jianshu.com/p/8758e8a237ee