不要再问**“那怎么可能”,而是问“为什么不能”**
大家好,我是柒八九。
今天,我们来谈谈,浏览器的关键渲染路径。针对浏览器的一些其他文章,我们前面有介绍。分别从浏览器架构和最新的渲染引擎介绍了关于页面渲染的相关概念。对应连接如下。
而今天的主角是{关键渲染路径| Critical Rendering Path}。它是影响页面在加载阶段的主要标准。
这里再啰嗦一点,通常一个页面有三个阶段
- 加载阶段
- 是指从发出请求到渲染出完整页面的过程
- 影响到这个阶段的主要因素有网络和 JavaScript 脚本
- 交互阶段
- 主要是从页面加载完成到用户交互的整个过程
- 影响到这个阶段的主要因素是 JavaScript 脚本
- 关闭阶段
- 主要是用户发出关闭指令后页面所做的一些清理操作
好了,时间不早了。开干。
能所学到的知识点
- 关键渲染路径的各种指标
- {关键资源| Critical Resource}:所有可能阻碍页面渲染的资源
- {关键路径长度|Critical Path Length}:获取构建页面所需的所有关键资源所需的 RTT(Round Trip Time)
- {关键字节| Critical Bytes}:作为完成和构建页面的一部分而传输的字节总数。
- 重温HTTP缓存
- 针对关键渲染路径进行各种优化处理
- 针对
React
应用做优化处理
1. 加载阶段关键数据
{文档对象模型| Document Object Model}
DOM:是
HTML
页面在解析后,基于对象的表现形式。
DOM是一个应用编程接口(API),通过创建表示文档的树,以一种独立于平台和语言的方式访问和修改一个页面的内容和结构。
在 HTML
文档中,Web开发者可以使用JS
来CRUD DOM 结构,其主要的目的是动态改变HTML文档的结构。
DOM 将整个
HTML
页面抽象为一组分层节点
DOM 并非只能通过 JS 访问, 像{可伸缩矢量图| SVG}、{数学标记语言| MathML}和{同步多媒体集成语言| SMIL}都增加了该语言独有的 DOM
方法和接口。
一旦HTML被解析,就会建立一个DOM树。
下面的代码有三个区域:header
、main
和footer
。并且style.css
为外部文件。
<html> <head> <link rel="stylesheet" href="style.css"> <title>关键渲染路径示例</title> </head> <body> <header> <h1>...</h1> <p>...</p> </header> <main> <h1>...</h1> <p>...</p> </main> <footer> <small>...</small> </footer> </body> </html> 复制代码
当上述 HTML
代码被浏览器解析为 DOM树
状结构时,其各个节点的关系如下。
每个浏览器都需要一些时间解析HTML。并且,清晰的语义标记有助于减少浏览器解析HTML所需的时间。(不完整或者错误的语义标记,还需要浏览器根据上下文去分析和判断)
具体,浏览器是如何将HTML
字符串信息,转换成能够被JS操作的DOM
对象,不在此文的讨论范围内。不过,我们可以举一个很小的例子。在我们JS算法探险之栈(Stack)中,有一个题就是如何判断括号的正确性。
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。 有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
示例:
输入:s = "()[]{}" 输出:true
输入:s = "(]" 输出:false
其实,上面的例子就是最简单的一种标签匹配。或者说的稳妥点,它们的主要思想是一致的。
CSSOM Tree
CSSOM
也是一个基于对象的树。它负责处理与DOM树相关的样式。
承接上文,我们这里有和上面HTML
配套的CSS
样式。
header{ background-color: white; color: black; } p{ font-weight:400; } h1{ font-size:72px; } small{ text-align:left } 复制代码
对于上述CSS声明,CSSOM树
将显示如下。
由于,css
的部分属性能够被继承,所以,在父级节点定义的属性,如果满足情况,子节点也是会有对应的属性信息,最后将对应的样式信息,渲染到页面上。
一般来说,CSS被认为是一种{渲染阻断| Render-Blocking}资源。
什么是渲染阻断?渲染阻塞资源是一个组件,它将不允许浏览器渲染整个DOM树,直到给定的资源被完全加载。
CSS
是一种渲染阻断资源,因为在CSS完全加载之前,你无法渲染树。
起初,页面中所有CSS
信息都被存放在一个文件中 。现在,开发人员通过一些技术手段,能够将CSS
文件分割开来,只在渲染的早期阶段提供关键样式。
执行JS
先将一个小知识点,其实,在前面的文章中,我们已经讲过了。这里,我们再啰嗦一遍。
在浏览器环境下,JS = ECMAScript + DOM + BOM
。
ECMAScript
JS的核心部分,即 ECMA-262 定义的语言,并不局限于 Web 浏览器。
Web
浏览器只是 ECMAScript
实现可能存在的一种{宿主环境| Host Environment}。而宿主环境提供 ECMAScript
的基准实现和与环境自身交互必需的扩展。(比如 DOM
使用 ECMAScript
核心类型和语法,提供特定于环境的额外功能)。
像我们比较常见的Web 浏览器、 Node.js和已经被淘汰的 Adobe Flash都是ECMA
的宿主环境。
ECMAScript
只是对实现ECMA-262规范的一门语言的称呼, JS
实现了 ECMAScript
,Adobe ActionScript
也实现 ECMAScript
。
上面的内容只是做一个知识点的补充,我们这篇文章中出现的JS
还是一般意义上的含义:即javascript
文本信息。
JavaScript
是一种用来操作DOM
的语言。这些操作花费时间,并增加网站的整体加载时间。所有,
JavaScript
代码被称为 {解析器阻塞| Parser Blocking}资源。
什么是解析器阻塞?当需要下载和执行JavaScript
代码时,浏览器会暂停执行和构建DOM树。当JavaScript代码被执行完后,DOM树的构建才继续进行。
所以才有, JavaScript
是一种昂贵的资源的说法。
示例演示
下面是一段HTML
代码的演示结果,显示了一些文字和图片。正如你所看到的,整个页面的显示只花了大约40ms。即使有一张图片,页面显示的时间也更短。这是因为在进行第一次绘制时,图像没有被当作关键资源。
记住,
{关键渲染路径| Critical Rendering Path}都是关于
HTML
、CSS
和Javascript
的
现在,在这段代码中添加css
。正如下图所示,一个额外的请求被触发了。尽管加载html
文件的时间减少了,但处理和显示页面的总体时间却增加了近10倍。为什么呢?
- 普通的
HTML
并不涉及太多的资源获取和解析工作。但是,对于CSS文件,必须构建一个CSSOM。HTML
的DOM
和CSS
的CSSOM
都必须被构建。这无疑是一个耗时的过程。 JavaScript
很有可能会查询CSSOM
。这意味着,在执行任何JavaScript之前,CSS文件必须被完全下载和解析。
注意:domContentLoaded
在HTML DOM
被完全解析和加载时被触发。该事件不会等待image
、子frame
甚至是样式表被完全加载。唯一的目标是文档被加载。可以在window
中添加事件,以查看DOM是否被解析和加载。
window.addEventListener('DOMContentLoaded', (event) => { console.log('DOM被解析且加载成功'); }); 复制代码
即使你选择用内联脚本取代外部文件,性能也不会有大的改变。主要是因为需要构建CSSOM
。如果你考虑使用外部脚本,可以添加 async
属性。这将解除对解析器的阻断。
关键路径相关术语
- {关键资源| Critical Resource}:所有可能阻碍页面渲染的资源
- {关键路径长度|Critical Path Length}:获取构建页面所需的所有关键资源所需的RTT(Round Trip Time)
- 当使用
TCP
协议传输一个文件时,由于TCP
的特性,这个数据并不是一次传输到服务端的,而是需要拆分成一个个数据包来回多次进行传输的 RTT
就是这里的往返时延
- 它是网络中一个重要的性能指标表示从发送端发送数据开始,到发送端收到来自接收端的确认,总共经历的时延
- 通常 1 个
HTTP
的数据包在14KB
左右
- 首先是请求
HTML
资源,假设大小是6KB
,小于14KB
,所以 1 个 RTT 就可以解决
- 至于
JavaScript
和CSS
文件
- 由于渲染引擎有一个预解析的线程,在接收到
HTML
数据之后,预解析线程会快速扫描HTML
数据中的关键资源,一旦扫描到了,会立马发起请求 - 可以认为
JavaScript
和CSS
是同时发起请求的,所以它们的请求是重叠的,计算它们的 RTT 时,只需要计算体积最大的那个数据就可以了
- {关键字节| Critical Bytes}:作为完成和构建页面的一部分而传输的字节总数。
在我们的第一个例子中,如果是普通的HTML脚本,上面各个指标的值如下
- 1个关键资源(
html
) - 1个RTT
- 192字节的数据
在第二个例子中,一个普通的HTML和外部CSS脚本,上面各个指标的值如下
- 2个关键资源(
html
+css
) - 2个RTT
- 400字节的数据
如果你希望优化任何框架中的关键渲染路径,你需要在上述指标上下功夫并加以改进。
- 优化关键资源
- 将
JavaScript
和CSS
改成内联的形式 (性能提升不是很大)- 如果
JavaScript
代码没有DOM
或者CSSOM
的操作,则可以改成sync
或者defer
属性- 首屏内容可以优先加载,非首屏内容采用滚动加载
- 优化关键路径长度
- 压缩
CSS
和JavaScript
资源- 移除
HTML
、CSS
、JavaScript
文件中一些注释内容
- 优化关键字节
- 通过减少关键资源的个数和减少关键资源的大小搭配来实现
- 使用
CDN
来减少每次RTT
时长
减少渲染器阻塞资源
懒加载
加载的关键是 "懒加载"。任何媒体资源、CSS
、JavaScript
、图像、甚至HTML
都可以被懒加载。每次加载有限的页面的内容,可以提高关键渲染路径。
- 不要在加载页面时加载这个整个页面的
CSS
、JavaScript
和HTML
。 - 相反,可以为一个
button
添加一个事件监听,只有在用户点击按钮时才加载脚本。 - 使用
Webpack
来完成懒加载功能。
这里有一些利用纯JavaScript实现懒加载的技术。
比如,现在又一个/