from 从输入 URL 到页面加载的过程?如何由一道题完善自己的前端知识体系!
作为进阶回头再来看看:从输入 URL 到页面加载完成的过程中都发生了什么事情?
解析页面流程
流程简介
- 解析
html,生成DOM树 - 解析
css,生成css规则树 - 合并
dom树 和css规则树,生成render树 - 布局
render树 (layout/relow) ,负责各尺寸和位置的计算 - 绘制
render树 (paint),绘制页面像素信息 - 浏览器会把各层的信息发给
GPU,GPU会把各层的信息合成composite,最终显示屏幕上

html 解析,构建 dom 树
bytes -> characters -> tokens -> node -> dom
比如下面一段 html 代码
1
2
3
4
5
6
7
8
9
10
11
<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>
浏览器的处理过程

其中的重要流程有
Conversion转换: 浏览器把获取到的html内容 (bytes) 基于它的编码转成各个字符Tokenizing分词: 浏览器按照html规范把这些分词转换成不同的标记token,每个token都有自己独特的含义和规则集Lexing词法分析: 分词得到一堆的token,此时把它们转成对象,这些对象有他们的属性和规则dom构建: 由于html标记定义的就是不同标签之间的关系,这个关系就像属性结构一样(比如body对象的父节点就是HTML对象,然后段落p对象的父节点就是body对象)
生成 css 规则树
类似也是
Bytes → characters → tokens → nodes → CSSOM
比如
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
body {
font-size: 16px;
}
p {
font-weight: bold;
}
span {
color: red;
}
p span {
display: none;
}
img {
float: right;
}
最终生成下面的树

构建渲染树
一般渲染树和 dom 树一一对应,但是也不一定,一些不可见的元素不会出现在渲染树,比如 head 标签,和 display:none 等不可见的元素

渲染
有了渲染树就可以开始渲染了,基本流程如下图

- 计算
css样式 - 构建渲染树
- 布局,主要定位坐标和大小,是否换行,各种
positionoverflowz-index属性 - 绘制,把图像绘制出来
js 动态修改了 dom 和 css 的话,会导致重新布局 layout 或者 渲染repaint
他们是有区别的
- layout,也叫做
reflow,就是回流,意味着元素的内容,结构,位置,尺寸发生了变化,需要重新计算样式和渲染树 - repaint,重绘,以为发生了一些影响外观的变化,比如背景颜色,边框颜色,文字颜色等,只要应用新的样式就好了
回流成本更加高,一个节点的回流经常导致子节点和同级节点的回流,所以优化的方案一般包含了避免回流
什么会引起回流
- 改变字体大小
- 页面初始化渲染
dom结构变化render树变化,比如padding变少了- 窗口
resize - 获取某些属性,也会
很多浏览器做了优化,会等到一定量才统一处理
但是除了 render 树的直接变化,获取一些属性的时候,浏览器为了获取到正确的值也会触发回流,这样浏览器的优化也无效了
比如
offset(Top/Left/Width/Height)scroll(Top/Left/Width/Height)cilent(Top/Left/Width/Height)width,height- 调用了
getComputedStyle()或者IE的currentStyle
回流一定伴随着重绘,重绘却可以单独出现
优化方案有下面几个
- 减少逐项更改样式,最好一次性更改
style,或者将样式定义为class并一次性更新 - 避免循环操作
dom,创建一个documentFragment或div,在它上面应用所有DOM操作,最后再把它添加进去 - 避免多次读取
offset等属性,无法避免则将它们缓存到变量 - 将复杂的元素绝对定位或固定定位,使得它脱离文档流,否则回流代价会很高
最后看看一个案例
1
2
3
4
5
6
7
8
const s = document.body.style;
s.padding = "2px"; // 回流 + 重绘
s.border = "1px solid red"; // 再一次 回流 + 重绘
s.color = "blue"; // 再一次重绘
s.backgroundColor = "#ccc"; // 再一次 重绘
s.fontSize = "14px"; // 再一次 回流 + 重绘
// 添加node,再一次 回流 + 重绘
document.body.appendChild(document.createTextNode("abc!"));
简单层与复合层
上面止步于绘制,但是其实这一步没那么简单,简单介绍下 简单层和复合层
- 可以认为默认只有一个复合层,所有的
dom都是在这个复合层里面 - 如果开启了硬件加速功能,可以把某个节点变成复合层
- 复合层之间的绘制互不干扰,由
gpu控制 - 简单图层中,就算是
absolute等布局,变化时不影响整体的回流,但是在同一个图层中,仍然会影响绘制的,因此做动画的性能很低,复合层是独立的,所以动画一般推荐使用复合层
资源外链的下载
html 解析的过程会遇到一些外链,需要单独处理
遇到的资源举例有下面三种
CSS样式资源JS脚本资源img图片类资源
当遇到上述的外链时,会单独开启一个下载线程去下载资源(http1.1 中是每一个资源的下载都要开启一个 http 请求,对应一个 tcp/ip 链接)
遇到 CSS 样式资源
CSS下载时异步,不会阻塞浏览器构建DOM树- 但是会阻塞渲染,也就是在构建
render时,会等到css下载解析完毕后才进行(这点与浏览器优化有关,防止css规则不断改变,避免了重复的构建) - 有例外,
media query声明的CSS是不会阻塞渲染的
遇到 JS 脚本资源
- 阻塞浏览器的解析,也就是说发现一个外链脚本时,需等待脚本下载完成并执行后才会继续解析
HTML - 浏览器的优化,一般现代浏览器有优化,在脚本阻塞时,也会继续下载其它资源(当然有并发上限),但是虽然脚本可以并行下载,解析过程仍然是阻塞的,也就是说必须这个脚本执行完毕后才会接下来的解析,并行下载只是一种优化而已
defer与async,普通的脚本是会阻塞浏览器解析的,但是可以加上defer或async属性,这样脚本就变成异步了,可以等到解析完毕后再执行
defer 是延迟执行,而 async 是异步执行
async是异步执行,异步下载完毕后就会执行,不确保执行顺序,一定在onload前,但不确定在DOMContentLoaded事件的前或后defer是延迟执行,在浏览器看起来的效果像是将脚本放在了body后面一样(虽然按规范应该是在DOMContentLoaded事件前,但实际上不同浏览器的优化效果不一样,也有可能在它后面)
遇到 img 图片类资源
遇到图片等资源时,直接就是异步下载,不会阻塞解析,下载完毕后直接用图片替换原有 src 的地方
loaded 和 domcontentloaded
DOMContentLoaded事件触发时,仅当DOM加载完成,不包括样式表,图片(譬如如果有async加载的脚本就不一定完成)load事件触发时,页面上所有的DOM,样式表,脚本,图片都已经加载完成了