图片懒加载

图片懒加载

概念

是什么

图片懒加载,即图片延迟加载。图片未需要时,不加载图片资源;直到图片需要(或即将需要)时,加载图片资源。

为什么

减少不必要的资源请求。

  • 视口区域(用户在网页上可以看到的区域)有限,用户一定时间内看到的内容是有限的,页面不一定要短时间内加载完所有资源内容。
  • 网络的传输效率有限,服务器的处理能力有限,当资源较多时,完成所有资源请求需要较长的时间。
  • 用户等待资源加载耐心有限,使用体验与白屏时间相关。

绑定监听器

实现

  • 对所有图片都设置一个项目中的默认的图片,用作占位并表示未加载(也可不设置),将真正的图片路径存于img标签的自定义属性中。
  • 使用一个数组,存储所有的未被加载的图片。
  • 给所有图片的外部容器绑定一个滚动的监听事件,其中计算哪些未被加载的图片进入显示,将路径赋值给图片的src中。
html 复制代码
<div class="pics">
    <img class="lazy-load" src="../assets/0.png" data-src="src/assets/1.png"/>
</div>
js 复制代码
const lazyImages = ref([])

const init = () => {
    lazyImages.value = [].slice.call(document.querySelectorAll("img.lazy-load"))
    const lazyLoad = () => {
        const el = document.querySelector(".pics")
        lazyImages.value.forEach((img) => {
            if (img.offsetTop < el.clientHeight + el.scrollTop) {
                img.src = img.getAttribute("data-src")
                console.log(img.src)
                img.classList.remove("lazy-load")
            }
        })
    }
    lazyLoad()
    document.querySelector(".pics").addEventListener("scroll", lazyLoad)
}

优化

  • 对滚动事件加上节流,避免触发过于频繁。
  • 如果能提前得知所有图片的宽高,可以通过纯数学计算判断哪个元素被加载显示,不会引起过多的回流。(获取图片offsetTop值时,浏览器会暂停js执行,优先执行完回流重绘队列中的任务)

IntersectionObserver视口监听

IntersectionObserver,一个异步观察目标元素与其祖先元素或顶级文档视口交叉状态的方法。相比于监听器,检测图片是否进入视口更为精确。

  • 为需要懒加载的图片都绑定一个IntersectionObserver事件。
  • 回调函数中,设置图片应有的路径,并停止对该图片的观察。
html 复制代码
<div class="pics">
    <img class="lazy-load" src="../assets/0.png" data-src="src/assets/1.png"/>
</div>
js 复制代码
const lazyImages = ref([])

const init = () => {
    lazyImages.value = [].slice.call(document.querySelectorAll("img.lazy-load"))
    const lazyLoad = new IntersectionObserver((entries) => {
        entries.forEach(item => {
            if (item.isIntersecting) {
                item.target.src = item.target.dataset.src
                lazyLoad.unobserve(item.target)
            }
        })
    })
    lazyImages.value.forEach((image) => {
        lazyLoad.observe(image)
    })
}

与监听器比较

  • 监听器中需要遍历未显示的图片,如果图片数量较多,js执行的耗时会较多。
  • IntersectionObserver仅需对各个图片绑定一次监听即可。

优化

  • 图片过多时,每个图片都绑定一个IntersectionObserver可能会增大性能开销。可以考虑图片分组监听。如10张图片一组,每组图片的第一张图片绑定IntersectionObserver,触发时,请求该组图片的资源。(当然了,IntersectionObserver自身采用了惰性计算,即仅在特定时机计算交叉状态、延迟处理等优化,性能较好)
  • 同上,可以考虑维护一个正在IntersectionObserver监听的图片队列,图片完成监听后退出队列。当队列数量少于一定阈值时,将后续未绑定IntersectionObserver监听的图片加入队列并绑定监听。
  • 总而言之,尝试避免过多监听器存在可能导致的性能问题。

组件库lazy

一般的组件库中,图片组件提供懒加载的配置,如TDesign组件库的t-image标签。

html 复制代码
<t-image :lazy="true" class="lazy-load" src="src/assets/1.png"/>

与前两种方法比较

  • 所需的时间与IntersectionObserver相近,优于监听器。
  • 相比IntersectionObserver,只需设置属性即可,更为简捷。

实现原理

参见:github.com/Tencent/tde...

  • t-image组件创建过程中,如果启用了lazy,会为其外层包裹的div绑定一个IntersectionObserver
  • t-image组件取消挂载时,对存在的IntersectionObserver进行关闭。
ini 复制代码
setup(props: TdImageProps) {
    const divRef = ref<HTMLElement>(null);
    const imgRef = ref<HTMLImageElement>(null);
    let io: IntersectionObserver = null;

    const { src } = toRefs(props);

    const renderTNodeJSX = useTNodeJSX();

    onMounted(() => {
      //在nuxt3中img的onload事件会失效
      if (imgRef.value?.complete && !props.lazy) {
        triggerHandleLoad();
      }

      if (!props.lazy || !divRef.value) return;

      const ioObserver = observe(divRef.value, null, handleLoadImage, 0);
      io = ioObserver;
    });
    onUnmounted(() => {
      divRef.value && io && io.unobserve(divRef.value);
    });
}
  • 其中绑定IntersectionObserver的函数observe,其核心代码如下。
  • 与前面自己使用IntersectionObserver完成监听的操作相近。
