实现图片懒加载

使用原生 HTML、JavaScript 和 IntersectionObserver API 实现图片懒加载

问题

如何实现图片懒加载,避免一次性加载所有图片导致的性能问题和流量浪费?

解答

为什么需要懒加载

图片是影响网页性能的主要因素,一次性加载所有图片会带来两个问题:

  1. 影响用户体验,页面加载缓慢
  2. 浪费流量,用户可能不会浏览所有内容

懒加载的核心思路是:只加载可视区域内的图片,其他图片在滚动到可视区域时再加载。

方式一:HTML 原生属性

最简单的实现方式是使用 loading 属性:

<img src="./example.jpg" loading="lazy">

该属性兼容性良好,可以直接在生产环境使用。

方式二:JavaScript 实现

基本原理

  1. 图片初始时将真实地址存在 data-src 属性中,src 设为占位图
  2. 监听页面滚动,判断图片是否进入可视区域
  3. 进入可视区域后,将 data-src 的值赋给 src
<!-- 初始状态 -->
<img data-src="http://xx.com/xx.png" src="./img/default.png" />

<!-- 进入可视区域后 -->
<img data-src="http://xx.com/xx.png" src="http://xx.com/xx.png" />

背景图的实现方式类似:

<!-- 初始状态 -->
<div data-src="http://xx.com/xx.png" style="background-image: none;"></div>

<!-- 进入可视区域后 -->
<div data-src="http://xx.com/xx.png" style="background-image: url(http://xx.com/xx.png);"></div>

完整示例

HTML 结构:

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Lazyload</title>
    <style>
      img {
        display: block;
        margin-bottom: 50px;
        height: 200px;
        width: 400px;
      }
    </style>
  </head>
  <body>
    <img src="./img/default.png" data-src="./img/1.jpg" />
    <img src="./img/default.png" data-src="./img/2.jpg" />
    <img src="./img/default.png" data-src="./img/3.jpg" />
    <img src="./img/default.png" data-src="./img/4.jpg" />
    <img src="./img/default.png" data-src="./img/5.jpg" />
  </body>
</html>

懒加载函数:

function lazyload() {
  // 获取可视区高度
  let viewHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
  let imgs = document.querySelectorAll('img[data-src]')
  
  imgs.forEach((item) => {
    if (item.dataset.src === '') return
    
    // 获取元素相对浏览器视窗的位置
    let rect = item.getBoundingClientRect()
    if (rect.bottom >= 0 && rect.top < viewHeight) {
      item.src = item.dataset.src
      item.removeAttribute('data-src')
    }
  })
}

性能优化:节流

直接监听 scroll 事件会导致函数频繁触发,需要使用节流函数优化:

function throttle(fn, delay) {
  let timer
  let prevTime
  return function (...args) {
    const currTime = Date.now()
    const context = this
    if (!prevTime) prevTime = currTime
    clearTimeout(timer)

    if (currTime - prevTime > delay) {
      prevTime = currTime
      fn.apply(context, args)
    } else {
      timer = setTimeout(() => {
        prevTime = currTime
        fn.apply(context, args)
      }, delay)
    }
  }
}

// 使用节流函数
window.addEventListener('scroll', throttle(lazyload, 200))

方式三:IntersectionObserver API

IntersectionObserver 是浏览器原生 API,可以自动观察元素是否进入可视区域,无需手动计算位置和监听滚动事件。

基本用法:

var io = new IntersectionObserver(callback, option)

// 开始观察
io.observe(document.getElementById('example'))

// 停止观察
io.unobserve(element)

// 关闭观察器
io.disconnect()

实现图片懒加载:

const imgs = document.querySelectorAll('img[data-src]')
const config = {
  rootMargin: '0px',
  threshold: 0,
}

let observer = new IntersectionObserver((entries, self) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      let img = entry.target
      let src = img.dataset.src
      if (src) {
        img.src = src
        img.removeAttribute('data-src')
      }
      // 解除观察
      self.unobserve(entry.target)
    }
  })
}, config)

imgs.forEach((image) => {
  observer.observe(image)
})

关键点

  • 使用 loading="lazy" 属性是最简单的实现方式
  • JavaScript 实现需要判断元素是否进入可视区域,使用 getBoundingClientRect() 获取位置信息
  • 监听 scroll 事件时必须使用节流函数优化性能
  • IntersectionObserver API 是更现代的解决方案,无需手动计算位置和处理滚动事件
  • 图片真实地址存储在 data-src 中,进入可视区域后再赋值给 src 属性