script标签会阻塞页面的渲染!
浏览器并未规定我们要将script标签放置在何处,你可以把它放在body的前面,中间,后面,甚至是放在head里。当浏览器解析到一个script标签时,它会先加载这个脚本,然后解析与执行它,而这个过程会中断后续HTML内容的解析与渲染。
考虑下面的示例,浏览器会像一个流水线一样,先解析"Hello World"这个标题并在页面上渲染出来,然后加载和执行"longtask.js"脚本,一切完成后再继续解析和渲染"你好"这个标题。如果longtask.js的加载与执行一共要花费一秒,那么页面效果会是这样:先看到"Hello world",过了一秒之后,再出现"你好"~
html
<body>
<h1>Hello World</h1>
<script src="longtask.js"></script>
<h1>你好</h1>
</body>

当你尝试这个示例后,就能在心中确定:script脚本真的会影响我页面的渲染呀!我可不想自己的页面像播放视频时卡帧了一样断断续续的。
非阻塞加载
将script标签置于body底部
要想不阻塞html页面内容的渲染,一个最简单的办法就是把script标签放在body的底部:等其他html内容处理完了再解析script标签。
html
<body>
<h1>Hello World</h1>
<h1>你好</h1>
<script src="longtask.js"></script>
</body>
如果要实现script放在其他任何地方都不阻塞页面渲染,则可以使用defer
属性。
defer
html
<head>
<script defer src="longtask.js"></script>
</head>
<body>
<h1>Hello World</h1>
<h1>你好</h1>
</body>
带有defer的脚本会立即下载,但延迟执行:直到整个HTML文档解析和渲染完成后再执行。有一个事件是DOMContentLoaded,它会在HTML完全解析,且所有defer脚本下载与执行后触发。
html
<head>
<script defer src="longtask.js"></script>
</head>
<body>
<h1>Hello World</h1>
<h1>你好</h1>
<script>
document.addEventListener("DOMContentLoaded", (event) => {
console.log("DOMContentLoaded");
});
</script>
</body>
create-react-app脚手架生成的应用在打包后也会生成这样一个带defer的脚本来加载JS(对于html文档,defer="defer"
与defer
两种写法没有区别。如果文件是.xhtml
的文档,则要求使用前者)。

由于defer脚本是在所有HTML内容解析完之后执行,所以即使它放在head中,我们仍然可以在JS代码中获取body元素的内容。
js
console.log(document.getElementById("root")); // <div id="root"></div>
目前defer属性几乎是所有浏览器都支持的,可以放心使用。在任何情况下,我们都应该使用defer来避免script标签阻塞渲染,除非你真的有必要在所有HTML内容渲染完成前打断它。
async
async(异步)脚本同样不会阻塞HTML的解析,它与defer的不同是:它在下载完成后会立刻执行,而不关心所有HTML页面内容是否已经加载与解析完成。考虑下面一个例子:
html
<head>
<link rel="stylesheet" href="style.css" />
<script async src="main.js"></script>
</head>
<body>
<div id="app"></div>
</body>
style.css文件的内容如下:
css
div {
width: 200px;
height: 200px;
}
在main.js中获取id为"app"的元素并打印:
js
console.log(document.getElementById("app").clientHeight)
最终main.js里打印的结果是什么呢?可能是200,也可能是0!这取决于style.css文件是否在main.js加载执行前完成了加载与解析。另外,多个async文件也是不保证执行顺序的,并非代码中位置在前面的async脚本就一定先执行。
所以,async适用于与页面内容无关,同时也与其他JS文件无关的独立脚本。它就像一个独行侠🚀,不要期望它会听你的指令在某个具体时机执行。
使用行内代码还是外部文件
JavaScript代码也是可以内联的,像下面这样:
html
<body>
<div id="app"></div>
<script>
console.log("Hello World");
</script>
</body>
考虑下如果我们把所有的JS代码都内联到HTML文件中会怎样呢?那这个HTML体积会很大,网络加载时间会更久,页面白屏时间也就会更长。另外HTML文件一般是不设置缓存的,所以即使这些JS代码没有变更,也需要每次都重新加载。
所以,实际项目中倾向于把这些JS代码保存到独立文件中,并设置一定的缓存策略。同时,如果有多个小的JS文件,也会把它们合并为一个,以减少请求数量。