从输入 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 请求
包含三部分:请求行、请求报头和请求正文。
- 请求行
一个 HTTP 方法:
GET
PUT
POST
OPTION
HEAD
等;请求目标:通常是一个 URL,或者是协议、端口和域名的绝对路径;
HTTP 版本
- header部分
允许客户端向服务器传递请求的附加信息和客户端自身的信息,常见的请求报头有:
- Accept:指定客户端用于接受哪些类型的信息
- Accept-Encoding:用于指定接受的编码方式
- Connection:设置为Keep-alive用于告诉客户端本次HTTP请求结束之后并不需要关闭TCP连接,这样可以使下次HTTP请求使用相同的TCP通道,节省TCP连接建立的时间
- Accept-Language:指定接受的语言
- Cache-Control:缓存控制
- 请求正文
当使用POST, PUT等方法时,通常需要客户端向服务器传递数据。这些数据就储存在请求正文中。
服务器返回 response
响应消息也是由三部分构成:状态行、响应消息报头以及响应正文
- 状态行
由HTTP协议版本号,状态码和原因描述组成
- 状态码:常见的有200,304,404,500等
- 响应消息报头
常见的报头有:
- Location:用于重定向接受者到一个新的位置
- Server:包含了服务器用来处理请求的软件信息
- Content-Length:用于指明实体正文的长度
- Content-Type:用于指明发送给接收者的实体正文的媒体类型
- 其他等
- 响应正文
一般是服务器发给客户端的内容,包括HTML、图片等
浏览器渲染界面
如果说响应的内容是HTML文档的话,就需要浏览器进行解析渲染呈现给用户。整个过程涉及两个方面:解析和渲染;对于现代浏览器,为了达到更好的用户体验,浏览器的呈现引擎会力求尽快将内容显示到屏幕上;而不必等到整个HTML文档解析完毕之后再去构建渲染树然后布局渲染;也就是说这是一个渐进的过程。
解析,构建对象模型
在渲染页面之前,需要构建DOM树和CSSOM树。
DOM树和CSSOM树的构建基本过程是这样的:
- Conversion转换:浏览器将获得的HTML内容(Bytes)基于他的编码转换为单个字符
- Tokenizing分词:浏览器按照HTML规范标准将这些字符转换为不同的标记token。每个token都有自己独特的含义以及规则集
- Lexing词法分析:分词的结果是得到一堆的token,此时把他们转换为对象,这些对象分别定义他们的属性和规则
- DOM构建:因为HTML标记定义的就是不同标签之间的关系,这个关系就像是一个树形结构一样
构建渲染树
渲染树与DOM树对应,但并不是一一对应,因为非可视化的DOM元素不会被插入到渲染树中,例如head元素、display:none不会出现在渲染树。
渲染
有了渲染树,就可以进行渲染了,包含四个步骤:
- 计算CSS样式
- 构建渲染树
- 布局,主要定位坐标和大小,是否换行,各种position、overflow、z-index等
- 调用操作系统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到页面加载的简单过程就完毕了,在这个过程中,对于前端工程师而言,需要很熟悉页面的渲染过程,以消耗最少的成本完成页面的加载,代码的优化是无止境的。