原文: JavaScript Performance – How to Improve Page Speed with async and defer
在 web 开发中,JavaScript 给你的网页带来了交互性和动态效果。虽然 HTML 和 CSS 负责网页的结构和美学,但如果没有 JavaScript 在背后工作,它们将只停留在“可用”的层面。
你可以通过几种方式在 HTML 文件中引入 JavaScript 代码。最标准的方法是将 JavaScript 代码放在一个以 .js
为扩展名的单独文件中,然后将该文件加载到 HTML 文件中,下载并执行 JavaScript 代码。
在这篇文章中,你将学习将 JavaScript 文件加载到 HTML 中的最有效方法,以提高应用程序的页面加载速度。我们将深入浅出地了解两个特殊的 HTML 属性async
和defer
,以及它们如何提高页面加载速度。
如果你也喜欢从视频内容中学习,这篇文章也提供视频教程。🙂
我们如何将脚本加载到 HTML 中?
让我们先了解一下从外部文件加载 JavaScript 代码的基本原理。假设我们有一个叫 some-script.js
的文件(注意文件扩展名,是 .js
,JavaScript 的缩写),里面全是 JavaScript 代码。
你需要使用 <script>
标签来加载和执行这些代码。<script>
标签中的 src
属性写上你要加载的 JavaScript 文件。
<script src="some-script.js"></script>
最后,你需要确保将 <script>
标签放在 HTML 文件的 <head>
标签内或 <body>
标签的后面。
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Some Title</title>
<link rel="stylesheet" href="./styles/main.css" />
<script src="some-script.js"></script>
</head>
<body></body>
</html>
在 <head>
或 <body>
标签内指定 <script>
标签有不同的结果。我们将很快了解它们。
为什么要关心脚本加载?
如果你的应用程序是一个很小的应用程序,处理几 KB 的脚本文件,你只需要关心一下页面速度和脚本加载。
但你可能会处理由第三方库或你在现实生活中编写的较大的脚本。你必须确保页面的加载速度不会因此而降低。
但是,请等一下! 较大的脚本文件是如何降低页面加载速度的?让我们通过一个名为 圣诞老人的秘密(The Secret Santa Game)
的简单应用的演示来了解。
圣诞老人的秘密 - 页面速度演示
圣诞老人的秘密(The Secret Santa Game)
是一个简单的游戏,可以选择一个圣诞老人,一个孩子,以及圣诞老人要送给孩子的礼物。每当你点击“播放”按钮,就会选择一个新的圣诞老人、孩子和礼物。
在网页上显示礼物的图像以及圣诞老人和孩子的名字。它有一个带有文字 play
的按钮和一个显示版权文本的页脚。
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Secret Santa - V1</title>
<link rel="stylesheet" href="./styles/main.css" />
<script src="./js/script-1.js"></script>
<script src="./js/script-2.js"></script>
<script src="./js/script-3.js"></script>
</head>
<body>
<div class="container">
<header>
<h1>Secret Santa Game</h1>
</header>
<div class="content">
<p id="gift-id" class="gift"></p>
<p style="font-size: 3rem;">
🎅 <strong>Santa</strong>: <span id="santa-id"></span>
</p>
<p style="font-size: 3rem;">
👶 <strong>Child</strong>: <span id="child-id"></span>
</p>
<button class="play-btn" onclick="init()">Play</button>
</div>
<footer id="footer-id"></footer>
</div>
</body>
</html>
看一下 HTML 文件的 <head>
标签。我们在这里加载三个脚本。
script-1.js: 这个文件包含负责 DOM 更新的 JavaScript 代码。init()
方法提取随机的参与者和礼物值,在 DOM 节点上呈现。当点击 play
按钮时,同样的 init 方法被调用。
const gifts = ['hoodie', 'moon-light', 'perfumes', 'watch', 'studio-light'];
const participants = ['Alex', 'Bob', 'Carl', 'Dell', 'Emle'];
const getRandomElem = (arr) => {
return arr[Math.floor(Math.random() * arr.length)];
};
const init = () => {
const giftElem = document.getElementById('gift-id');
const childElem = document.getElementById('child-id');
const santaElem = document.getElementById('santa-id');
const child = getRandomElem(participants);
const santa = getRandomElem(participants.filter((elem) => elem !== child));
const gift = getRandomElem(gifts);
console.log(`${santa} to give ${gift} to ${child}`);
childElem.innerText = '';
childElem.innerText = child;
santaElem.innerText = '';
santaElem.innerText = santa;
giftElem.innerHTML = '';
const img = document.createElement('img');
img.src = `./gift-images/${gift}.png`;
img.alt = gift;
img.width = '300';
img.height = '300';
giftElem.appendChild(img);
};
init();
script-2.js: 这个 JavaScript 文件包含一个较小的代码,用于将版权文本设置到页脚标签(footer element)中。
const addToFooter = () => {
const footerElem = document.getElementById('footer-id');
footerElem.innerText = `CopyRight ${new Date().getFullYear()} @tapasadhikary`;
};
addToFooter();
script-3.js: 最后的 JavaScript 文件不包含操作 DOM 的代码,但为应用程序带来额外的功能,如 AD 、分析、聊天工具等。
前面的两个脚本控制 DOM 的脚本,其中一个是很小的一个。第三个不控制 DOM,并给应用程序带来一些独立的功能。
在 head 中加载一个 JavaScript 文件的问题
那么,当我们在 HTML 文件的 <head>
标签里加载这些脚本时,会发生什么?正如我们在上面看到的那样?不幸的是,我们不会看到任何设置在 DOM 中的值,使页面看起来不完整。
看看下面的图片,它清楚地显示了从script-1.js
和script.js
中发现 DOM 标签为null
的错误(译者注:脚本文件先于想控制的 DOM 标签加载,并运行,自然就找不到)。另外,我们没有看到礼物图片和参与者的名字(圣诞老人和孩子)

