什么是预加载?有什么用?
什么是预加载?有什么用?
预加载(规范)是一项旨在改善性能并为Web开发人员提供更精细的加载控制的新Web标准。它使开发人员能够定义自定义加载逻辑,而不会遭受基于脚本的资源加载器所带来的性能损失。大部分的现代浏览器已经支持了预加载。但预加载是什么?它有什么作用?它如何帮助你?
几周前,我在Chrome Canary中发布预加载支持,除非出现意想不到的错误,它将在四月中旬到达Chrome稳定版。但是,什么是preload?它是做什么的?如何帮助您?
好吧,<link rel=“preload”>
是一个声明性获取指令。
用人类的语言来说,它是一种告诉浏览器开始获取某个特定资源的方法,因为我们作为作者(或作为服务器管理员,或作为聪明的服务器开发人员)知道浏览器很快会需要那个特定资源。
我们不是已经有这个了吗?
有点是,但也不完全是。在Web上已经支持 <link rel=“prefetch”>
很长时间了,并且有着 相当好的浏览器支持。此外,我们在Chrome中也支持了 [<link rel=“subresource”>](<https://web.archive.org/web/20150907034803/https://www.chromium.org/spdy/link-headers-and-server-hint/link-rel-subresource>)
很长一段时间了。那么 preload 有什么新东西?它与这些其他指令有什么不同?它们都是告诉浏览器去获取资源,不是吗?
是的,但它们之间有很大的区别。这些区别值得一个全新的指令来处理许多旧指令从未处理过的用例。
<link rel=“prefetch”>
是一个指令,它告诉浏览器去获取下一个导航可能需要的资源。这基本上意味着资源将以极低的优先级被获取(因为浏览器知道当前页面所需的所有内容都比我们猜测下一个页面可能需要的资源更重要)。这意味着 prefetch 的主要用例是加快下一个导航的速度,而不是当前导航的速度。
<link rel=“subresource”>
最初计划用于处理当前导航,但在某些方面它未能做到这一点。由于Web开发人员无法定义资源的优先级,因此浏览器(实际上只有Chrome和基于Chromium的浏览器)以相当低的优先级下载它,这意味着在大多数情况下,资源请求的时间与没有 subresource 时的时间几乎相同。
如何使预加载变得更好?
预加载与子资源一样可以用于当前导航,但它包含一个小但重要的区别。它具有一个“as”属性,使浏览器可以完成一些子资源和预获取无法完成的操作:
- 浏览器可以设置正确的资源优先级,以便相应地加载资源,不会延迟更重要的资源,也不会落后于不太重要的资源。
- 浏览器可以确保请求受到正确的Content-Security-Policy指令的影响,如果不应该,则不会转到服务器。
- 浏览器可以基于资源类型发送适当的“Accept”标头(例如,在获取图像时广告支持“image/webp”)。
- 浏览器知道资源类型,因此可以稍后确定资源是否可以重复使用,以满足需要相同资源的未来请求。
预加载还不同,因为它具有功能性的“onload”事件(在Chrome中,它至少对于其他两个“rel”值而言并不起作用)。
除此之外,预加载不会阻止窗口的“onload”事件,除非该资源也被请求阻止该事件的资源请求。
将所有这些特性结合在一起可以实现一些以前不可能实现的新功能。
让我们来看看它们,好吗?
晚发现资源的预加载
您可以使用预加载的基本方法是提前加载晚发现的资源。虽然大多数基于标记的资源由浏览器的预加载器相当早地发现,但并非所有资源都是基于标记的。一些资源隐藏在CSS和JavaScript中,浏览器无法知道它需要它们,直到已经相当晚了。因此,在许多情况下,这些资源会延迟第一次渲染、文本渲染或页面关键部分的加载。
现在您有了手段告诉浏览器:“嘿,浏览器!这是您稍后需要的资源,请立即开始加载。”
这样做看起来会像这样:
<link **rel**="preload" **href**="late_discovered_thing.js" **as**="script">
as
属性告诉浏览器它将要下载的内容。可能的 as
值包括:
"script"
,"style"
,"image"
,"media"
,- 以及
"document"
。
(请参阅 fetch spec 获取完整列表。)
省略 as
属性或具有无效值等同于 XHR 请求,其中浏览器不知道它正在获取什么,并以相当低的优先级获取它。
字体的早期加载
“晚发现的关键资源”模式的一个常见例子是网络字体。一方面,在大多数情况下,它们对于呈现页面上的文本至关重要(除非您使用光亮的 font-display CSS 值)。另一方面,它们深深地埋在 CSS 中,即使浏览器的预加载器解析了 CSS,它也不能确定它们是否需要,直到它也知道需要它们的选择器实际上应用于 DOM 的某些节点。虽然从理论上讲,浏览器可以弄清楚这一点,但它们都没有这样做,如果它们这样做,当字体规则在更多 CSS 规则进来之后被进一步覆盖时,它可能会导致虚假下载。
简而言之,这很复杂。
但是,您可以通过包含字体的预加载指令来摆脱所有这些复杂性。类似于:
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
值得一提的一点是:当获取字体时,您必须添加crossorigin
属性,因为它们是使用匿名模式CORS获取的。是的,即使您的字体与页面相同。对不起。
此外,type
属性在这里是为了确保该资源仅在支持该文件类型的浏览器上预加载。目前,只有 Chrome 支持预加载,它也支持 WOFF2,但是更多的浏览器可能在未来支持预加载,我们不能假设他们也支持 WOFF2。对于您要预加载的任何资源类型,支持浏览器并不普遍。
不执行的动态加载
另一个有趣的场景是,你想下载一个资源,因为你知道你会需要它,但是你还不想立即执行它。例如,想象一下这样一个场景,你想在页面的某个特定时刻执行一个脚本,但是你无法控制脚本(因此无法添加runNow()
函数)。
目前,你的选择非常有限。如果你只在想要运行的时候注入脚本,那么浏览器必须先下载脚本才能执行,这可能需要一段时间。你可以使用XHR预先下载脚本,但是浏览器会拒绝重用它,因为该资源的下载类型与试图使用该资源的类型不同。
那么你该怎么办?
在preload之前,没什么办法。(在某些情况下,你可以eval()
脚本的内容,但这并不总是可行的,也不是没有副作用的。)但是有了preload,你就可以了!
var preload = document.createElement("link"); link.href = "myscript.js"; link.rel = "preload"; link.as = "script"; document.head.appendChild(link);
你可以在页面加载过程的早期运行它,远在你想让脚本执行的时间点之前(但是一旦你相当有信心脚本加载不会干扰其他更关键的需要加载的资源)。然后当你想要运行它时,只需注入一个script
标签即可。
var script = document.createElement("script"); script.src = "myscript.js"; document.body.appendChild(script);
基于标记的异步加载程序
另一个很酷的技巧是使用onload
处理程序来创建某种基于标记的异步加载程序。Scott Jehl是第一个尝试这样做的人,作为他的loadCSS库的一部分。简而言之,你可以这样做:
<link **rel**="preload" **as**="style" **href**="async_style.css" **onload**="this.rel='stylesheet'">
并在标记中获得异步加载的样式!Scott也有一个不错的demo页面来演示这个功能。
异步脚本也可以这样做。
你可能会说我们已经有了<script async>
?好吧,<script async>
很棒,但它会阻止窗口的onload事件。在某些情况下,这正是你想要它做的,但在其他情况下则不然。
假设你想要下载一个分析脚本。你希望它下载得相当快(以避免错过分析脚本没有捕捉到的访问者),但你不希望它延迟对用户体验产生影响的任何指标,特别是你不希望它延迟onload。 (你可以声称onload不是影响用户的唯一指标,你是对的,但是早点停止旋转加载图标仍然很好。)
有了preload,实现这一点很容易:
<link **rel**="preload" **as**="script" **href**="async_script.js" **onload**="var script = document.createElement('script'); script.src = this.href; document.body.appendChild(script);">
(在onload
属性中包含长JS函数可能并不是一个好主意,因此你可能希望将该部分定义为内联函数。)
响应式加载
由于preload是一个链接,根据规范,它有一个media
属性。(它目前在Chrome中不受支持,但很快将会支持。)该属性可以启用有条件的资源加载。
那有什么好处呢?假设您的网站的初始视口有一个大型交互式地图,适用于桌面/宽视口版本的网站,但仅为移动设备/窄视口版本显示静态地图。
如果您聪明地使用,希望仅加载其中一个资源而不是两个。唯一的方法是通过使用JS动态加载它们。但通过这样做,您使这些资源对预加载程序不可见,它们可能会比必要的晚加载,这可能会影响用户的视觉体验,并且对您的SpeedIndex分数产生负面影响。
我们能做些什么来确保浏览器尽早知道这些资源呢?
你猜对了!预加载。
我们可以使用preload提前加载它们,我们可以使用它的media
属性,以便仅预加载所需的脚本:
`<link rel="preload" as="image" href="map.png" media="(max-width: 600px)">
`标题
使用链接标签的一个免费特性是它们可以作为 HTTP头部 的形式进行表示。这意味着,在我上面展示的大多数标记示例中,您可以拥有一个完全相同的 HTTP 响应头。 (唯一的例外是 与 onload
相关的示例。您不能将 onload 处理程序定义为 HTTP 标头的一部分。)
此类 HTTP 响应头的示例可能类似于:
`Link: <thing_to_load.js>;rel="preload";as="script"
Link: <thing_to_load.woff2>;rel="preload";as="font";crossorigin`
当负责优化的人与负责编辑标记的人不同时,HTTP 头部非常有用。显而易见的例子是扫描内容并对其进行优化的 外部优化引擎 (免责声明:我正在其中一个上工作)。
其他例子可能包括一个专门的性能团队,他们想要添加这些优化,或者一个优化构建流程,其中避免 HTML 操作可以显著减少复杂性。
特性检测
最后一点:在我们上面的一些示例中,我们依赖于 preload 被支持以进行基本功能,如脚本或样式加载。那些不支持 preload 的浏览器会发生什么?
一切都会崩溃!
我们不希望出现这种情况。因此,在 preload 工作的一部分,我们还更改了 DOM 规范,以便通过特性检测支持的 rel 关键字。
一个 示例特性检测 函数可能如下所示:
var DOMTokenListSupports = function(tokenList, token) {
if (!tokenList || !tokenList.supports) {
return;
}
try {
return tokenList.supports(token);
} catch (e) {
if (e instanceof TypeError) {
console.log("The DOMTokenList doesn't have a supported tokens list");
} else {
console.error("That shouldn't have happened");
}
}
};
var linkSupportsPreload = DOMTokenListSupports(document.createElement("link").relList, "preload");
if (!linkSupportsPreload) {
// 动态加载依赖于 preload 的内容。
}
这使您能够在缺乏 preload 支持会破坏您的网站的情况下提供后备加载机制。非常方便!
HTTP/2推送是否涵盖这些用例?
不完全涵盖。虽然这些功能有一些重叠,但它们大多是互补的。
HTTP/2推送具有推送浏览器尚未请求的资源的优势。这意味着在HTML甚至开始发送到浏览器之前,推送可以发送资源。它还可以用于在打开的HTTP/2连接上发送资源,而不需要响应,这些资源可以附加HTTP链接标头。
另一方面,预加载可以用于解决HTTP/2无法解决的用例。如我们所见,使用预加载,应用程序知道正在发生的资源加载,并且可以在资源完全加载后被通知。这不是HTTP/2推送的设计目的。此外,HTTP/2推送无法用于第三方资源,而预加载可以像在一方资源上一样有效地用于它们。
此外,HTTP/2推送无法考虑浏览器的缓存和非全局Cookie状态。虽然可以通过新的缓存摘要规范来解决缓存状态,但对于非全局Cookie,无法做任何事情,因此不能将Push用于依赖此类Cookie的资源。对于这样的资源,预加载是您的朋友。
预加载的另一个优点是它可以执行内容协商,而HTTP/2推送无法执行。这意味着如果您想使用Client-Hints来找出要发送到浏览器的正确图像,或者使用Accept:标头找出最佳格式,HTTP/2推送不能帮助您。
所以...
我希望您现在相信预加载打开了一组以前不可行的加载功能,并且您对使用它感到兴奋。
我要求您去拿起Chrome Canary,玩弄预加载,将其分成碎片,然后哭泣地回到我这里。这是一个新功能,像任何新功能一样,它可能包含错误。请帮助我尽早找到并修复它们。