渲染页面:浏览器的工作原理
大部分情况下,浏览器是单线程执行的.为了有流畅的交互,需要尽可能快的发送请求,减少网络等待时间.Web性能优化则需要尽可能快的
页面资源的访问
DNS 查找
- 缓存
- 递归
- 权威
- DNS 查找的问题
- 移动端距离
- TCP 协议
- TLS 加密传输协议
8次往返后浏览器才发出请求
服务端响应
与 web 服务器建立连接后, 浏览器才发送初始的 HTTP GET 请求获取页面资源
响应
- 一旦服务器收到请求,使用相关的 响应头 和 HTML 回复
- 初始请求的响应包含所接收数据的第一个字节
- “Time to First Byte” (TTFB) 是进行请求到收到第一个包之间的时间
- 第一块 内容通常是14kb
TCP 慢开始 / 14kb 规则
- 第一个响应包是 14kb 大小
- 慢开始是一种均衡网络连接的速度的算法,逐渐增减发送数据的大小,直到最大网络带宽
- 在收到 ACK 后 会加倍 包的大小 (直到阈值 或 遇到拥塞)
这就是为什么web性能优化需要将此初始14Kb响应作为优化重点的原因
拥塞控制是作用于网络,它是防止过多的包被发送到网络中,避免出现网络负载过大,网络拥塞的情况。
- 避免发送包速率过快导致,客户端丢弃数据包
- 包丢弃意味着,将不会有确认帧的返回。服务器把它们当做确认帧丢失。拥塞控制算法使用这个发送包和确认帧流来确定发送速率
- 拥塞控制算法 - 拥塞状态机 - 各状态下的发包策略及状态间的变更条件
延伸: 移动端的拥塞控制算法Verus
解析
一旦浏览器收到数据的第一块,就可以开始解析收到的信息.即使请求页面的HTML大于初始的14kb数据包. 推测性解析,解析 是浏览器将数据转为DOM 和 CSSOM的步骤,解析后通过渲染器把 DOM+CSSOM在屏幕上绘制.
这就是为什么前14kb 最好包含浏览器开始渲染页面所需要的所有内容,或至少包含页面模版(第一次渲染所需的CSS和HTML).但在渲染到屏幕上面之前,HTML,CSS,JavaScript必须被解析完成
构建DOM树
第一步:处理HTML标记并构造DOM树.HTML解析涉及到DOMTokenization 和 DOM 树的构造.DOM 格式良好 会解析的简单而快速.DOM节点数量越多,构建DOM树所需的时间越长
- 当解析器发现非阻塞资源,例如一张图片,浏览器会请求这些资源并且继续解析.当遇到CSS文件时,也可以继续进行
- 阻塞资源
<script>
标签 (特别是没有async 或者 defer 属性)则会阻塞渲染并停止HTML的解析. - 所以过多脚本是一个重要的性能瓶颈
预加载扫描器
- 浏览器构建DOM树,占用了主线程.
- 预加载扫描器将解析可用的内容并请求高优先级资源,如 CSS,JavaScropt 和 web Fonts.
- 不必等到解析起找到对外部资源的引用来请求他.
- 减少了阻塞,优化渲染速度
当主线程在解析HTML和CSS时,预加载扫描器将找到脚本和图像,并开始下载它们。为了确保脚本不会阻塞进程,当JavaScript解析和执行顺序不重要时,可以添加async属性或defer属性。
等待获取CSS不会阻塞HTML的解析或者下载,但是它的确阻塞JavaScript,因为JavaScript经常用于查询元素的CSS属性。
构建 CSSOM 树
第二步: 处理CSS并构建CSSOM树.
- CSS对象模型和DOM是相似的.
- CSSOM 和 DOM 是两棵树,有独立的数据结构
- 浏览器将CSS规则转换为可以理解和使用的样式映射.
- 浏览器便利CSS中的规则集,根据CSS选择器创建具有父/子/兄弟关系的节点树
- 浏览器构建CSSOM 相比于DOM ,规则更具体,级联属性强,非常快 ,小于一次DNS查找所需的时间
其他过程
- JavaScript 编译
- 当CSS被解析并创建CSSOM时,其他资源正在下载(preload scanner 预加载扫描器).随后JavaScript被解析,编译,执行.
- 脚本被解析为抽象语法树.
- 浏览器引擎使用抽象语法树,并将其传递到解释器中,输出在主线程上执行的字节码,这就是所谓的JavaScript编译
- 构建辅助功能树
- 用于可访问对象模型 (AOM),类似于 DOM的语义版本
- 当 DOM更新时,浏览器会更新辅助功能树
- 辅助功能树无法修改可访问性树
- 构建AOM之前,屏幕阅读器(scree readers) 无法访问内容
渲染
渲染包括 布局,样式,绘制,在某些情况下还包含合成.
- 在解析步骤中创建的CSSOM树 和DOM树 组合成一个Render树
- 用于计算每个可见元素的布局,然后将其绘制到屏幕上
- 在某些情况下,可以将内容提升到它们自己的层,并进行合成,通过GPU来绘制屏幕的一部分提高性能,释放主线程
Style
第三步: 将DOM树 和 CSSOM树组合成Render树,计算样式树或渲染树丛 DOM树的根开始构建,遍历每个可见节点
- 像
<head>
标签和它的字节点 - 以及任何具有
{ display: none;}
样式的节点 - 这些标签都不会显示,也就是不会出现在Render树上.
- 但 具有
visibility: hidden
的节点会出现在Render树上,因此它们会占用空间 <script>
节点也不会包含在Render树中
Render树保存所有具有内容和计算样式的可见节点——将所有相关样式匹配到DOM树中的每个可见节点,并根据CSS级联确定每个节点的计算样式。
Layout
第四步: 在Render树上执行布局处理逻辑,计算出每个节点的几何体.布局处理逻辑是确定呈现树中所有节点的宽度,高度和位置,以及确定页面上每个对象的大小和位置的过程
- 构建渲染树后,开始布局
- 渲染树标识显示哪些节点(即使不可见)及其计算样式.但不标识每个节点的尺寸或位置
- 为了确定每个对象的确切大小和位置,浏览区从渲染树的根开始遍历它
- 首先确定尺寸,不同节点 类似不同盒子
- 按照视区大小为基础,布局从body开始,用每个元素的框模型属性排列所有body的子孙元素的尺寸,为无法确定尺寸的元素(例如未设置大小的img标签)提供占位符空间
- 第一次确定节点的大小和位置称之为布局.
- 随后对节点大小和位置的重新计算称为回流.(img标签)
Paint
最后一步: 将各个节点绘制到屏幕上,第一次出现的节点称为
first_meaningful_paint.
在绘制或光栅格化阶段,浏览器将在布局阶段计算的每一个框转换为屏幕上的实际像素(所以浏览器可以有绘制过程的展示设置-Show paint rectangles)
- 绘制包括将元素的每个可视部分会知道屏幕上,包括
颜色,文本,边框,阴影和提花呢元素(按钮/图像) - 为了确保平滑滚动和动画,占据主线程的所有内容,包括样式计算,以及回流和绘制,必须让浏览器在16.67毫米内完成.
- 为了确保重绘的速度比初始绘制速度更快,屏幕上的绘图通常被分解成数层.如果发生这种情况,则需要进行合成.
- 绘制可以将DOM树上的元素分解为多个层.将内容提升到GPU上的层(而不是CPU上的主线程),可以提高绘制和重绘的性能.
- 有一些特定的属性和元素可以实例化一个层,包括
<video>
和<canvas>
,任何CSS属性为opacity
, 3D转换,will-change
的元素,还有一些其他元素. - 这些节点将于子节点一起绘制到他们自己的层上,除非子节点由于上述一个或者多个原因需要自己的层
- 层绘制 确实可以提高性能,但是它以内存管理为代价,因此不作为web性能优化策略而过度使用的一部分
Compositing
当DOM树各个部分以不同的层绘制,相互重叠时,必须进行合成,以确保它们以正确的顺序绘制到屏幕上,并正确显示内容.
当页面继续加载资源时,可能会发生回流(类似图片加载),回流会出发重新绘制和重新组合.如果我们定义了图像的大小,只需重新绘制需要更新的层,并在必要的时候进行合成.
但没有设置大小则会在获取图像后,渲染过程将返回到布局步骤并从那里开始重新开始(回流)
交互
主线程绘制完页面后,并没有完全准备好
如果加载了 JavaScript ,并且延迟到onload 事件触发后执行,则主线程可能很忙,无法用于滚动,触摸和其他交互
TTI (Time to Interactive) 是测量第一个请求从DNS查询和SSL链接到页面可交互时所花费的时间
可交互是 “First Contentful Paint”之后的时间点. 页面可在50ms内响应用户的交互
如果主线程正在解析,编译和执行JavaScript,则它不可用,因此无法及时(小于50ms) 响应用户请求
现象: 用户可以非常快地看到页面,但是在下载、解析和执行脚本之前,就无法滚动。主线程在这段时间内完全被占用,对单击事件或屏幕点击没有响应。
实验观察
- 本地页面文件,浏览器打开
- 控制台设置加载资源的速度
- 验证页面资源加载对页面绘制的影响
- CSS 加载对 HTML/JS 的影响
- 打开 Rendering -> Paint flashing 绘制布局
- 打开 Show overview / Capture screenshots 绘制过程
引用