发生这种情况是因为在下载和执行脚本的时候,DOM 还没有准备好。
浏览器会从上到下解析 HTML 文档。当它遇到<head>
部分的脚本时,其余的 DOM 标签的创建将暂停,以便脚本的下载和执行。一旦完成,剩余的 HTML 将被处理以创建 DOM 标签。
差劲的解决方法--把它挪到 body 后面
那么,我们该如何解决这个问题呢?一个显而易见但不太妙的解决方法是把下载和执行脚本的工作移到<body>
标签的后面。这将确保在我们下载和运行脚本之前,所有的 DOM 标签都已构建并准备就绪。
你猜怎么着?这次应用程序可以正常工作,没有任何错误。

但为什么说这是一个差劲的修复?交互性和数据渲染等待的时间更长,甚至在 DOM 构造之后。我们的许多用户可能没有使用高速的 4G/5G 网络。一个大的脚本将需要很长的时间来下载和执行。下载时间可能会变得如此之长,以至于终端用户可能会感到沮丧并决定放弃使用该应用程序。
下面的图片显示,当我们在网络节流(3G 网络模拟)和禁用缓存的情况下运行同一个应用程序时,加载时间较长。正如你所看到的,DOM 内容在最终加载发生之前已经加载了很多。
这里有一个开发技巧。你可以使用浏览器的 DevTools 来模拟你的应用程序在较慢的网络上的加载情况。我们所有的用户可能没有 4G/5G 的网络。请查看此推文以了解更多细节。
使用浏览器开发工具,你可以模拟你的应用程序在一个较慢的网络中如何加载(我们所有的用户可能没有 4G/5G)。
- 你可以设置一个网络节流(3G,自定义)
- 禁用浏览器缓存
- 检查加载时间值得进一步探索。 pic.twitter.com/KgvKL6fcUE
— Tapas Adhikary (@tapasadhikary) December 23, 2022
在 Twitter 上关注我,每日获取新知识。
让我们直观地了解这个问题
好吧,让我们现在直观地了解这两种情况。毕竟,一张图片胜过千言万语。下面的图片显示了在<header>
标签和<body>
标签末尾加载脚本文件的两种情况。
在第一种情况下,我们看到 DOM 的构建被暂停了,因为脚本正在被下载和执行。一旦完成,DOM 的构建就恢复并完成了。因此,很明显,当浏览器执行脚本时,有相当一部分 DOM 标签没有被创建,没有对它们进行控制。
在另一种情况下,我们在<body>
标签的结尾处加载脚本,DOM 内容已经完全准备好。最后,浏览器下载并执行了这些脚本。
这次一切正常,因为当脚本执行时,DOM 已经准备好更新内容。页面完全运行所需的总时间是由脚本下载和运行最后完成的时间。
在这两种情况下,指定脚本的顺序很重要。脚本将按照 HTML 文档中指定的相同顺序下载和执行。

