使用 JavaScript 添加交互性

发布时间: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),甚至将计算出的 display 样式属性从“none”覆盖为“inline”。现在我们的页面显示“Hello interactive students!”。

  • JavaScript 还允许我们在 DOM 中创建、设置样式、附加和删除新元素。从技术上讲,我们的整个页面可能只是一个大的 JavaScript 文件,它逐个创建和设置元素的样式。虽然这可行,但在实践中使用 HTML 和 CSS 更容易。在 JavaScript 函数的第二部分中,我们创建一个新的 div 元素,设置其文本内容,设置其样式,并将其附加到 body。

A preview of a page rendered on a mobile device.

这样,我们就修改了现有 DOM 节点的内容和 CSS 样式,并在文档中添加了一个全新的节点。我们的页面不会赢得任何设计奖项,但这说明了 JavaScript 为我们提供的强大功能和灵活性。

但是,虽然 JavaScript 为我们提供了强大的功能,但它也对页面呈现的方式和时间施加了许多额外的限制。

首先,请注意,在前面的示例中,我们的内联脚本位于页面的底部附近。为什么?好吧,您应该自己尝试一下,但是如果我们将脚本移动到 <span> 元素之上,您会注意到脚本失败并抱怨它无法在文档中找到对任何 <span> 元素的引用;也就是说,getElementsByTagName('span') 返回 null。这证明了一个重要的属性:我们的脚本在插入到文档的确切位置执行。当 HTML 解析器遇到 script 标签时,它会暂停其构建 DOM 的过程,并将控制权交给 JavaScript 引擎;在 JavaScript 引擎完成运行后,浏览器会从中断的地方继续,并恢复 DOM 构建。

换句话说,我们的脚本块找不到页面后面任何元素,因为它们尚未被处理!或者,换句话说:执行我们的内联脚本会阻塞 DOM 构建,这也会延迟初始渲染。

将脚本引入到页面的另一个微妙特性是,它们不仅可以读取和修改 DOM,还可以读取和修改 CSSOM 属性。事实上,这正是我们在示例中所做的,当我们更改 span 元素的 display 属性从 none 到 inline 时。最终结果?我们现在遇到了竞争条件。

如果我们想运行脚本时,浏览器尚未完成下载和构建 CSSOM 怎么办?对于性能而言,答案不是很好:浏览器会延迟脚本执行和 DOM 构建,直到它完成下载和构建 CSSOM。

简而言之,JavaScript 在 DOM、CSSOM 和 JavaScript 执行之间引入了许多新的依赖关系。这可能会导致浏览器在处理和渲染屏幕上的页面时出现明显的延迟

  • 脚本在文档中的位置非常重要。
  • 当浏览器遇到 script 标签时,DOM 构建会暂停,直到脚本执行完成。
  • JavaScript 可以查询和修改 DOM 和 CSSOM。
  • JavaScript 执行会暂停,直到 CSSOM 准备就绪。

在很大程度上,“优化关键渲染路径”是指理解和优化 HTML、CSS 和 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>

试用

在 script 标签中添加 async 关键字会告诉浏览器在等待脚本可用时不要阻塞 DOM 构建,这可以显着提高性能。

反馈