从输入 URL 到页面加载的全过程参考 计算机网络系列 -- 从URL到网页加载
这里主要讲解页面渲染部分的过程
渲染流程概述
浏览器从最初接收请求来的HTML、CSS、javascript等`二进制形式`的资源,然后`解析、构建树、渲染布局、绘制`
最后呈现给用户能看到的界面的整个过程
渲染引擎
一个渲染引擎主要包括:HTML解析器,CSS解析器,javascript引擎,布局layout模块,绘图模块
- HTML解析器:解释HTML文档的解析器,主要作用是将HTML文本解释成DOM树。
- CSS解析器:它的作用是为DOM中的各个元素对象计算出样式信息,为布局提供基础设施
- Javascript引擎:使用Javascript代码可以修改网页的内容,也能修改css的信息,javascript引擎能够解释javascript代码,并通过DOM接口和CSS树接口来修改网页内容和样式信息,从而改变渲染的结果。
- 布局(layout):在DOM创建之后,Webkit需要将其中的元素对象同样式信息结合起来,计算他们的大小位置等布局信息,形成一个能表达这所有信息的内部表示模型
- 绘图模块(paint):使用图形库将布局计算后的各个网页的节点绘制成图像结果
渲染整体过程
此段内容参考 深入剖析:Vue核心之虚拟DOM
- 构建 DOM 树:用 HTML 分析器,解析 HTML 元素,构建一棵 DOM 树;
- 构建 CSSOM 树:用 CSS 分析器,分析 CSS 文件和元素上的 inline 样式,构建出一棵 CSSOM 树;
- 若在构建 DOM 树的过程中,当 HTML 解析器遇到一个
script标记时,即遇到了 JS,将立即阻塞 DOM 树的构建,将控制权移交给 JS 引擎,等到 JS 引擎运行完毕,浏览器才会从中断的地方恢复 DOM 树的构建;
其根本原因在于,JS会对 DOM 节点进行操作,浏览器
无法预测未来的 DOM 节点的会被 JS 怎么样去修改,所以为了防止无效操作,节省资源,只能阻塞DOM树的构建。例如:若 JS 删除了某个 DOM 节点 A,那么浏览器为构建此节点 A 花费的资源是无效的。
- 构建 Render 树:将 DOM 树和 CSSOM 树关联起来,构建一棵 Render 树;
每个 DOM 节点都有 attach 方法,接受样式信息,返回一个 render 对象,这些 render 对象最终会被构建成一棵 Render 树;
- 确定节点坐标:根据 Render 树结构,为每个 Render 树上的节点计算确定一个在显示屏上出现的精确坐标;
- 绘制页面:根据 Render 树和节点显示坐标,然后调用每个节点的 paint 方法,将它们绘制出来。
注意事项
- 在一个 HTML 文档里面,浏览器渲染引擎会
看标签办事
1. 当这些节点是`普通节点`的话,HTML解析器就会将这些节点`加入到DOM树中`
2. 当这些节点是`JS代码`的话,HTML解析器就会将控制权`交给JS引擎`
3. 如果这些节点是`CSS代码`的话,HTML解析器就会将控制权`交给CSS解析器`
- 为了尽可能避免 JS 阻塞渲染,我们通常采取以下两种措施:
- 将 JS 文件放到 HTML
底部进行加载 - 对 JS 文件执行
async或defer加载。
- 上面的步骤中的每一步(除了第 3 步的 JS 阻塞渲染外)都不是说必须得上一步完成后才能继续往下一步走,就是说浏览器会一边解析 HTML、一边解析 CSS 、一边构建 render 树、一边进行计算和绘制。都是为了尽快将内容显示在屏幕上,以达到更好的用户体验
- 浏览器甚至还会在没有完全接收 HTML 文件时便开始渲染、显示网页,即
边接收边渲染;在执行 HTML 中代码时,根据需要,浏览器会继续请求图片、CSS、JavsScript等文件,过程同请求 HTML
渲染具体过程
1. 构建DOM树
当浏览器接收到服务器响应来的HTML文档后,会遍历文档节点,HTML解析器开始工作,构建DOM树
浏览器接收到服务器响应来的HTML文档是二进制字节流的形式
过程分析
- Bytes:当服务器返回一个HTML文件给浏览器时,浏览器接受到的是
一些字节数据 - Characters:浏览器根据HTTP响应中的编码方式(通常是utf-8),解析字节数据,
得到HTML文档 - Tokens:浏览器根据DTD中的对元素的定义,对接受到的字符进行
语义化(tokens) - Nodes:浏览器使用这些语义块(tokens)创建对象(Nodes)
- DOM:HTML解析器构建当前节点的所有子节点,再去构建当前节点的下一个兄弟节点
1.由于CSS解析器、JS解析器的优先级大,所以DOM树会暂时停止构建,因而造成`白屏`,就是所谓`浏览器阻塞`
2.不过,当外联的JS代码和CSS代码还没从服务器传到浏览器的时候,如果此时`DOM树上有可视元素`的话
浏览器通常会将一些内容`提前渲染`到屏幕上来
阻塞渲染问题参考 浏览器系列 -- 阻塞渲染
- 当HTML解析器读到最后一个节点的时候,整个DOM树也构建完成了,这个时候就会触发【domContentloaded】事件。至此,DOM树就全部构建完成了。
1. `domContentloaded`事件触发时:仅当DOM加载完成,`不包括样式表,图片等`
2. `load`事件触发时:页面上`所有的DOM、样式表、脚本、图片都已加载完成`
注意
1. DOM树在构建的过程中可能会因为`CSS和JS的加载`而`被阻塞`
2. `display:none`的元素也会在DOM树中
3. `注释`也会在DOM树中
4. `script标签`会在DOM树中
2. 构建CSSOM规则树
当获取style标签内的css、或者内嵌的css,浏览器会发送请求获得该标签中标记的CSS
当渲染引擎接收到 CSS 二进制文本后,会经过一系列的转换,最终变成浏览器可以理解的styleSheets
CSSOM规则树的构建也是要经过 Bytes→characters→tokens→nodes→object model 的过程
CSS 解析是自右至左读取的,所以 右边 的选择器的属性
会覆盖左边 的选择器的属性,同时我们可以得知:嵌套太多层会影响性能
注意
1. CSS解析可以与DOM解析`同时进行`
2. CSS解析与script的`执行互斥`
3. 在Webkit内核中进行了script执行优化,`只有在JS访问CSS时`才会发生互斥
分析
2. CSSOM规则未构建好`会阻塞`渲染树的构建(即CSS阻塞渲染)
3. JS代码的执行会阻塞DOM解析,而如果JS代码访问了CSS,则CSS解析优先于JS代码优先于DOM树的构建
(联系到重绘、回流和浏览器阻塞渲染)
阻塞渲染问题参考 浏览器系列 -- 阻塞渲染
3. 构建渲染树
浏览器会先从DOM树的根节点开始遍历每个可见节点,然后对每个可见节点找到适配的CSS样式规则并应用
注意:渲染树和DOM 树【不完全对应】
因为
1. 【display: none】【meta】【script】【link】的元素【不在】Render Tree中
2. 但【visibility: hidden】的元素【在】Render Tree中
渲染树生成后,还是没有办法渲染到屏幕上,渲染到屏幕需要得到各个节点的位置信息,这就需要布局(Layout)的处理了
4. 渲染树布局(layout of the render tree)
根据生成的渲染树,从渲染树的根节点开始遍历,计算每个节点的几何信息(在设备视口内的确切位置和大小)
布局阶段的输出就是常说的盒子模型,它会精确地捕获每个元素在屏幕内的确切位置与大小
注意
1. 【float元素】,【absoulte元素】,【fixed元素】会发生位置偏移。
2. 【脱离文档流】,其实就是【脱离Render Tree】
5. 渲染树绘制(Painting the render tree)
经过生成的渲染树和Layout阶段,得到了所有可见节点具体的几何信息与样式,然后将渲染树的每个节点转换成【屏幕上的实际像素】。渲染树的绘制工作是【由浏览器的UI后端组件】完成的
重绘与回流
我们都知道HTML默认是流式布局的,但CSS和JS会打破这种布局,尤其是用户与网页交互时,【改变DOM的外观样式以及大小和位置】。因此我们就需要知道两个概念:repaint 和 reflow
重绘 repaint (操作一个DOM节点不会影响其他节点)
在布局过程中,遇到style属性或js操作,改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性时,被改变元素本身要重画,但是元素的几何尺寸、元素以及其他元素的位置没有发生改变
回流 reflow (操作一个DOM节点会影响其他节点)
布局过程中,遇到style属性或js操作,对元素的大小、定位等有改变,需要重新计算和绘制所有的结点几何尺寸和位置,则会引起回流。
值得注意的是:比如修改了元素的样式,浏览器【并不会立刻】reflow或repaint一次
因为有【flush队列】让多次回流重绘【变成一次】回流重绘
触发重绘的原因
除了元素的尺寸和位置变化的变化:
背景色、边框颜色、文字颜色、字体改变
触发回流的原因
- 页面第一次渲染(初始化)
- DOM树变化(如:增删节点)
- padding、margin改变
- 元素的尺寸大小、包括文字大小改变
- 浏览器窗口resize
- 获取元素的某些属性
减少reflow、repaint触发次数
CSS方面 —— (多半是style属性引起的:把样式放在style标签或link标签)
- 避免设置多层内联样式,将样式
合并在一个外部类
// 内联样式
<div style="width: 65px;height: 20px;border: 1px solid;">元素</div>
// 外部类
<head>
<meta charset="utf-8" />
<style type="text/css">
div {
width: 65px;
height: 20px;
border: 1px solid;
}
</style>
</head>
- 将需要多次回流的元素position属性设为
absolute或fixed(比如动画效果)
position属性设为absolute或fixed意味着脱离文档流,因而元素样式发生改变时不会引起回流重绘
JS方面 —— (操作DOM节点时引起的:重点减少次数)
- 避免多次读取 DOM 节点
可以用变量将结果存起来 - 需要多次操作同一 DOM 节点时可以先将该节点的元素设置
display:none,操作完再显示。(因为隐藏元素不在render树内,因此修改隐藏元素不会触发回流重绘) - 操作多个 DOM 节点时将所有的 DOM 操作
集中在一起(原因如下)
a. 浏览器会维护1个`队列`,把所有会引起回流、重绘的操作放入这个队列
b. 等队列中的操作到了`一定的数量`或者到了`一定的时间间隔`,浏览器就会`flush队列`,进行一个批处理
c. 这样就会让多次的回流、重绘`变成一次`回流重绘
d. 有特殊情况,会`提前执行`flush队列
需要获取这些值时:
width、height;
clientWidth、clientHeight、clientTop、clientLeft;(clientWidth = width+左右padding)
offsetWidth、offsetHeight、offsetTop、offsetLeft;(offsetWidth = width+左右padding+左右boder)
scrollWidth、scrollHeight、scrollTop、scrollLeft;
4.形变和位移用transfrom代替 DOM 节点操作
附加问题
display:none和visibility: hidden的区别
相同点:控制元素的显示与隐藏
不同点:
- 意义上:display:none表示元素不要了,visibility: hidden表示元素看不见
- 原理上:display:none的元素不在渲染树上,visibility: hidden的元素在渲染树上
注意:display:none的元素虽然不在渲染树上,但会在 DOM 树上
- 影响上:设置元素为display:none时会引起回流,设置元素为visibility: hidden会引起重绘
注意:display:none的元素是操作 DOM 节点,使得该元素所在节点去掉,所以需要回流
回流 reflow 和重绘 repaint 的联系与区别
联系:回流 reflow 必定引起重绘 repaint
区别:
- 本质上
- 触发原因上
- 过程上
如何减少reflow、repaint触发次数
CSS 方面
外部类代替多个内联样式- 设置
position脱离文档流
JS 方面
- 多次读取 DOM 节点时
变量来存 - 多次操作同一 DOM 节点时
先设置display:none - 同时操作多个 DOM 节点时
集中,有flush队列 - 形变和位移用
transfrom代替 DOM 节点操作
如何优化渲染效率
优化方案见 浏览器系列 -- 优化页面加载