背景
继上篇《图片懒加载原理方案详解》,在前端性能优化中还有另一个相反的技巧:预加载。懒加载是延迟加载非关键资源,而预加载是在页面空闲的时候提前加载后面可能需要的资源,需要时能快速呈现资源。本文主要讨论脚本的懒加载和预加载。通过本文你能收获:
- 脚本懒加载原理实现。
- 脚本预加载原理实现。
- 脚本懒加载和预加载如何选择。
脚本懒加载
延迟加载 (懒加载) 是一种将资源标识为非阻塞(非关键)资源并仅在需要时加载它们的策略。这是一种缩短关键渲染路径长度的方法,可以缩短页面加载时间。 ——MDN
MDN 中定义的懒加载是延迟加载,按照这个定义脚本懒加载就包含 script 标签的 defer 和 async 属性的延迟加载。
懒加载或者按需加载,是一种很好的优化网页或应用的方式。这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。 ——webpack
webpack 定义懒加载是按需加载,按需加载也是延迟加载。本文目的是详细讨论脚本懒加载,按延迟加载解析。
实现方式
script 标签放在 body 中的最后
script 标签放在 body 中的最后是一种简单的脚本延迟加载,把脚本的加载延迟到解析 html 之后。这种方式解决解析 html 阻塞导致页面加载有一段时间白屏,脚本的加载和解析会阻塞 DOMContentLoaded。
script 标签 async 属性:
异步下载脚本并在允许的情况下执行,没有先后顺序主要看什么时候加载完什么时候就执行,脚本有可能在DOMContentLoaded 事件之前执行也有可能在之后执行。脚本的加载没有阻塞 html 的解析,性能上节省了部分等待脚本加载的时间,在 DOMContentLoaded 之前执行会阻塞 DOMContentLoaded,在 DOMContentLoaded 之后执行会阻塞load。
async没有执行顺序,而且执行的时间也不确定,因此使用 async 属性的脚本必须独立运行并且不操作 DOM 的非关键脚本。
async 属性也可以用在预加载后面可能要用到的插件脚本,一般插件脚本都是声明一个全局函数或类,使用时执行这个函数或类的中的方法,后面解析预加载。
兼容性:主流浏览器都已支持。
script 标签 defer属性
脚本异步加载,延迟执行。在 html 解析完成后 DOMContentLoaded 事件执行前执行,DOMContentLoaded 要等到 defer 标签脚本执行完之后才会执行,defer 是按照脚本加载顺序执行的。脚本的加载也没有阻塞 html 的解析,但是可能阻塞 DOMContentLoaded。 兼容性:
主流浏览器都已支持,在 ie 和 Firefox 早期版本会执行顺序问题。
defer 属性效果和 script 标签放在 body 中的最后的效果类似,只是脚本的下载是异步的,节省了一部分脚本的下载时间。因此 defer 属性是它的优化方案,关键脚本、需要操作 DOM 脚本、需要执行顺序脚本都可以使用defer 属性,其中有个要注意的点,异步加载的在浏览器网络请求优先级要低于同步。如果异步加载的脚本中有document.write("<script src='xxxx'/>") 同步加载另一个脚本,不会加载这个脚本。
网络请求优先级图如下:
动态加载脚本
动态加载脚本是在 JavaScript 代码中创建 script 标签的DOM对象。
function loadScript(url){
var script = document.createElement("script");
script.type = "text/javascript";
script.src = url;
document.body.appendChild(script);
}
默认和 async 属性一样异步加载,加载完马上执行,如果属性 async=false,会按照 defer 一样加载。经常用这种方式实现按需加载。
缺点
按需加载的缺点当使用脚本时需要等待时间去加载。 defer 和 async 虽然节省了部分加载时间,但是依然阻塞页面的渲染。
脚本预加载
预加载经常用到图片的优化中,脚本同样也可以使用预加载。资源的加载会有本地缓存,所以可以提前加载资源使用时就能快速呈现或使用。
实现方式
link 的 Preload 属性
<link>元素的 rel 属性的 preload 值允许您在 HTML 的<head>中声明获取请求页面中很快需要的资源,你希望在页面生命周期的早期开始加载,在浏览器的主要渲染机制启动之前。这确保它们更早可用,并且不太可能阻止页面的呈现,从而提高性能。尽管这个名字包含了加载一词,但它也不会加载和执行脚本,而只是将其安排在更高的优先级上进行下载和缓存。 ——MDN
link元素rel属性值为preload可以预加载资源放到本地缓存中,使用时就可以从本地缓存中获取。preload也可以作用在脚本上。
<link rel="preload" href="preload.js" as="script">
as 设置预加载的内容类型。
这种方式有几个特点:
第一是脚本是异步加载,既不会阻塞 html 解析,也不会阻塞 DOMContentLoaded 事件,更不会阻塞 load 事件。
第二是脚本下载后不会执行,只会保存在本地缓存中,等遇到 script 标签加载这个脚本才会执行。
第三由前面网络请求优先级图可知,preload 加载的脚本的优先级是 hight,比 async 加载的脚本优先级要高,会提前请求下载。
使用场景
- 预加载当前页面使用的脚本 preload 可以将下载和执行分开,预先异步下载脚本,等到使用的时候通过代码来执行。
var script = document.createElement("script");
script.src = "preload.js";
document.body.appendChild(script);
这样脚本的的下载和执行都没有阻塞页面的渲染,从而提高页面的性能。
- 异步加载,延迟执行
<head>
<meta charset="utf-8" />
<link rel="preload" href="main.js" as="script" />
</head>
<body>
<div>preload实现类似script defer属性效果</div>
<script src="main.js"></script>
</body>
类似 script defer 属性,浏览器会预加载 link 元素中的 main.js 文件,加载完后在 DOMContentLoaded 事件执行之前执行,从而可能阻塞 DOMContentLoaded 事件的执行。性能上要比 script defer 好些,因为 preload 的优先级要高会提前请求脚本。
- 异步加载,马上执行
<link rel="preload" as="script" href="preload.js"
onload="var script = document.createElement('script');
script.src = this.href;
document.body.appendChild(script);">
在脚本加载完触发的 load 事件中添加 script 标签执行脚本,类似 script async,性能上要比script async好些。
- 不执行的动态加载
var preload = document.createElement("link");
link.href = "preload.js";
link.rel = "preload";
link.as = "script";
document.head.appendChild(link);
后面要执行只需注入script标签就可以了。
var script = document.createElement("script");
script.src = "preload.js";
document.body.appendChild(script);
兼容性
link的Prefetch属性
Prefetch 是提前在后台下载可能被请求的内容,可以供用户后面立即使用内容。在用户没有明确请求的情况下,内容被下载和缓存以备将来使用。 ——MDN
prefetch网络请求优先级是 lowest,下载方式和 preload 一样异步下载不阻塞页面渲染,只下载不执行。被用于下个导航可能用到的脚本。
兼容性
脚本懒加载和预加载选择
懒加载一般用在单页应用中,因为单页应用首页的加载脚本比较多,需要用懒加载把非关键脚本放到后面按需加载,而减轻首页的脚本加载压力。
预加载一般用在多页应用中,把脚本比较多的页面中的一些脚本提前在上一个导航页预加载而减轻当前页的脚本加载压力。
懒加载和预加载其实都是为了均衡页面生命周期中的加载负担问题,合理的把一些闲置时间用于加载资源。因此在项目中也可以一起使用。
关键脚本可以添加 reload 和 defer 来提前加载脚本,不支持 preload 的浏览器会按照 defer 来加载脚本。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="renderer" content="webkit" />
<link rel="preload" href="./preload.js" as="script" />
</head>
<body>
<div style="color: red">preload是否阻塞页面的渲染</div>
</body>
<script defer src="./preload.js"></script>
</html>
路由和一些小的脚本可以用按需加载的方式引入。比较大的脚本可以用 prefetch 的方式先预加载。