JavaScript怎么实现图片懒加载

151 阅读9分钟

图片懒加载实现方案:原理、代码与优化(原生 / 框架通用)

图片懒加载(Lazy Loading)是 优化页面性能的核心手段—— 核心原理是:图片进入(或即将进入)浏览器可视区域时,才加载图片资源(替换 src 或 srcset),避免页面初始化时加载所有图片导致的资源浪费、加载缓慢问题。

以下是 3 种主流实现方案(原生 / Intersection Observer / 框架适配),覆盖从简单场景到生产环境的全需求,附完整代码和优化细节。

一、核心概念铺垫

1. 为什么需要懒加载?

  • 长页面(如电商列表、新闻流)通常包含大量图片,初始化加载所有图片会导致:

    • 网络请求过多,带宽占用大(尤其移动端);
    • 页面加载时间延长,首屏渲染慢;
    • 内存占用过高,可能导致页面卡顿。
  • 懒加载只加载 “用户能看到” 的图片,显著提升首屏加载速度和用户体验。

2. 实现核心思路

  1. 页面初始化时,图片元素不设置真实 src(避免默认加载),而是将真实地址存放在自定义属性中(如 data-src/data-srcset);
  2. 监听图片是否进入可视区域(通过滚动事件、Intersection Observer API 等);
  3. 图片进入可视区域时,将 data-src 赋值给 src(或 data-srcset 赋值给 srcset),触发图片加载;
  4. 加载完成后,移除监听(避免重复触发)。

二、方案 1:原生滚动监听(兼容旧浏览器,简单易懂)

最基础的实现方式:监听 scroll 事件,通过 getBoundingClientRect() 判断图片是否进入可视区域。适合简单场景或需要兼容旧浏览器(如 IE)的情况。

1. 实现步骤

(1)HTML 结构(关键:用 data-src 存真实地址)
<!-- 懒加载图片:src 用占位图(可选),data-src 存真实地址 -->
<img 
  class="lazy" 
  data-src="https://example.com/real-img-1.jpg" 
  src="https://example.com/placeholder.jpg"  <!-- 占位图(可选,优化用户体验) -->
  alt="商品图片"
  width="300" 
  height="200"  <!-- 建议设置宽高,避免布局偏移(CLS) -->
>
<img 
  class="lazy" 
  data-src="https://example.com/real-img-2.jpg" 
  src="https://example.com/placeholder.jpg"
  alt="商品图片"
  width="300" 
  height="200"
>
(2)CSS 优化(可选:占位图样式、淡入效果)
/* 占位图样式(灰色背景+居中文字) */
.lazy {
  background: #f5f5f5;
  display: inline-block;
  overflow: hidden;
}
/* 图片加载完成后淡入(优化体验) */
.lazy.loaded {
  opacity: 1;
  transition: opacity 0.3s ease;
}
.lazy:not(.loaded) {
  opacity: 0;
}
(3)JavaScript 实现(原生无依赖)
// 1. 获取所有懒加载图片
const lazyImages = document.querySelectorAll('img.lazy');

// 2. 核心判断:图片是否进入可视区域
function isInViewport(el) {
  const rect = el.getBoundingClientRect();
  // 可视区域高度(窗口高度 + 额外偏移量100px,提前加载,优化体验)
  const viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
  // 图片顶部 <= 可视区域底部 + 偏移量,且图片底部 >= 可视区域顶部
  return (
    rect.top <= viewHeight + 100 &&
    rect.bottom >= 0
  );
}

// 3. 加载图片(替换 data-src 到 src)
function loadImage(el) {
  if (el.dataset.src && !el.classList.contains('loaded')) {
    // 替换真实地址
    el.src = el.dataset.src;
    // 处理 srcset(适配响应式图片,可选)
    if (el.dataset.srcset) {
      el.srcset = el.dataset.srcset;
    }
    // 加载完成后添加类名(触发淡入效果)
    el.onload = () => {
      el.classList.add('loaded');
    };
    // 移除自定义属性(避免重复处理)
    delete el.dataset.src;
    delete el.dataset.srcset;
  }
}