ini 复制代码
io = new window.IntersectionObserver(
      (entries) => {
        const entry = entries[0];
        if (entry.isIntersecting) {
          callback();
          io.unobserve(element);
        }
      }
    );

v-lazy

vue中有插件,可以实现图片懒加载。

html 复制代码
<div class="pics">
    <div v-for="item in pictures" :key="item">
        <img v-lazy="item"/>
    </div>
</div>

与前三种方法比较

  • 性能与第二、三种相近,优于第一种。
  • 引入组件后,开盒即用,如果所用组件库无相关配置,是个不错的选择。

实现原理

参见:github.com/murongg/vue...

  • 组件挂载时,为组件调用绑定IntersectionObserver的方法。
kotlin 复制代码
public mount(el: HTMLElement, binding: string | DirectiveBinding<string | ValueFormatterObject>): void {
    if (!el)
      return
    const { src, loading, error, lifecycle, delay } = this._valueFormatter(typeof binding === 'string' ? binding : binding.value)
    this._lifecycle(LifecycleEnum.LOADING, lifecycle, el)
    el.setAttribute('src', loading || DEFAULT_LOADING)
    if (!hasIntersectionObserver) {
      this.loadImages(el, src, error, lifecycle)
      this._log(() => {
        this._logger('Not support IntersectionObserver!')
      })
    }
    this._initIntersectionObserver(el, src, error, lifecycle, delay)
  }
  • IntersectionObserver初始化的方法中,和前面的IntersectionObserver一样,为组件绑定IntersectionObserver
  • 同时,根据是否需要延迟执行分两种处理方式。
typescript 复制代码
private _initIntersectionObserver(el: HTMLElement, src: string, error?: string, lifecycle?: Lifecycle, delay?: number): void {
    const observerOptions = this.options.observerOptions
    this._images.set(el, new IntersectionObserver((entries) => {
      Array.prototype.forEach.call(entries, (entry) => {
        if (delay && delay > 0)
          this._delayedIntersectionCallback(el, entry, delay, src, error, lifecycle)
        else
          this._intersectionCallback(el, entry, src, error, lifecycle)
      })
    }, observerOptions))
    this._realObserver(el)?.observe(el)
  }
  • 对于无需延迟的组件,执行完关闭监听。
php 复制代码
  private _intersectionCallback(el: HTMLElement, entry: IntersectionObserverEntry, src: string, error?: string, lifecycle?: Lifecycle): void {
    if (entry.isIntersecting) {
      this._realObserver(el)?.unobserve(entry.target)
      this._setImageSrc(el, src, error, lifecycle)
    }
  }
  • 对于需延迟的组件,做异步处理。
typescript 复制代码
  private _delayedIntersectionCallback(el: HTMLElement, entry: IntersectionObserverEntry, delay: number, src: string, error?: string, lifecycle?: Lifecycle): void {
    if (entry.isIntersecting) {
      if (entry.target.hasAttribute(TIMEOUT_ID_DATA_ATTR))
        return

      const timeoutId = setTimeout(() => {
        this._intersectionCallback(el, entry, src, error, lifecycle)
        entry.target.removeAttribute(TIMEOUT_ID_DATA_ATTR)
      }, delay)
      entry.target.setAttribute(TIMEOUT_ID_DATA_ATTR, String(timeoutId))
    }
    else {
      if (entry.target.hasAttribute(TIMEOUT_ID_DATA_ATTR)) {
        clearTimeout(Number(entry.target.getAttribute(TIMEOUT_ID_DATA_ATTR)))
        entry.target.removeAttribute(TIMEOUT_ID_DATA_ATTR)
      }
    }
  }
相关推荐
Acacia.~16 分钟前
第八章 利用CSS制作导航栏
前端·css
摆烂工程师39 分钟前
GPT4变笨了?教你解决GPT4降智问题!同时封装了个Chrome扩展程序进行检查GPT
前端·后端·程序员
编程老船长40 分钟前
网页设计基础 第十九讲:CSS定位实战 —— 打造精美布局的个人简介页
前端·css·html
snow_wind_rain1 小时前
网页作业9
前端·css·css3
zhzhzhen_1 小时前
如何在项目中用elementui实现分页器功能
前端·javascript·elementui
向明天乄1 小时前
elementui el-table中给表头 el-table-column 加一个鼠标移入提示说明
前端·javascript·vue.js·elementui
Akiiiira1 小时前
【网页设计】CSS3 进阶(动画篇)
前端·javascript·css3
蒜蓉大猩猩2 小时前
Vue3.js - 一文看懂Vuex
前端·javascript·vue.js·前端框架·html5
excel2 小时前
three EdgeSplitModifier
前端
前端Hardy3 小时前
探索 HTML 和 CSS 实现的 3D旋转相册
前端·css·3d·html·css3