从输入 URL 到页面加载发生了什么

URL

首先,在回答在个问题之前,我们需要搞清楚URL是什么。

常见的 URL 会包含:

  • 协议名
    常见的协议是 HTTP 协议,除此之外还包括
    • HTTPS
    • FTP
    • FILE 等
  • 域名
  • 端口号
    端口号一般都是默认隐藏的,HTTP 协议默认端口是 80,HTTPS 协议默认端口是 443.
  • path
    一般取决于服务器的路由结构
  • 问号参数和 hash
    • 问号传参是客户端把信息传递给服务器的一种方式(也有可能是跳转到某一个页面,把参数值传递给页面用来标识的)
    • hash 主要用于页面中锚点定位和 hash 路由切换

从输入 URL 到页面加载发生的事

这个过程主要可以分为以下几个过程:

  • DNS 解析
  • 建立 TCP 连接
  • 客户端发送 HTTP 协议
  • 服务器返回 response
  • 浏览器渲染页面
  • 结束连接

DNS 解析

DNS 解析是找到对应服务器的 IP 地址

  • 查找方法有:迭代和递归两种
  • 查询的位置有
    • 浏览器缓存
    • 操作系统缓存
    • 本地域名服务器
    • 顶级域名服务器
    • 根域名服务器
      直到获得准确的 IP 地址

建立 TCP 连接

在获得 IP 地址后,便开始建立一次连接,由 TCP 协议完成,主要通过 3 次握手进行连接,作为前端工程师,这与我们的前端关系不大,就不细说。

客户端发送 HTTP 请求

这里的客户端主要指浏览器,还有其他的,比如命令行和一些前端调试工具等。

HTTP 与 HTTPS 的区别

HTTP 报文是包裹在 TCP 报文中发送的,HTTP 报文是明文,容易被截取,所以就出现了 HTTPS。HTTPS 在讲 HTTP 报文包裹进 TCP 报文时,使用 SSL 进行了加密,HTTPS 在数据传输之前,客户端会与服务器进行一次握手,确定加密传输的密码信息,如此可以保证数据传输的安全性。

HTTP 请求

包含三部分:请求行、请求报头和请求正文。

  • 请求行
  1. 一个 HTTP 方法:GET PUT POST OPTION HEAD等;

  2. 请求目标:通常是一个 URL,或者是协议、端口和域名的绝对路径;

  3. HTTP 版本

  • header部分

允许客户端向服务器传递请求的附加信息和客户端自身的信息,常见的请求报头有:

  1. Accept:指定客户端用于接受哪些类型的信息
  2. Accept-Encoding:用于指定接受的编码方式
  3. Connection:设置为Keep-alive用于告诉客户端本次HTTP请求结束之后并不需要关闭TCP连接,这样可以使下次HTTP请求使用相同的TCP通道,节省TCP连接建立的时间
  4. Accept-Language:指定接受的语言
  5. Cache-Control:缓存控制
  • 请求正文

当使用POST, PUT等方法时,通常需要客户端向服务器传递数据。这些数据就储存在请求正文中。

服务器返回 response

响应消息也是由三部分构成:状态行、响应消息报头以及响应正文

  • 状态行

由HTTP协议版本号,状态码和原因描述组成

  1. 状态码:常见的有200,304,404,500等
  • 响应消息报头

常见的报头有:

  1. Location:用于重定向接受者到一个新的位置
  2. Server:包含了服务器用来处理请求的软件信息
  3. Content-Length:用于指明实体正文的长度
  4. Content-Type:用于指明发送给接收者的实体正文的媒体类型
  5. 其他等
  • 响应正文

一般是服务器发给客户端的内容,包括HTML、图片等

浏览器渲染界面

如果说响应的内容是HTML文档的话,就需要浏览器进行解析渲染呈现给用户。整个过程涉及两个方面:解析和渲染;对于现代浏览器,为了达到更好的用户体验,浏览器的呈现引擎会力求尽快将内容显示到屏幕上;而不必等到整个HTML文档解析完毕之后再去构建渲染树然后布局渲染;也就是说这是一个渐进的过程。

解析,构建对象模型

在渲染页面之前,需要构建DOM树和CSSOM树。

DOM树和CSSOM树的构建基本过程是这样的:

  1. Conversion转换:浏览器将获得的HTML内容(Bytes)基于他的编码转换为单个字符
  2. Tokenizing分词:浏览器按照HTML规范标准将这些字符转换为不同的标记token。每个token都有自己独特的含义以及规则集
  3. Lexing词法分析:分词的结果是得到一堆的token,此时把他们转换为对象,这些对象分别定义他们的属性和规则
  4. DOM构建:因为HTML标记定义的就是不同标签之间的关系,这个关系就像是一个树形结构一样

