深入了解图片懒加载🖼
🟠 本文写于 4 年前,一些内容(例如阈值)已经有所改进。
第一个问题是……为什么?
在当今的Web应用世界中,用户访问网页时节省时间和网络流量意味着更高的用户参与度,并能带来更佳的用户体验。相信我,大多数情况下,用户加载网页时,我们都在浪费大量资源,例如网络带宽。
无需成为专家也能意识到,如果网络开发中最大的问题之一是浪费资源,那么解决方案就是阻止用户的手机和电脑浪费资源,对吧?
不要加载超过你实际需要的量。
这个概念不仅源于网页开发,也源于游戏开发。在游戏开发领域,他们称之为视锥体剔除,根据维基百科的解释,它是:
将完全位于视锥体之外的物体从渲染过程中移除。渲染这些物体是浪费时间,因为它们不直接可见。
如果我们把这句话翻译成网页开发环境,我们可以看到我们的视锥体就是网页的首屏。
为什么我认为原生懒加载不是一个可行的选择
从 Chrome 76 开始,您可以使用 loading 属性来实现资源的延迟加载,而无需编写自定义的延迟加载代码或使用单独的 JavaScript 库。我第一次在一个网站上实现图片延迟加载策略时就是这么做的,但是代码实现之后……却没有任何效果。为什么呢?
为了更好地理解发生了什么,我决定深入研究 Chromium 代码,更好地了解 Chromium 工程师是如何实现他们的延迟加载解决方案的,以便了解我哪里做错了。
原生懒加载是如何工作的?
浏览器将调用下一个函数以初始化图像监控,实现延迟加载。请查看此处的代码:
void LazyImageHelper::StartMonitoring(blink::Element* element) {
Document* document = GetRootDocumentOrNull(element);
if (!document)
return;
// Getting messages in order to perform console.log operations latter if an attribute is not ok.
using DeferralMessage = LazyLoadImageObserver::DeferralMessage;
auto deferral_message = DeferralMessage::kNone;
if (auto* html_image = ToHTMLImageElementOrNull(element)) {
// Get loading att value, it can be eager, lazy auto or nothing.
LoadingAttrValue loading_attr = GetLoadingAttrValue(*html_image);
DCHECK_NE(loading_attr, LoadingAttrValue::kEager);
if (loading_attr == LoadingAttrValue::kAuto) {
deferral_message = DeferralMessage::kLoadEventsDeferred;
} else if (!IsDimensionAbsoluteLarge(*html_image)) {
DCHECK_EQ(loading_attr, LoadingAttrValue::kLazy);
deferral_message = DeferralMessage::kMissingDimensionForLazy;
}
}
// Here is where all start: Call the lazy load image observer and start monitoring
document->EnsureLazyLoadImageObserver().StartMonitoringNearViewport(
document, element, deferral_message);
}
这段代码片段会触发StartMonitoringNearViewport以下函数:
void LazyLoadImageObserver::StartMonitoringNearViewport(
Document* root_document,
Element* element,
DeferralMessage deferral_message) {
DCHECK(RuntimeEnabledFeatures::LazyImageLoadingEnabled());
if (!lazy_load_intersection_observer_) { // 1
lazy_load_intersection_observer_ = IntersectionObserver::Create(
{Length::Fixed(
GetLazyImageLoadingViewportDistanceThresholdPx(*root_document))}, // 2
{std::numeric_limits<float>::min()}, root_document,
WTF::BindRepeating(&LazyLoadImageObserver::LoadIfNearViewport, // 3
WrapWeakPersistent(this)));
}
为了便于理解,我在一些线条上标上了数字,下面我会解释这些数字的含义。
这段代码具体做了什么?
1 - 他们检查是否已创建交叉路口观察器,否则他们创建它。
你没看出来吗?他们用的是和 JavaScript库一样的原生图片懒加载实现方式,但用的是底层的交叉观察者 API,是不是很神奇?🙂
2 - 调用GetLazyLoadImageLoadingViewportDistanceThresholdPX:此函数将根据您使用的网络获取加载图像所需的阈值。
这里是代码实现,但如果您不关心实现细节,可以直接跳到下表了解有关阈值的更多信息:
int GetLazyImageLoadingViewportDistanceThresholdPx(const Document& document) {
const Settings* settings = document.GetSettings();
if (!settings)
return 0;
switch (GetNetworkStateNotifier().EffectiveType()) {
case WebEffectiveConnectionType::kTypeUnknown:
return settings->GetLazyImageLoadingDistanceThresholdPxUnknown();
case WebEffectiveConnectionType::kTypeOffline:
return settings->GetLazyImageLoadingDistanceThresholdPxOffline();
case WebEffectiveConnectionType::kTypeSlow2G:
return settings->GetLazyImageLoadingDistanceThresholdPxSlow2G();
case WebEffectiveConnectionType::kType2G:
return settings->GetLazyImageLoadingDistanceThresholdPx2G();
case WebEffectiveConnectionType::kType3G:
return settings->GetLazyImageLoadingDistanceThresholdPx3G();
case WebEffectiveConnectionType::kType4G:
return settings->GetLazyImageLoadingDistanceThresholdPx4G();
}
NOTREACHED();
return 0;
}
根据原生配置的 JSON5代码,我们可以看到,对于我们的网络连接,会存在一个或多个阈值,但该阈值始终大于等于 3000 像素,这实际上已经很大了。
| 网络 | 临界点 |
|---|---|
| 慢速 2g | 8000像素 |
| 2克 | 6000像素 |
| 3克 | 4000像素 |
| 4克 | 3000像素 |
| 离线 | 8000像素 |
| 未知 | 5000像素 |
3 - 最后,调用“回调”函数,该函数将执行以下操作(查看完整代码片段):
void LazyLoadImageObserver::LoadIfNearViewport(
const HeapVector<Member<IntersectionObserverEntry>>& entries) {
DCHECK(!entries.IsEmpty());
for (auto entry : entries) {
Element* element = entry->target();
auto* image_element = DynamicTo<HTMLImageElement>(element);
// If the loading_attr is 'lazy' explicitly, we'd better to wait for
// intersection.
if (!entry->isIntersecting() && image_element &&
!EqualIgnoringASCIICase(image_element->FastGetAttribute(html_names::kLoadingAttr), "lazy")) {
// Fully load the invisible image elements. The elements can be invisible
// by style such as display:none, visibility: hidden, or hidden via
// attribute, etc. Style might also not be calculated if the ancestors
// were invisible.
const ComputedStyle* style = entry->target()->GetComputedStyle();
if (!style || style->Visibility() != EVisibility::kVisible ||
style->Display() == EDisplay::kNone) {
// Check that style was null because it was not computed since the
// element was in an invisible subtree.
DCHECK(style || IsElementInInvisibleSubTree(*element));
image_element->LoadDeferredImage();
lazy_load_intersection_observer_->unobserve(element);
}
}
if (!entry->isIntersecting())
continue;
if (image_element)
image_element->LoadDeferredImage();
// Load the background image if the element has one deferred.
if (const ComputedStyle* style = element->GetComputedStyle())
style->LoadDeferredImages(element->GetDocument());
lazy_load_intersection_observer_->unobserve(element);
}
}
您可以在这里查看其他人对此话题的观点。
所以你的意思是说我应该使用一个JS库,但是……用哪个库呢?
我参考了 web.dev 上的文章《延迟加载图像和视频》,花了一些时间分析了我们有哪些不同的选择,以及其中一些选择的优缺点。
分析现有技术
首先,我根据 web.dev 的建议,查看了目前市场上有哪些解决方案,以及它们的维护情况和在社区中的受欢迎程度。
我们有 4 个建议,它们都依赖于 IntersectionObserver API 来执行其工作。
我将使用以下五个指标对它们进行分析:
- 星星
- 发布
- 使用它的公共存储库
- 贡献者
- 图书馆规模
- NPM 下载趋势
Github
| 库名称 | ⭐️ 星星 | 🚀 发布 | 📦 使用对象 | 👥 贡献者 | 🏋🏽♂️ 尺寸 |
|---|---|---|---|---|---|
| 洛萨德 | 6.2k | 17 | 1.5k | 31 | 1kb |
| 布雷兹 | 2.6k | 19 | 541 | 3 | 1.9kb |
| 你们 | 1千 | 13 | 69 | 13 | 1kb |
| 懒人尺寸 | 13.3k | 100 | 11.2k | 38 | 3.3kb |
NPM趋势
结论
lazysizes 似乎是社区支持度最高的库,但也是最重的库,所以我将选择两个库进行测试和基准测试。
- Lazysizes
- 洛萨德
实地测试
为了检验哪个库的 API 更好,我决定在 codesandbox 网站上进行一个小测试,并检查每种实现的表现。
洛萨德:
import React, { useEffect } from 'react';
import lozad from 'lozad';
export default ({ src, ...other }) => {
const { observe } = lozad();
useEffect(() => {
observe();
}, []);
return <img className="lozad" data-src={src} {...other} />;
};
Lozad使用 className 作为库的标识符,以便将 data-src 替换为真正的 src 属性来加载图像。
它还使用了一个观察函数来观察元素。观察函数会将元素标记为已加载,因此多次调用该函数完全不会影响性能。您可以在 load.js 源代码中查看该函数的代码实现 -这里。
LazySizes:
import React from 'react';
import 'lazysizes';
import 'lazysizes/plugins/attrchange/ls.attrchange';
export default ({ src, ...other }) => {
return <img className="lazyload" data-src={src} {...other} />;
};
LazySizes 的 API 与 lozad 类似,但无需调用 observe 函数,它会在导入时自动调用。另一方面,如果您动态更改 data-src 值,则需要添加一个插件来监视 data-src 值,以便在它更改时重新触发图像加载函数。
更多关于 ls.attrchange 的信息请点击此处
总结:利与弊
Lozad PROS 👍
- Lozad是一个非常小的库(只有1kb!)
- Lozad 非常易于使用,并赋予我们调用 observe 和 unobserve 方法的自主权。
- 它只会加载需要加载的内容,默认阈值为(移动设备上为 2 张图片)。
- 它是可配置的
Lozad CONS 👎
- 我不喜欢在每个组件上运行 observable,即使这不会造成性能问题,我也不希望在惰性图像组件定义之外使用 lozad.observe,解决方案必须按原样提供,不能有任何额外的工作。
- 他们没有明确说明该图书馆是否符合搜索引擎优化 (SEO) 标准,如果您重视 SEO,这将是一个问题——更多信息请点击此处。
LazySizes 的优点👍
- 这个API真的很容易使用。
- 背后的社区非常庞大。
- 这是谷歌推荐的图书馆。
- 它完全符合搜索引擎优化 (SEO) 标准。
- 它可以通过插件扩展容量,点击此处查看。
- 它还是可配置的。
- 它开箱即用,你只需要导入库即可。
LazySizes 的缺点👎
- 图书馆规模是 lozad 的三倍
- 如果要进行配置,则必须在窗口上放置一个配置对象,这不太优雅。
如果你在意SSR,需要考虑的一般权衡是:
- 我们使用一个库来实现图片懒加载,该库会被导入并添加到我们的打包文件中。这意味着我们会失去图片服务器端渲染(SSR)的优势,因为这段JS代码必须在首次渲染时加载才能显示图片。但至少对于你的打包文件中需要加载大量JS代码的情况来说,这应该不会造成问题。
结论
在我看来,在这种情况下,社区和谷歌选择了正确的库来信任它,惰性求值的大小略有不同,这为我们提供了大小、可用性和可维护性之间的平衡。
我的建议是给 lazysizes 一个机会,测试一下它的可行性。
题图由 Kate Stone Matheson 拍摄,来自 Unsplash
文章来源:https://dev.to/carlesnunez/deep-dive-into-lazy-loading-images-211f