// 4. 批量处理所有图片
function handleLazyLoad() {
  lazyImages.forEach(el => {
    if (isInViewport(el)) {
      loadImage(el);
    }
  });
}

// 5. 监听触发事件(滚动、窗口 resize、页面加载完成)
window.addEventListener('scroll', handleLazyLoad);
window.addEventListener('resize', handleLazyLoad); // 窗口大小变化时重新判断
window.addEventListener('load', handleLazyLoad); // 页面初始化时先处理一次(首屏可见图片)

// 6. 初始执行一次(处理首屏图片)
handleLazyLoad();

2. 优点与缺点

  • 优点:原生无依赖、兼容所有浏览器(包括 IE)、实现简单易懂;
  • 缺点:scroll 事件触发频率高(每滚动 1px 就触发),可能导致性能问题(尤其长列表);需手动处理窗口 resize 等场景。

3. 性能优化(可选)

通过 throttle(节流)减少 scroll 事件触发次数(推荐使用 Lodash 的 throttle,或原生实现):

// 原生节流函数(避免依赖 Lodash)
function throttle(fn, delay = 100) {
  let lastTime = 0;
  return () => {
    const now = Date.now();
    if (now - lastTime >= delay) {
      fn();
      lastTime = now;
    }
  };
}

// 用节流包装处理函数(100ms 内仅触发一次)
const throttledLazyLoad = throttle(handleLazyLoad);
window.addEventListener('scroll', throttledLazyLoad);
window.addEventListener('resize', throttledLazyLoad);

三、方案 2:Intersection Observer API(推荐生产环境,高性能)

Intersection Observer 是浏览器原生 API(现代浏览器支持),专门用于监听 “元素是否进入可视区域”,无需手动监听 scroll 事件,性能更优(浏览器底层优化,触发频率低)。

1. 实现步骤

(1)HTML/CSS 结构(与方案 1 完全一致)
<img 
  class="lazy" 
  data-src="https://example.com/real-img-1.jpg" 
  src="https://example.com/placeholder.jpg"
  alt="商品图片"
  width="300" 
  height="200"
>
(2)JavaScript 实现(高性能核心)
// 1. 兼容性判断(可选:降级处理旧浏览器)
if ('IntersectionObserver' in window) {
  // 2. 创建观察者实例(配置回调函数和观察选项)
  const lazyObserver = new IntersectionObserver((entries, observer) => {
    // entries:所有被观察元素的状态变化数组
    entries.forEach(entry => {
      // entry.isIntersecting:元素是否进入可视区域
      if (entry.isIntersecting) {
        const img = entry.target; // 当前进入可视区域的图片
        // 加载图片(替换 data-src 到 src)
        if (img.dataset.src) {
          img.src = img.dataset.src;
          if (img.dataset.srcset) {
            img.srcset = img.dataset.srcset;
          }
          // 加载完成后添加淡入效果
          img.onload = () => {
            img.classList.add('loaded');
          };
          // 停止观察(避免重复处理)
          observer.unobserve(img);
          // 移除自定义属性
          delete img.dataset.src;
          delete img.dataset.srcset;
        }
      }
    });
  }, {
    rootMargin: '100px 0px', // 提前100px加载(优化体验,避免用户看到占位图)
    threshold: 0.1 // 元素进入可视区域10%时触发回调
  });

  // 3. 观察所有懒加载图片
  document.querySelectorAll('img.lazy').forEach(img => {
    lazyObserver.observe(img);
  });
} else {
  // 降级处理:旧浏览器使用方案1的滚动监听
  handleLazyLoad(); // 方案1中的 handleLazyLoad 函数
  window.addEventListener('scroll', throttle(handleLazyLoad));
  window.addEventListener('resize', throttle(handleLazyLoad));
}

2. 核心配置说明(IntersectionObserver 选项)

