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

最近经常可以看到一个前端面试题,就是从输入URL到页面加载,这个过程会发生没什么。其实,这是一个非常开放性的问题,曾看到一个调侃,如果面试官敢问这个问题,他可以用一个下午把整个流程讲一遍,从硬件方面键盘工作的原理到网路的传输都说一遍。网上大部分的回答主要是关于DNS解析和到服务端之前的过程。最近,看到一篇关于浏览器是如何工作的文章,所以我想聊下,这个问题的后半部分,就是当静态资源传输到浏览器之后,会经过哪些部分,又做了哪些处理。

这里一个主要的部分就是呈现引擎。呈现引擎主要负责解析HTML和CSS,并将内容转化为肉眼可见的UI。

市场上每家浏览器都使用略微不同的引擎,IE使用自己开发的Trident,火狐使用Gecko,Safari是使用开源的Webkit,Chrome和Opera fork了webkit,并在此基础上开发了Blink。每个呈现引擎在构建DOM树的方式和流程都有些不同,也导致同一份代码,在不同的浏览器上会有不用的显示效果。下面,是以WebKit为例子,看下他们的流程是什么样子的。

当浏览器遇到静态资源

HTML,CSS这些静态资源,千里迢迢经过各个环节来到浏览器的时候,主要经历四个基本步骤

解析HTML和CSS。HTML的解析过程,会把HTML标签按照顺序,组成内容树。这种解析并不是转化为机器代码,而是转换成另外一种格式保存这些数据。

呈现树的构建。呈现树是内容树和样式规则的结合。简单总结就是HTML和CSS通过解析,转换为树状格式,方便浏览器进行渲染。

布局。来到这里,浏览器会递归的对每个节点计算出位置和大小信息。

绘制。在这一步,真正的UI才会被绘制在浏览器上面,供用户浏览和使用。

每个浏览器对这四个步骤都会有不同的实现,下图为WebKit的实现流程。

图1:Webkit 呈现引擎的流程。

如图所示,HTML和CSS首先会通过两个解析器,分别为HTML Parser(HTML解析器)和CSS Parser(CSS 解析器)。解析过程在呈现引擎里面是非常重要的一步。

可以通过下面一个例子,简单理解这个过程。

2 + (3 - 1)

假设我们要去教一个幼儿园的小朋友上面的计算方式。我们可能需要以下几步去开始。

学单词。比如,首先知道这些数字,加号,减号和括号是什么意思。

学规则。比如,括号内的级别优先。运算顺序从左及右。

同理,解析器在面对HTML,CSS和JavaScript的时候也需要去定义单词和规则,这里的单词量更大,规则更为复杂。也有了更学术的名字,词法分析(学单词)和语法分析(学语法)。

词法分析将无关内容剔除,比如空格和换行,提取有效内容并分割成更小的单位,如具体的单词。这就相当于告诉计算机每个单词的意思。认识了单词之后,还需要学习语法,才能懂的上面公式的结果。语法分析会根据定义的规则去构建并学习单词之间的关系,最终明白了整个句子的意思。在这个过程中还会碰到很多问题,比如有些HTML写的并不是很规范或者要兼容旧的HTML规范。HTML和CSS经过解析之后,格式会经过处理,变成了树状结构,有一定的层级关系。相当于计算机通过我们定义的单词和语法,理解了我们的代码之后,经过自己的理解转换成一种更方便计算机处理的方式 --- 呈现树。

呈现树是按照用户肉眼可见的元素排列而成,它和我们熟悉的DOM树很相似但又有些差别,比如DOM里面有些元素不会和呈现树里的元素一一相对应,如head标签,又或display为none的元素,这些元素是不会被展示出来,所以并没有被纳入到呈现树中。所以呈现树只会保留所有用户可见的元素,用来方便接下来的构建和渲染。

在呈现树中,因为是一种树状结构,我们可以看到每个节点之间的关系,可以明显的找出上下级关系,这个上下级关系,也是最终渲染的关系,从里面可以看出是否有子节点或者同级节点。样式经过解析,也会以另外一种更加节省内存的结构储存下来。样式的规则会附加在内容树上面从而组成了最终的呈现树。

有了呈现树,浏览器可以依次计算出具体节点的位置和大小。这个过程是个递归的过程。从顶端<html>开始,根据从左到右,由上及下的开始计算,直到所有的节点的信息都计算完成。为了应对局部的小变化而导致整体的重新计算,浏览器会把变化的部分标记为dirty,或者children are dirty,用来表示本节点或者子节点需要重新计算。剩下的工作就是把所有计算的内容,渲染到页面上。同样,渲染的过程也会分局部和全局渲染。

整个的渲染过程是单线程同步运行。解析器在解析HTML的过程中,如果碰到<script>标签,会停下渲染树的构建,转而去解析JavaScript,特别在需要异步加载JavaScript的时候,由于资源需要网络传输,线程会停下来,直到资源加载并解析完成之后,才会继续之前的工作。遇到这个过程,用户界面会出现停滞,非常影响用户体验。而如何保证HTML和CSS的正常解析不受影响,会直接关系到用户什么时候可以看到UI界面,减少等待时间,提高用户留存率。在<script>标签上添加defer属性,会让JavaScript的解析延后,保证HTML优先得到展示。HTML 5 新的属性async则会让JavaScript这些静态资源通过新的线程平行的去下载和解析。Chrome和火狐,还可以通过提前解析,去寻找是否还有其他的静态资源去下载,如果有,就通过新的线程去下载。

当我们的前端页面正确的展示在浏览器上之后,之前被延后的JavaScript开始解析和执行。下面图2是Chrome V8的处理JavaScript的过程。

图2:V8引擎中,JavaScript的运行过程

Ignition为JavaScript的解释器,负责产生和运行bytecode。在运行这些bytecode的过程中,会记录下运行数据(profiling data)。如果某个方法运行的次数很多,bytecode和profiling data会被传给加速器(TurboFan)进行优化并转化为运行速度更快的机器码,一个编译过的机器码出现问题,则会被降级转回之前的bytecode。

一般,如果我们的网站内容交互性不强,注重展示内容,我们可以延后解析JavaScript,让主线程优先渲染HTML和CSS,以求得到更好的用户体验。也可以把加载和解析JavaScript的任务交给其他线程来处理。良好的代码规范也可以避免触发渲染引擎的容错机制。

作者 | 极链科技Video++ 整理 | 包包

参考文章:

《浏览器的工作原理 How Browers Work》

《JavaScript engine fundamentals》