什么是 async 属性,它是如何帮助页面加载的?
<script>
标签的async
属性确保其他脚本下载不会等待该脚本的下载。浏览器在遇到该脚本时也不会阻止 DOM 内容的创建。该脚本在后台被下载,一旦完成就会执行。
该脚本以 先加载(load-first)
的顺序执行。即使一个较小的 async 脚本在 HTML 文件中被指定为较低的顺序(在 html 文件中,放在后面的位置),它也可能在所有其他脚本之前执行。
当你为一个执行任何 DOM 操作的脚本添加 async
属性时,你必须小心。让我们用我们的 圣诞老人的秘密(The Secret Santa Game)
来体验一个棘手的场景吧
让我们为所有的脚本添加async
属性,而不改变它们在 HTML 文档的<head>
中的位置顺序。记住,script-1.js
和script-2.js
都是对 DOM 内容进行操作的,script-2.js
的大小较小。script-3
是另一个小脚本,不进行任何 DOM 操作。
<script async src="./js/script-1.js"></script>
<script async src="./js/script-2.js"></script>
<script async src="./js/script-3.js"></script>
现在,当你在一个慢速网络上运行该应用程序时,你可以看到脚本的加载顺序发生了变化。大小较小的 script-2
,首先被下载并执行,然后是 script-3
,最后是 script-1
。所以,它们在 HTML 文档中的顺序在这里并不重要。
这正是我们的应用程序所发生的情况。在 “Play” 按钮下面的版权声明没有呈现。我们从错误中得知,DOM 中的footer
元素无法让脚本添加所需的文本。
现在我们来看看使用async
属性的脚本的下载和执行情况。
正如你所看到的,当脚本被下载时,浏览器不会停顿。脚本在被下载后就开始执行了。当一个 异步(async)
脚本执行时,并不能保证相关的 DOM 已经加载到浏览器中。

原则是不要在操作 DOM 的脚本中使用async
属性。在应用程序外部不操作 DOM 的脚本中使用async
。像库、聊天机器人、分析工具等脚本是必须考虑使用async
属性的合适情况。
什么是 defer 属性,它是如何帮助页面加载的?
最后一种也是最有效的加载脚本的方法是使用 defer
属性。defer
属性的工作原理与 async
属性基本相同,但有几个关键的区别。
<script defer src="./js/script-1.js"></script>
<script defer src="./js/script-2.js"></script>
<script defer src="./js/script-3.js"></script>
和 async
一样,defer
在后台下载脚本,但它在执行时绝不会中断页面的渲染。
请看下面的图片,我们在这里添加了 defer
属性的下载和执行流程。

正如你所看到的,带有 defer
属性的脚本是与页面文档同时下载的。尽管如此,它仍然只在文档加载完毕后执行。如果有多个带有defer
属性的脚本,它们都会在 DOMContentLoaded
事件之前按顺序执行。
这是与 async
最显著的区别,在 async
中,脚本在加载时立即执行,不遵循任何顺序。
原则是对操作 DOM 的脚本使用defer
属性。它将通过在后台下载脚本并在 DOM 准备好后执行来改善页面加载。
这里是一个快速回顾
让我们快速回顾一下我们在本文中学到的东西:
- 在 HTML 文档中,
<script>
标签的最佳位置是在<head>...</head>
标签内。然而,你可能会在设置 DOM 内容时遇到问题。 - 将
<script>
标签放在<body>
标签的后面,是处理脚本的理想方式。 - HTML 提供了
async
和defer
属性,通过在后台下载,可以更快地加载页面,并将较大的脚本加载带来的延迟降到最低 - 对于不执行 DOM 操作的外部脚本,使用
async
。async
不能保证脚本执行时页面渲染的中断。 - 对所有执行 DOM 操作的脚本使用
defer
。带有defer
属性的脚本会在页面加载结束时依次执行。
在我们结束之前......
现在就说到这里。我希望你能发现这篇文章的信息量和洞察力。本文使用的所有源代码都可以在这个 GitHub 仓库上找到。
交流方式:
- 订阅我的 YouTube 频道,如果你想以实用的方式学习 JavaScript、ReactJS、Node.js、Git 和所有关于 Web 开发的知识。
- 如果你不想错过每天的 Web 开发和编程技巧,请关注我的 Twitter 或 LinkedIn
- 可以在 GitHub 上查看我的开源代码。
- 在 Showwcase 上关注基于社区的学习。
我的下一篇文章很快就会与你见面。希望你保持快乐,身体健康。