构建渲染树

渲染树与DOM树对应,但并不是一一对应,因为非可视化的DOM元素不会被插入到渲染树中,例如head元素、display:none不会出现在渲染树。

渲染

有了渲染树,就可以进行渲染了,包含四个步骤:

  1. 计算CSS样式
  2. 构建渲染树
  3. 布局,主要定位坐标和大小,是否换行,各种position、overflow、z-index等
  4. 调用操作系统Native GUI的api绘制内容

JS动态修改了DOM或者CSSOM,会导致重新布局或者渲染

这里涉及到了两个重要的概念:reflow(回流)和repaint(重绘)

  • reflow:一般意味着元素的内容、结构、位置或尺寸发生了变化,需要重新计算样式和渲染树,这个过程称为Reflow
  • repaint:意味着元素发生的改变只是影响了元素的一些外观之类的时候(例如,背景色,边框颜色,文字颜色等),此时只需要应用新样式绘制这个元素就OK了,这个过程称为Repaint

所以说Reflow的成本比Repaint的成本高得多的多。DOM树里的每个结点都会有reflow方法,一个结点的reflow很有可能导致子结点,甚至父点以及同级结点的reflow

下面这些动作很大可能会是成本比较高的:

  • 增加、删除、修改DOM结点时,会导致Reflow或Repaint
  • 移动DOM的位置,或是搞个动画的时候
  • 内容发生变化
  • 修改CSS样式的时候
  • Resize窗口的时候(移动端没有这个问题),或是滚动的时候
  • 修改网页的默认字体时

注:display:none会触发reflow,而visibility:hidden只会触发repaint,因为没有发生位置变化。

基本上来说,reflow有如下的几个原因:

  • 网页初始化的时候
  • 一些Javascript在操作DOM树时
  • 其些元件的尺寸变了
  • 如果CSS的属性发生变化了
  • 几个Incremental的reflow发生在同一个frame的子树上

解析和渲染总结

这里需要注意的一件事情就是在HTML解析过程中回去加载外链的CSS,但是不会影响继续解析HTML的;在外链CSS得到之后要解析CSS。从前面的介绍可知渲染的话是需要DOM和CSSOM一起构建出来渲染树,然后渲染出来的,也就是说默认情况下CSS是会阻塞渲染的,为啥说默认情况呢,难道还有不阻塞渲染的时候?答案是有的,通过media query就可以使得CSS资源是非阻塞渲染的。

JS脚本

说完了DOM和CSSOM了,就该说说这个JS脚本了

通过JS脚本可以通过DOM API和CSSOM API来才做DOM树和CSSOM树(或者说CSS规则树);但是呢JS是会阻塞DOM的构建(除非显示的声明为异步async的)也会阻塞CSSOM的构建,也就意味着会推迟这个页面的渲染完成。

在页面中的脚本有两种情况,一种就是内嵌的,还有一种外链的

对于脚本内嵌的情况,在解析HTML的过程中,直接执行脚本,这个时候会阻塞HTML解析来构建DOM,因为CSS不会修改DOM;还有一种情况那就是如果说正在脚本前面还有CSS的话,而此时CSSOM还未构建完成,那么浏览器就会推迟脚本的执行直至下载并构建好了CSSOM,而且在这个等待的过程中DOM的构建也会停止。所以说,在内嵌脚本之前不要有外链CSS,否则的话就会出现所谓的“CSS阻塞”,其实就是必须等到CSS加载完成解析构建CSSOM之后才会执行脚本,执行完脚本才会继续解析HTML构建DOM(这里Webkit则更智能一点,在执行脚本过程中发现引用了样式的话才暂停脚本的执行,等待CSS下载解析,然后再恢复)

然后第二种情况,对于外链脚本而言,在解析HTML的过程中发现了外链的脚本,会发一个请求去得到脚本内容,但是这个过程是同步的,需要等待脚本下载完成且执行之后才会继续解析HTML构建DOM;但是对于现代浏览器在这个时候会生成第二个线程解析HTML文档,会继续下载资源,所以有多个外链脚本的话,会并行请求下载脚本内容,但是浏览器对于一个域的资源是有最大并行限制的,一般是6个,超过的就只能等待了。脚本虽然可以并行加载,但是执行的顺序是按照在页面中先后顺序执行的,执行的过程会阻塞后续解析构建渲染,同样也会阻止其他资源的下载。

总结

至此,输入一个简单的URL到页面加载的简单过程就完毕了,在这个过程中,对于前端工程师而言,需要很熟悉页面的渲染过程,以消耗最少的成本完成页面的加载,代码的优化是无止境的。