选项含义
root观察的根元素(默认是视口 viewport),可指定某个 DOM 元素(如滚动容器)
rootMargin根元素的外边距(提前 / 延迟触发),如 100px 0px 表示提前 100px 加载
threshold触发回调的阈值(元素进入可视区域的比例),如 0.1 表示 10% 进入时触发

3. 优点与缺点

  • 优点:高性能(浏览器底层优化,无频繁事件触发)、代码简洁、支持自定义观察规则(如提前加载);
  • 缺点:不兼容 IE 浏览器(现代浏览器均支持,如 Chrome 51+、Firefox 55+、Edge 16+)。

4. 兼容性处理(可选)

如果需要兼容 IE,可使用 polyfill(推荐 intersection-observer 库):

# 安装 polyfill
npm install intersection-observer --save

在项目入口文件(如 main.js)引入:

import 'intersection-observer'; // 引入后,IE 会支持 IntersectionObserver

四、方案 3:框架适配(Vue/React 项目专用)

在 Vue/React 等框架中,可基于上述原理封装成组件或指令,更贴合框架开发习惯。以下以 Vue 3 指令 和 React 组件 为例:

1. Vue 3 自定义指令(推荐)

封装成全局指令 v-lazy,在模板中直接使用:

// main.js(全局注册指令)
import { createApp } from 'vue';
import App from './App.vue';

const app = createApp(App);

// 注册 v-lazy 指令
app.directive('lazy', {
  // 元素挂载到 DOM 时执行
  mounted(el, binding) {
    // binding.value 是指令的值(即真实图片地址)
    el.dataset.src = binding.value;
    el.src = 'https://example.com/placeholder.jpg'; // 占位图

    // 利用 IntersectionObserver 监听
    if ('IntersectionObserver' in window) {
      const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            el.src = el.dataset.src;
            el.onload = () => {
              el.classList.add('loaded');
            };
            observer.unobserve(el);
          }
        });
      }, { rootMargin: '100px 0px' });

      observer.observe(el);
    } else {
      // 降级处理(滚动监听)
      const handleScroll = () => {
        const rect = el.getBoundingClientRect();
        const viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
        if (rect.top <= viewHeight + 100) {
          el.src = el.dataset.src;
          el.onload = () => el.classList.add('loaded');
          window.removeEventListener('scroll', handleScroll);
        }
      };
      window.addEventListener('scroll', handleScroll);
    }
  }
});

app.mount('#app');
模板中使用:
<template>
  <!-- 直接用 v-lazy 绑定真实图片地址 -->
  <img 
    v-lazy="imageUrl" 
    class="lazy" 
    alt="商品图片"
    width="300" 
    height="200"
  >
</template>

<script setup>
const imageUrl = 'https://example.com/real-img.jpg';
</script>

<style>
/* 同方案1的 CSS 样式 */
.lazy { background: #f5f5f5; opacity: 0; transition: opacity 0.3s; }
.lazy.loaded { opacity: 1; }
</style>

2. React 组件封装

封装 LazyImage 组件,直接传入真实地址和占位图:

// LazyImage.jsx
import { useEffect, useRef } from 'react';

const LazyImage = ({ src, placeholder = 'https://example.com/placeholder.jpg', alt, width, height }) => {
  const imgRef = useRef(null);

  useEffect(() => {
    const img = imgRef.current;
    if (!img) return;

    // 初始化占位图
    img.src = placeholder;

    if ('IntersectionObserver' in window) {
      const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            img.src = src;
            img.onload = () => {
              img.classList.add('loaded');
            };
            observer.unobserve(img);
          }
        });
      }, { rootMargin: '100px 0px' });

      observer.observe(img);
      return () => observer.disconnect(); // 组件卸载时停止观察
    } else {
      // 降级处理
      const handleScroll = () => {
        const rect = img.getBoundingClientRect();
        const viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
        if (rect.top <= viewHeight + 100) {
          img.src = src;
          img.onload = () => img.classList.add('loaded');
          window.removeEventListener('scroll', handleScroll);
        }
      };
      window.addEventListener('scroll', handleScroll);
      return () => window.removeEventListener('scroll', handleScroll);
    }
  }, [src, placeholder]);

  return (
    <img
      ref={imgRef}
      alt={alt}
      style={{ width, height, background: '#f5f5f5', opacity: 0, transition: 'opacity 0.3s' }}
      className="lazy"
    />
  );
};

