JavaScript 允许我们修改网页的方方面面:内容、样式及其对用户互动的响应。不过,JavaScript 也会阻止 DOM 构建和延迟网页渲染。为了实现最佳性能,请让您的 JavaScript 异步执行,并去除关键渲染路径中任何不必要的 JavaScript。
摘要
- JavaScript 可以查询和修改 DOM 和 CSSOM。
- JavaScript 执行因 CSSOM 而阻止。
- 除非明确声明为异步,否则 JavaScript 会阻止 DOM 构建。
JavaScript 是一种在浏览器中运行的动态语言,它允许我们更改网页行为的几乎所有方面:我们可以通过在 DOM 树中添加和移除元素来修改内容;我们可以修改每个元素的 CSSOM 属性;我们可以处理用户输入,等等。为进行说明,让我们用一个简单的内联脚本对之前的“Hello World”示例进行扩充:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
<title>Critical Path: Script</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script>
var span = document.getElementsByTagName('span')[0];
span.textContent = 'interactive'; // change DOM text content
span.style.display = 'inline'; // change CSSOM property
// create a new element, style it, and append it to the DOM
var loadTime = document.createElement('div');
loadTime.textContent = 'You loaded this page on: ' + new Date();
loadTime.style.color = 'blue';
document.body.appendChild(loadTime);
</script>
</body>
</html>
JavaScript 允许我们访问 DOM 并拉取对隐藏 span 节点的引用;该节点可能在渲染树中不可见,但仍存在于 DOM 中。然后,当我们获得引用后,就可以更改其文本(通过 .textContent),甚至可以将其计算出的 display 样式属性从“none”替换为“inline”。现在,我们的页面显示“Hello interactivestudent!”。
JavaScript 还允许我们在 DOM 中创建、样式化、附加和移除新元素。从技术上讲,我们的整个页面可以是一个大的 JavaScript 文件,可用于逐一创建各个元素并为其设置样式。虽然可以,但在实践中,使用 HTML 和 CSS 要容易得多。在 JavaScript 函数的第二部分,我们创建一个新的 div 元素,设置其文本内容,设置其样式,然后将其附加到正文。
因此,我们修改了现有 DOM 节点的内容和 CSS 样式,并向文档中添加了一个全新的节点。我们的网页不会赢得任何设计奖,但却说明了 JavaScript 赋予我们的强大力量和灵活性。
虽然 JavaScript 赋予我们大量功能,但它在如何以及何时渲染网页时造成了许多其他限制。
首先,请注意上例中的内联脚本靠近页面底部。原因何在?您应该亲自尝试一下,但如果我们将脚本移至 span 元素上方,您会注意到该脚本运行失败,并提示无法在文档中找到对任何 span 元素的引用;也就是说,getElementsByTagName(‘span') 会返回 null。这展示了一个重要属性:我们的脚本在文档中插入该脚本的确切位置执行。当 HTML 解析器遇到一个脚本标记时,它会暂停构建 DOM 的过程,并将控制权移交给 JavaScript 引擎;在 JavaScript 引擎运行完毕后,浏览器会从上次停止的位置继续进行 DOM 构建。
也就是说,我们的脚本块在网页中的后半部分找不到任何元素,因为它们尚未进行处理!或者,稍微换个说法:执行我们的内联脚本会阻止 DOM 构建,进而延迟首次渲染。
在网页中引入脚本的另一个细微属性是,它们不仅可以读取和修改 DOM 属性,还可以读取和修改 CSSOM 属性。事实上,这正是我们在示例中将 span 元素的 display 属性从 none 更改为 inline 时的做法。最终效果如何?我们现在遇到了竞态条件。
如果当我们想运行脚本时,浏览器还没有完成 CSSOM 的下载和构建,该怎么办?答案很简单,对性能不太好:浏览器会延迟脚本执行和 DOM 构建,直到完成 CSSOM 的下载和构建为止。
简而言之,JavaScript 在 DOM、CSSOM 和 JavaScript 执行之间引入了许多新的依赖关系。这可能会导致浏览器在处理网页以及在屏幕上显示网页时出现严重延迟:
- 脚本在文档中的位置非常重要。
- 当浏览器遇到脚本标记时,DOM 构建将会暂停,直到脚本执行完毕。
- JavaScript 可以查询和修改 DOM 和 CSSOM。
- JavaScript 执行会暂停,直到 CSSOM 准备就绪。
“优化关键渲染路径”在很大程度上是指了解和优化 HTML、CSS 和 JavaScript 之间的依赖关系图。
解析器阻止与异步 JavaScript
默认情况下,JavaScript 执行会“阻止解析器”:当浏览器遇到文档中的脚本时,它必须暂停 DOM 构建,将控制权移交给 JavaScript 运行时,让脚本先执行,然后再继续构建 DOM。我们在之前的示例中见证了内联脚本的实际应用。实际上,内联脚本始终会阻止解析器,除非您编写额外的代码来延迟其执行。
通过脚本标记包含的脚本怎么样?我们来看一下前面的示例,并将代码提取到一个单独的文件中:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
<title>Critical Path: Script External</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script src="app.js"></script>
</body>
</html>
app.js
var span = document.getElementsByTagName('span')[0];
span.textContent = 'interactive'; // change DOM text content
span.style.display = 'inline'; // change CSSOM property
// create a new element, style it, and append it to the DOM
var loadTime = document.createElement('div');
loadTime.textContent = 'You loaded this page on: ' + new Date();
loadTime.style.color = 'blue';
document.body.appendChild(loadTime);
无论我们使用 <script> 标记还是内嵌 JavaScript 代码段,您希望两者的运作方式相同。在这两种情况下,浏览器都会在处理文档的其余部分之前暂停并执行脚本。但是,对于外部 JavaScript 文件,浏览器必须暂停以等待从磁盘、缓存或远程服务器提取脚本,这可能会使关键渲染路径增加数万毫秒的延迟。
默认情况下,所有 JavaScript 都会阻止解析器。由于浏览器不知道脚本计划在网页上执行什么操作,它会假定最糟糕的情况并阻止解析器。向浏览器表明,脚本不需要在引用它的确切位置执行,浏览器就可以继续构建 DOM,并在脚本就绪时(例如,从缓存或远程服务器获取文件之后)执行脚本。
为此,我们将脚本标记为 async:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
<title>Critical Path: Script Async</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script src="app.js" async></script>
</body>
</html>
将 async 关键字添加到脚本标记中可指示浏览器在等待脚本可用期间不要阻止 DOM 构建,这样可以显著提升性能。