发布于 2026-01-06 11 阅读
0

深入了解图片懒加载🖼

深入了解图片懒加载🖼

🟠 本文写于 4 年前,一些内容(例如阈值)已经有所改进。

第一个问题是……为什么?

在当今的Web应用世界中,用户访问网页时节省时间和网络流量意味着更高的用户参与度,并能带来更佳的用户体验。相信我,大多数情况下,用户加载网页时,我们都在浪费大量资源,例如网络带宽。

无需成为专家也能意识到,如果网络开发中最大的问题之一是浪费资源,那么解决方案就是阻止用户的手机和电脑浪费资源,对吧?

不要加载超过你实际需要的量。

这个概念不仅源于网页开发,也源于游戏开发。在游戏开发领域,他们称之为视锥体剔除,根据维基百科的解释,它是:

将完全位于视锥体之外的物体从渲染过程中移除。渲染这些物体是浪费时间,因为它们不直接可见。

如果我们把这句话翻译成网页开发环境,我们可以看到我们的视锥体就是网页的首屏。

Imgur

为什么我认为原生懒加载不是一个可行的选择

从 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);
}

Enter fullscreen mode Exit fullscreen mode

这段代码片段会触发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)));
  }

Enter fullscreen mode Exit fullscreen mode

为了便于理解,我在一些线条上标上了数字,下面我会解释这些数字的含义。

这段代码具体做了什么?

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;
}

Enter fullscreen mode Exit fullscreen mode

根据原生配置的 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);
  }
}

Enter fullscreen mode Exit fullscreen mode

您可以在这里查看其他人对此话题的观点。

所以你的意思是说我应该使用一个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趋势

https://i.imgur.com/9dglETK.png

结论

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} />;
};

Enter fullscreen mode Exit fullscreen mode

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} />;
};

Enter fullscreen mode Exit fullscreen mode

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