export default LazyImage;
使用组件:
// 父组件
import LazyImage from './LazyImage';

const App = () => {
  return (
    <div>
      <LazyImage
        src="https://example.com/real-img.jpg"
        alt="商品图片"
        width="300"
        height="200"
      />
    </div>
  );
};

export default App;

五、生产环境优化细节(必看)

1. 避免布局偏移(CLS 优化)

  • 图片标签必须设置 width 和 height 属性(或通过 CSS 固定宽高比),让浏览器提前预留空间,避免图片加载后布局偏移(影响 Core Web Vitals 评分);
  • 示例:width="300" height="200" 或 CSS aspect-ratio: 3/2(宽高比 3:2)。

2. 占位图优化

  • 使用低质量占位图(LQIP):先加载模糊的小图,再替换为高清图;
  • 使用纯色占位图:与页面风格一致的纯色背景,提升视觉体验;
  • 避免空占位(无 src):会导致浏览器发送无效请求(如 about:blank),建议用 1x1 透明像素图作为默认占位。

3. 响应式图片支持

如果需要适配不同屏幕尺寸,使用 data-srcset 和 sizes 属性:

<img
  class="lazy"
  data-srcset="
    https://example.com/img-small.jpg 480w,
    https://example.com/img-medium.jpg 768w,
    https://example.com/img-large.jpg 1200w
  "
  data-sizes="(max-width: 600px) 480px, (max-width: 900px) 768px, 1200px"
  src="placeholder.jpg"
  alt="响应式图片"
>

加载时替换:el.srcset = el.dataset.srcset; el.sizes = el.dataset.sizes;

4. 错误处理

图片加载失败时显示备用图:

el.onerror = () => {
  el.src = 'https://example.com/error-img.jpg'; // 备用图
};

5. 优先使用原生 loading="lazy"(极简方案)

现代浏览器(Chrome 76+、Firefox 75+、Edge 79+)支持原生懒加载属性 loading="lazy",无需写 JS 代码,直接在图片标签中使用:

<img
  src="https://example.com/real-img.jpg"
  alt="原生懒加载"
  loading="lazy"  <!-- 原生懒加载属性 -->
  width="300"
  height="200"
>
  • 优点:零 JS 代码、浏览器原生优化、性能最好;
  • 缺点:兼容性有限(不支持 IE 和旧版浏览器)、无法自定义占位图 / 淡入效果;
  • 适用场景:简单场景(无需复杂优化)、目标浏览器是现代浏览器。

六、方案对比与选型建议

方案兼容性性能灵活性(自定义)适用场景
原生滚动监听所有浏览器一般兼容旧浏览器(如 IE)、简单场景
Intersection Observer现代浏览器优秀生产环境、长列表、复杂优化需求
Vue/React 框架封装框架适配优秀Vue/React 项目、组件化开发
原生 loading="lazy"现代浏览器最优极简场景、无需复杂自定义

选型优先级

  1. 生产环境(现代浏览器为主) :Intersection Observer API(方案 2);
  2. Vue/React 项目:框架封装方案(方案 3);
  3. 兼容 IE 或简单场景:原生滚动监听(方案 1);
  4. 极简需求(无自定义) :原生 loading="lazy"(方案 5 优化细节)。

总结

图片懒加载的核心是 “按需加载”,关键在于 高效判断图片是否进入可视区域

  • 简单场景用原生 loading="lazy",零成本实现;
  • 生产环境优先用 Intersection Observer,兼顾性能和灵活性;
  • 框架项目用封装好的指令 / 组件,贴合开发习惯;
  • 优化重点:设置图片宽高(避免 CLS)、添加占位图、提前加载(rootMargin)、错误处理。

按上述方案实现,可显著提升长页面的首屏加载速度和用户体验,是前端性能优化的必备手段。