当我想到网站性能时,首先想到的是图像通常是页面上最后出现的元素。如今,图像在性能方面可能是一个主要问题,这很不幸,因为网站加载速度对用户成功完成他们访问页面的目的有直接影响(想想转化率)。
最近,Rahul Nanwani 撰写了一篇关于延迟加载图像的完整指南。我想从不同的方法来探讨相同主题:使用数据属性、交集观察器 和 Vue.js 中的自定义指令。
这实际上可以帮助我们解决两个问题
- 在不加载图像的情况下存储我们要加载的图像的
src
。 - 检测图像何时对用户可见并触发加载图像的请求。
相同的延迟加载基本概念,但使用另一种方法。
我创建了 一个示例,基于 Benjamin Taylor 在 他的博文中 描述的示例。它包含一个随机文章列表,每个列表都包含简短描述、图像和文章来源的链接。我们将逐步了解创建负责显示该列表、渲染文章以及延迟加载特定文章图像的组件的过程。

让我们变得懒惰吧!或者至少将此组件分解成一个个部分。
步骤 1:在 Vue 中创建 ImageItem 组件
让我们首先创建一个组件,该组件将显示图像(但目前还没有延迟加载)。我们将此文件命名为 ImageItem.vue
。在组件模板中,我们将使用一个包含图像的 figure
标签——图像标签本身将接收指向图像文件源 URL 的 src
属性。
<template>
<figure class="image__wrapper">
<img
class="image__item"
:src="source"
alt="random image"
>
</figure>
</template>
在组件的脚本部分,我们接收 source
道具,我们将用于我们显示的图像的 src
URL。
export default {
name: "ImageItem",
props: {
source: {
type: String,
required: true
}
}
};
所有这些都很好,并将按预期渲染图像。但是,如果我们在这里结束,图像将立即加载,而不会等待整个组件渲染完成。这不是我们想要的,所以让我们进行下一步。
步骤 2:阻止组件创建时加载图像
我们想要阻止图像加载可能听起来有点奇怪,因为我们想显示它,但这关乎在 *正确的时间* 加载图像,而不是无限期地阻止它。为了阻止图像加载,我们需要从 img
标签中删除 src
属性。但是,我们仍然需要将它存储在某个地方,以便我们可以在需要时使用它。一个不错的存储信息的地方是 data-
属性。它们允许我们存储关于标准、语义 HTML 元素的信息。实际上,您可能已经习惯于将它们用作 JavaScript 选择器。
在这种情况下,它们非常适合我们的需求!
<!--ImageItem.vue-->
<template>
<figure class="image__wrapper">
<img
class="image__item"
:data-url="source" // yay for data attributes!
alt="random image"
>
</figure>
</template>
这样一来,我们的图像 *不会* 加载,因为没有源 URL 可供提取。
这是一个良好的开端,但还不是我们真正想要的。我们希望在特定条件下加载图像。我们可以通过将 src
属性替换为我们存储在 data-url
属性中的图像源 URL 来请求加载图像。这很简单。真正的挑战在于找出何时用实际的源替换它。
我们的目标是将加载固定到用户的屏幕位置。因此,当用户滚动到图像进入视野的位置时,图像将在那里加载。
我们如何检测图像是否在视野中?这是我们的下一步。
步骤 3:检测图像何时对用户可见
您可能使用过 JavaScript 来确定元素何时在视野中。您可能还使用过一些很复杂的脚本。
例如,我们可以使用事件和事件处理程序来检测滚动位置、偏移值、元素高度和视窗高度,然后计算图像是否在视窗中。但这听起来已经很复杂了,不是吗?
但它可能会变得更糟。这会对性能产生直接影响。这些计算将在每次滚动事件中触发。更糟糕的是,想象一下几十个图像,每个图像都必须在每次滚动事件中重新计算它是否可见。*太疯狂了!*
交集观察器 来拯救我们!它提供了检测元素是否在视窗中可见的非常有效的方法。具体来说,它允许您配置一个 **回调**,当一个元素——称为 **目标**——与设备视窗或指定元素相交时触发。
那么,我们需要做什么才能使用它?一些事情
- 创建一个新的交集观察器
- 观察我们希望延迟加载的元素以查看可见性变化
- 当元素进入视窗时加载元素(通过用我们的
data-url
替换src
) - 在加载完成后停止观察可见性变化(
unobserve
)
Vue.js 有自定义指令,可以将所有这些功能包装在一起,并在我们需要时使用它,无论需要多少次。我们的下一步是将它付诸实践。
步骤 4:创建 Vue 自定义指令
什么是自定义指令?Vue 的 文档 将其描述为在元素上获取低级 DOM 访问权限的一种方式。例如,更改特定 DOM 元素的属性,在我们的例子中,可能是更改 img
元素的 src
属性。完美!
我们将在稍后进行详细介绍,但以下是我们正在使用的代码
export default {
inserted: el => {
function loadImage() {
const imageElement = Array.from(el.children).find(
el => el.nodeName === "IMG"
);
if (imageElement) {
imageElement.addEventListener("load", () => {
setTimeout(() => el.classList.add("loaded"), 100);
});
imageElement.addEventListener("error", () => console.log("error"));
imageElement.src = imageElement.dataset.url;
}
}
function handleIntersect(entries, observer) {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadImage();
observer.unobserve(el);
}
});
}
function createObserver() {
const options = {
root: null,
threshold: "0"
};
const observer = new IntersectionObserver(handleIntersect, options);
observer.observe(el);
}
if (window["IntersectionObserver"]) {
createObserver();
} else {
loadImage();
}
}
};
好的,让我们逐步解决这个问题。
钩子函数 允许我们在绑定元素生命周期的特定时刻触发自定义逻辑。我们使用 inserted
钩子,因为当绑定元素被插入到其父节点时会调用它(这保证父节点存在)。由于我们希望观察元素相对于其父元素(或任何祖先元素)的可见性,因此我们需要使用该钩子。
export default {
inserted: el => {
...
}
}
loadImage
函数负责用 data-url
替换 src
值。在其中,我们可以访问我们的元素(el
),我们将在此处应用指令。我们可以从该元素中提取 img
。
接下来,我们检查图像是否存在,如果存在,则添加一个侦听器,当加载完成时将触发回调函数。该回调函数将负责隐藏加载动画并使用 CSS 类为图像添加动画(淡入效果)。我们还添加了第二个侦听器,它将在 URL 加载失败的情况下被调用。
最后,我们将 img
元素的 src
替换为图像的源 URL 并显示它!
function loadImage() {
const imageElement = Array.from(el.children).find(
el => el.nodeName === "IMG"
);
if (imageElement) {
imageElement.addEventListener("load", () => {
setTimeout(() => el.classList.add("loaded"), 100);
});
imageElement.addEventListener("error", () => console.log("error"));
imageElement.src = imageElement.dataset.url;
}
}
我们使用交集观察器的 handleIntersect
函数,该函数负责在满足特定条件时触发 loadImage
。具体来说,当交集观察器检测到元素进入视窗或父组件元素时,它将被触发。
该函数可以访问 entries
,它是观察器观察的所有元素的数组,以及观察器本身。我们遍历 entries
并检查单个条目是否使用 isIntersecting
对用户可见——如果可见,则触发 loadImage
函数。一旦请求图像,我们就 unobserve
元素(将其从观察器的观察列表中删除),这将阻止图像再次加载。以及再次。以及再次。以及…
function handleIntersect(entries, observer) {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadImage();
observer.unobserve(el);
}
});
}
最后一部分是 createObserver
函数。这个函数负责创建我们的交集观察器并将其附加到我们的元素。IntersectionObserver
构造函数接受一个回调(我们的 handleIntersect
函数),当观察到的元素通过指定的 threshold
并且 options
对象携带我们的观察器选项时,它将被触发。
说到options
对象,它使用root
作为我们的参考对象,我们用它来作为我们观察元素可见性的基础。 它可能是对象的任何祖先,或者如果我们传递null
,则为我们的浏览器视窗。 该对象还指定了一个threshold
值,该值可以在0
到1
之间变化,并告诉我们目标可见度的百分比是多少,此时应执行observer
回调,其中0
表示即使只有一个像素可见,1
表示整个元素必须可见。
然后,在创建Intersection Observer之后,我们使用observe
方法将其附加到我们的元素。
function createObserver() {
const options = {
root: null,
threshold: "0"
};
const observer = new IntersectionObserver(handleIntersect, options);
observer.observe(el);
}
步骤 5:注册指令
要使用我们新创建的指令,我们首先需要注册它。 有两种方法可以做到这一点:全局(在应用程序中的任何地方都可用)或局部(在指定的组件级别)。
全局注册
对于全局注册,我们导入指令并使用Vue.directive
方法传递我们要调用的指令名称和指令本身。 这使我们能够在代码中的任何元素上添加v-lazyload
属性。
// main.js
import Vue from "vue";
import App from "./App";
import LazyLoadDirective from "./directives/LazyLoadDirective";
Vue.config.productionTip = false;
Vue.directive("lazyload", LazyLoadDirective);
new Vue({
el: "#app",
components: { App },
template: "<App/>"
});
局部注册
如果我们只想在特定组件中使用指令并限制对其的访问,我们可以局部注册指令。 为此,我们需要在将使用该指令的组件中导入该指令,并在directives
对象中注册它。 这将使我们能够在该组件中的任何元素上添加v-lazyload
属性。
import LazyLoadDirective from "./directives/LazyLoadDirective";
export default {
directives: {
lazyload: LazyLoadDirective
}
}
步骤 6:在ImageItem组件上使用指令
现在我们的指令已经注册,我们可以通过在承载我们图像的父元素(在本例中为figure
标记)上添加v-lazyload
来使用它。
<template>
<figure v-lazyload class="image__wrapper">
<ImageSpinner
class="image__spinner"
/>
<img
class="image__item"
:data-url="source"
alt="random image"
>
</figure>
</template>
浏览器支持
如果我们没有注意到浏览器支持,那我们就是失职了。 尽管Intersection Observe API并非所有浏览器都支持,但它确实覆盖了 73% 的用户(截至撰写本文时)。
此浏览器支持数据来自Caniuse,其中包含更多详细信息。 数字表示浏览器在该版本及其更高版本中支持该功能。
桌面
Chrome | Firefox | IE | Edge | Safari |
---|---|---|---|---|
58 | 55 | 否 | 16 | 12.1 |
移动设备/平板电脑
Android Chrome | Android Firefox | Android | iOS Safari |
---|---|---|---|
127 | 127 | 127 | 12.2-12.5 |
还不错。 真的还不错。
但是! 考虑到我们希望向所有用户显示图像(请记住,使用data-url
会完全阻止图像加载),我们需要在指令中添加一个额外的部分。 特别是,我们需要检查浏览器是否支持Intersection Observer,如果它不支持,则改为触发loadImage
。 这将是我们的后备方案。
if (window["IntersectionObserver"]) {
createObserver();
} else {
loadImage();
}
总结
延迟加载图像可以显著提高页面性能,因为它会将图像占用页面的重量,仅在用户实际需要时才加载它们。
对于那些仍然不确定延迟加载是否值得一试的人,这里有一些我们一直在使用的简单示例中的原始数字。 该列表包含 11 篇文章,每篇文章一张图片。 总共 11 张图片(数学!)。 这不像有很多图片,但我们仍然可以处理它。
以下是我们在 3G 连接上渲染所有 11 张图片而没有延迟加载的结果。

