图片懒加载的实践方案

2020.05.12

图片懒加载介绍

图片懒加载是非常常见的功能需求,其目的就是为了提升应用性能防止浏览的浪费。其本质功能就是图片进入用户的可视或者预可视(可视区域+buffer距离)区域才回去加载对应的图片

监听页面的滚动

实现方案

方案非常简单,就是监听window.scroll事件,在事件里判断图片是否已经进入可视(预可视)

注意点

  • window.scroll 只能监听body滚动的事件,因此列表设计样式的时候要让body成为滚动的区域
  • 避免window.scroll事件的覆盖,每个组件中都会去监听window.scroll事件,要保留之前window.scroll 的逻辑。
  const onscroll = window.onscroll || function() {}
  window.onscroll = throttle(() => {
    onscroll()
    this.lazyLoadImg()
  }, 1000, true)
  • 类似滚动这种高频触发事件需要节流(前端必备常识)

代码如下:

  // lazyLoadImg 函数实现
  // 已经加载过的就直接返回
  if (this.startedLoad) {
    return
  }
  // 已经元素没有进入视口区域,则不加载背景图片
  const scrollTop = document.documentElement.scrollTop || document.body.scrollTop // 滚动条距离顶部高度
  // 元素的滚动距离 > 视口高度 + body 滚动距离 + 某一常量时触发
  if (this.container.offsetTop > screenHeight + scrollTop + BUFFER_HEIGHT) {
    return
  }
  const contentElement = this.$refs.content
  if (!contentElement) return
  this.startedLoad = true
  const image = new Image()
  image.src = this.imgUrl
  image.onload = () => {
    contentElement.style.backgroundSize = 'cover'
    contentElement.style.backgroundImage = `url(${this.imgUrl})`
    this.loaded = true
  }

遗留问题

组件卸载掉后需要还原scroll事件,但是原来的事件早就已经被覆盖掉很难找回了,除非一开始就给它预留好,但是这样做代码逻辑就会变得很繁琐。

使用 IntersectionObserver 代替 window.scroll

使用 IntersectionObserver 方案可以避免方案一带来的问题,IntersectionObserver 是 Html5 新出的API,本身的作用就是用来监测元素是否在浏览器的可视区域。

    this.container = this.$refs.sectionContainer
    const observer = new IntersectionObserver(
      (changes) => {
        changes.forEach((change) => {
          const { boundingClientRect: { y: offsetTop }, rootBounds: { height: screenHeight }} = change
          if (offsetTop < screenHeight + BUFFER_HEIGHT) {
            this.lazyLoadImg()
            // 加载完成关闭观察器
            observer.disconnect()
          }
        })
      }
    )
    observer.observe(this.$refs.sectionContainer)

lazyLoadImg 函数也会变得更简单

    const contentElement = this.$refs.content
    if (!contentElement) return
    this.startedLoad = true
    const image = new Image()
    image.src = this.imgUrl
    image.onload = () => {
      contentElement.style.backgroundSize = 'cover'
      contentElement.style.backgroundImage = `url(${this.imgUrl})`
      this.loaded = true
    }

注意的点

发现 IntersectionObserver 这个 api 有个坑,就是在移动 boundingClientRect 对象中并没有 x 这个属性, 需要用 top 属性代替,而且 IntersectionObserver 的支持度不是很好,目前的做法是实现降级处理,如果不支持就采用 scroll 的方法。
当然也可以引入相应的 polyfill (https://polyfill.io/v2/polyfill.min.js?features=IntersectionObserver)。