发布日期:2013 年 12 月 31 日
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),甚至可以从“none”替换其计算的显示样式属性改为“inline”。现在,我们的页面会显示“Hello interactive members!”。
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
默认情况下,JavaScript 执行会“阻止解析器”:当浏览器遇到文档中的脚本时,它必须暂停 DOM 构建,将控制权移交给 JavaScript 运行时,让脚本执行完毕,然后再继续构建 DOM。在之前的示例中,我们通过内联脚本看到了实际运用情况。事实上,内联脚本总是会阻止解析器,除非您编写额外代码来延迟它们的执行。
使用 script 标记引入的脚本又怎样?以前面的示例为例,将代码提取到单独的文件中:
<!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
属性添加到 <script>
元素:
<!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 构建,这样可以显著提高性能。