11 个图像请求总共导致页面大小为 3.2 MB。 哇!
以下是同一个页面使用延迟加载的结果。

你说什么? 只有一个请求,一张图片。 我们的页面现在是 1.4 MB。 我们节省了 10 个请求,并将页面大小减少了 56%。
这是一个简单而孤立的例子吗? 是的,但数字仍然说明了一切。 希望您发现延迟加载是一种有效的对抗页面膨胀的方法,并且这种使用 Vue 和 Intersection Observer 的特定方法很实用。
我不明白 11 张图片怎么会突然变成 1 张图片。
您只加载视窗中的内容,按需加载。 这有点像无限滚动原理。
想知道如果 Observer 只创建一次,在导出函数的作用域之外,是否会提高性能。 另外,就个人而言,将函数放在模块的根部 (?) 作用域中并重构元素如何通过函数传递的方式,使函数更纯粹,感觉会更好。 能够使用指令为
IntersectionObserver
选项传递一个值会很好,也许只有当具有这些选项的观察者不存在时才会创建不同的观察者。我还没有真正深入研究服务器端渲染,但也许在组件/指令在服务器端渲染时添加一些动态
<noscript>
标签可以进一步改进。非常有趣的文章,并且自定义指令提供了很多新的机会 :)
但是,您将如何处理内容不会跳动的情况? 如果图像出现在视窗中,图像加载后下面的文本会向下跳动,这非常令人讨厌。 我认为您无法“预留”空间,因为您不知道图像的尺寸。
谢谢 :)
有很多方法可以解决这个问题。 您可以查看所有框架中现有的插件。
通常,如果内容来自 CMS 或 CDN,则会存在图像 API,您可以检索大小,或者使用低分辨率图像,模糊,调整大小和缩放。
这对 SEO 有什么影响?