前端性能优化-图片懒加载技术

前端性能优化:图片懒加载全攻略,3种实战方案+避坑详解

在前端性能优化体系中,图片资源往往是页面加载的"重灾区" ------电商列表、资讯长文、相册类页面,动辄十几张甚至上百张图片,若全量一次性加载,不仅拖慢首屏渲染、抢占带宽,还会造成大量无效请求。

图片懒加载 作为针对性极强的优化手段,核心逻辑是非首屏图片延迟加载,进入可视区域再请求真实资源,既能大幅降低首屏加载耗时,又能节省流量、提升页面流畅度,更是优化 LCP、CLS 等 Core Web Vitals 核心指标的关键。

本文专注拆解图片懒加载,从原理、适用场景、3种落地实现方案,到避坑指南、效果验证

一、先理清:图片懒加载的核心原理

图片懒加载没有复杂底层逻辑,本质是 "阻断默认加载 + 监听可视状态 + 动态替换资源" 的闭环流程,针对浏览器默认自上而下加载图片的机制做优化:

  1. 标记占位 :不直接将图片真实地址放入 src 属性(避免默认加载),改用 data-src等自定义属性存储真实地址,src 填充占位图(loading图、纯色占位、极小缩略图);
  2. 监听状态:监听页面滚动、元素位置,判断图片是否进入浏览器可视区域;
  3. 加载资源 :满足可视条件后,将 data-src 中的真实地址赋值给 src,完成图片加载,同时移除监听避免重复执行。

简单来说:先用占位图"糊弄"浏览器,等用户快看到图片时,再加载真实图片。

二、图片懒加载的适用场景

图片懒加载并非所有场景都适用,精准落地以下场景,优化收益最大化:

  • 长页面图片列表:电商商品页、资讯文章、瀑布流相册、短视频封面墙;
  • 非首屏图片:页面底部、折叠面板、弹窗内的图片,用户初始浏览不到的资源;
  • 大体积图片:高清banner、详情图、实拍图,单张体积超过100KB的资源。

禁忌场景:首屏核心图片(Logo、首屏banner、导航图标)严禁懒加载,否则会恶化首屏渲染速度。

三、实战:图片懒加载3种实现方案(从基础到进阶)

针对不同项目兼容性、性能要求,整理3种最常用的图片懒加载方案,覆盖原生JS、浏览器原生API、HTML原生属性,按需选择即可。

方案1:原生JS + 滚动监听 + getBoundingClientRect(兼容老浏览器)

这是最基础、兼容性最强的方案,通过监听 scroll 滚动事件,结合 getBoundingClientRect() 获取图片元素位置,判断是否进入视口,支持 IE 等老旧浏览器,适合老项目改造。

核心要点 :搭配节流函数优化scroll 高频触发问题,减少性能损耗。

完整代码实现
html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>图片懒加载-滚动监听版</title>
  <style>
    img {
      width: 100%;
      max-width: 800px;
      /* 固定宽高比,防止布局偏移(CLS) */
      aspect-ratio: 16/9;
      object-fit: cover;
      background: #f5f7fa;
      margin: 20px auto;
      display: block;
    }
  </style>
</head>
<body>
  <!-- 懒加载图片:data-src存真实地址,src为占位图 -->
  <img class="lazy-img" data-src="https://picsum.photos/800/450?1" src="loading.svg" alt="懒加载图片1">
  <img class="lazy-img" data-src="https://picsum.photos/800/450?2" src="loading.svg" alt="懒加载图片2">
  <img class="lazy-img" data-src="https://picsum.photos/800/450?3" src="loading.svg" alt="懒加载图片3">
  <img class="lazy-img" data-src="https://picsum.photos/800/450?4" src="loading.svg" alt="懒加载图片4">

  <script>
    // 1. 获取所有懒加载图片
    const lazyImages = document.querySelectorAll('.lazy-img');
    // 2. 节流函数:控制scroll触发频率,避免频繁执行
    const throttle = (fn, delay = 200) => {
      let timer = null;
      return (...args) => {
        if (!timer) {
          timer = setTimeout(() => {
            fn.apply(this, args);
            timer = null;
          }, delay);
        }
      };
    };

    // 3. 核心:判断图片是否进入可视区域
    const lazyLoad = () => {
      lazyImages.forEach((img) => {
        // 获取图片相对于视口的位置信息
        const rect = img.getBoundingClientRect();
        // 判定条件:图片顶部 ≤ 视口高度 且 图片底部 ≥ 0(进入可视区域)
        const isInView = rect.top <= window.innerHeight && rect.bottom >= 0;
        
        if (isInView) {
          // 替换真实图片地址
          img.src = img.dataset.src;
          // 加载失败兜底图
          img.onerror = () => { img.src = 'error.svg'; };
          // 移除懒加载类,避免重复处理
          img.classList.remove('lazy-img');
        }
      });
    };

    // 初始化:加载首屏图片
    lazyLoad();
    // 监听滚动事件(节流优化)
    window.addEventListener('scroll', throttle(lazyLoad));
    // 监听窗口缩放,适配不同屏幕
    window.addEventListener('resize', throttle(lazyLoad));
  </script>
</body>
</html>
方案优缺点
  • ✅ 优点:兼容性拉满,逻辑简单,无需依赖第三方库,易调试;
  • ❌ 缺点:scroll 事件触发频率高,即使节流仍有一定性能损耗,需手动处理边界场景。

方案2:Intersection Observer API(现代浏览器首选)

Intersection Observer 是浏览器原生提供的异步交叉观察器,专门用于监听元素与视口(或父容器)的交叉状态,无需手动监听滚动事件,由浏览器底层优化,性能远超滚动监听方案,是目前主流的图片懒加载实现方式。

核心优势:异步执行、无性能损耗、支持提前加载、配置灵活。

完整代码实现
html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>图片懒加载-Intersection Observer版</title>
  <style>
    img {
      width: 100%;
      max-width: 800px;
      aspect-ratio: 16/9;
      object-fit: cover;
      background: #f5f7fa;
      margin: 20px auto;
      display: block;
    }
  </style>
</head>
<body>
  <img class="lazy-img" data-src="https://picsum.photos/800/450?1" src="loading.svg" alt="懒加载图片1">
  <img class="lazy-img" data-src="https://picsum.photos/800/450?2" src="loading.svg" alt="懒加载图片2">
  <img class="lazy-img" data-src="https://picsum.photos/800/450?3" src="loading.svg" alt="懒加载图片3">
  <img class="lazy-img" data-src="https://picsum.photos/800/450?4" src="loading.svg" alt="懒加载图片4">

  <script>
    // 1. 创建观察器实例
    const imgObserver = new IntersectionObserver((entries, observer) => {
      entries.forEach(entry => {
        // 判断图片是否进入可视区域
        if (entry.isIntersecting) {
          const img = entry.target;
          // 加载真实图片
          img.src = img.dataset.src;
          // 错误兜底
          img.onerror = () => { img.src = 'error.svg'; };
          // 取消观察,避免重复触发
          observer.unobserve(img);
        }
      });
    }, {
      // 配置项:提前10%视口高度触发,提升用户体验
      rootMargin: '10% 0px',
      // 触发阈值:0表示元素刚进入视口就加载
      threshold: 0
    });

    // 2. 遍历所有图片,开启观察
    document.querySelectorAll('.lazy-img').forEach(img => {
      imgObserver.observe(img);
    });
  </script>
</body>
</html>
关键配置项解析
  • root:监听的根容器,默认是浏览器视口,可指定父容器实现局部滚动懒加载;
  • rootMargin:扩展触发边界,正值提前加载 ,负值延迟加载(例:10% 0px 表示图片距离视口底部10%高度时就加载);
  • threshold:元素可见比例,取值0-1,0为刚可见就触发,1为完全可见才触发。
方案优缺点
  • ✅ 优点:性能极致、代码简洁、支持预加载、无需处理节流/缩放;
  • ❌ 缺点:不兼容 IE 浏览器,可引入 polyfill 做兼容处理。

方案3:HTML原生loading属性(极简零代码)

现代浏览器(Chrome 77+、Firefox 75+、Edge 79+)支持原生 loading="lazy" 属性,无需编写任何 JS 代码,直接在 img 标签添加该属性,浏览器自动实现图片懒加载,是最简单、最轻量的方案。

适用场景:新项目、无需兼容老旧浏览器、追求极简开发的场景。

代码实现
html 复制代码
<!-- 原生懒加载:仅需添加 loading="lazy" -->
<img 
  src="https://picsum.photos/800/450?1" 
  loading="lazy" 
  alt="原生懒加载图片"
  width="800"
  height="450"
>
<img 
  src="https://picsum.photos/800/450?2" 
  loading="lazy" 
  alt="原生懒加载图片"
  width="800"
  height="450"
>
注意事项
  • 必须设置图片 widthheight,否则浏览器无法判断布局,可能失效;
  • 首屏图片不建议使用,浏览器可能会强制加载首屏内的图片;
  • 兼容性有限,老旧浏览器会忽略该属性,直接加载图片(优雅降级)。

四、图片懒加载避坑指南(实战必看)

实操图片懒加载时,这几个坑极易忽略,直接影响用户体验和性能指标:

1. 严防布局偏移(CLS)

这是最常见问题:图片未加载时无固定占位高度,加载后撑开页面导致布局抖动,CLS指标超标。

解决方案 :通过 CSS aspect-ratio 固定宽高比,或提前设置 width/height,预留图片空间。

2. 占位图优化

  • 占位图体积尽量小(建议<2KB),推荐使用 SVG 占位图、纯色背景或 Base64 缩略图;
  • 避免使用高清图做占位,失去懒加载意义。

3. 图片加载失败兜底

网络异常、图片地址失效会导致图片加载失败,需通过 onerror 事件替换兜底图,提升体验。

4. 及时销毁监听/观察器

JS 实现的懒加载,图片加载完成后务必移除滚动监听、取消 Intersection Observer 观察,防止内存泄漏。

5. 兼容禁用JS场景

部分用户禁用浏览器 JS,会导致图片无法加载,通过 <noscript> 标签兜底。

html 复制代码
<img class="lazy-img" data-src="real.jpg" src="loading.svg" alt="图片">
<!-- 禁用JS时直接加载真实图片 -->
<noscript>
  <img src="real.jpg" alt="图片" width="800" height="450">
</noscript>

五、优化效果验证工具

图片懒加载落地后,通过以下工具验证优化效果:

  1. Chrome 开发者工具:打开 Network 面板,筛选 Img,滚动页面观察图片请求是否延迟触发;
  2. Lighthouse:生成性能报告,查看 LCP(最大内容绘制)、CLS(累积布局偏移)指标是否优化;
  3. Performance 面板:查看首屏加载耗时、页面渲染帧率是否提升。

六、工程化进阶:懒加载指令封装+主流插件实战

实际项目开发中,重复手写懒加载逻辑效率太低,更推荐封装复用指令/Hooks或直接使用成熟插件,适配Vue、React等框架工程化场景,下面附上可直接复用的封装代码和插件用法。

6.1 Vue3 图片懒加载自定义指令(全局封装)

基于 Intersection Observer 封装全局懒加载指令,一键复用,无需重复写监听逻辑,适配Vue3项目。

步骤1:创建指令文件(directives/lazyLoad.js)
js 复制代码
// 全局图片懒加载指令
const lazyLoad = {
  mounted(el, binding) {
    // 初始化占位图
    el.src = 'loading.svg';
    // 创建观察器
    const observer = new IntersectionObserver((entries) => {
      const [entry] = entries;
      if (entry.isIntersecting) {
        // 加载真实图片
        el.src = binding.value;
        // 加载失败兜底
        el.onerror = () => { el.src = 'error.svg'; };
        // 停止观察
        observer.unobserve(el);
      }
    }, { rootMargin: '10% 0px' });
    // 绑定观察对象
    observer.observe(el);
    // 存储观察器,用于卸载
    el._observer = observer;
  },
  // 指令卸载时销毁观察器
  unmounted(el) {
    el._observer?.unobserve(el);
  }
};

export default lazyLoad;
步骤2:全局注册指令(main.js)
js 复制代码
import { createApp } from 'vue';
import App from './App.vue';
import lazyLoad from './directives/lazyLoad';

const app = createApp(App);
// 注册全局指令 v-lazy
app.directive('lazy', lazyLoad);
app.mount('#app');
步骤3:页面使用
js 复制代码
<!-- 直接使用 v-lazy 指令,传入真实图片地址 -->
<img v-lazy="https:/picsum.photos800/450?1" alt="指令懒加载" /

6.2 React 图片懒加载 Hooks 封装

封装自定义Hook,实现React函数组件复用,适配React项目。

js 复制代码
import { useEffect, useRef } from 'react';

// 自定义懒加载Hook
function useLazyImg() {
  const imgRef = useRef(null);

  useEffect(() => {
    const observer = new IntersectionObserver((entries) => {
      const [entry] = entries;
      if (entry.isIntersecting) {
        const img = entry.target;
        img.src = img.dataset.src;
        img.onerror = () => { img.src = 'error.svg'; };
        observer.unobserve(img);
      }
    }, { rootMargin: '10% 0px' });

    if (imgRef.current) observer.observe(imgRef.current);

    return () => {
      if (imgRef.current) observer.unobserve(imgRef.current);
    };
  }, []);

  return imgRef;
}

// 组件使用
export default function LazyImage({ src, alt }) {
  const imgRef = useLazyImg();
  return (
    <img
      ref={src}
      src="loading.svg"
      alt={alt}
      style={{ width: '100%', aspectRatio: '16/9' }}
    />
  );
}

6.3 主流懒加载插件推荐(开箱即用)

不想手动封装,可直接使用社区成熟插件,配置简单、功能完善。

Vue2/Vue3:vue-lazyload

Vue生态最常用的图片懒加载插件,支持占位图、加载失败、节流等功能。

bash 复制代码
# 安装
npm install vue-lazyload --save
js 复制代码
// 注册(main.js)
import Vue from 'vue';
import VueLazyload from 'vue-lazyload';

Vue.use(VueLazyload, {
  preLoad: 1.3, // 提前加载比例
  error: 'error.svg', // 失败图
  loading: 'loading.svg', // 占位图
  attempt: 1 // 加载次数
});

// 页面使用
React:react-lazy-load-image-component

React专用懒加载组件,支持淡入动画、占位、响应式,适配SSR场景。

bash 复制代码
# 安装
npm install react-lazy-load-image-component --save
js 复制代码
import { LazyLoadImage } from 'react-lazy-load-image-component';
import 'react-lazy-load-image-component/src/effects/blur.css';

// 使用
function ImageList() {
  return (
    <LazyLoadImage
      src="https://picsum.photos/800/450"
      alt="插件懒加载"
      effect="blur" // 淡入模糊效果
      placeholderSrc="loading.svg" // 占位图
      width="100%"
    />
  );
}

七、方案选型总结

  • 原生开发/老项目 :选滚动监听 + getBoundingClientRect 方案;
  • 现代浏览器/追求性能 :选Intersection Observer API 方案(首选);
  • 极简开发/零代码 :选原生 loading="lazy" 属性;
  • Vue/React工程化 :优先用自定义指令/Hooks,快速复用;
  • 快速落地 :直接用 vue-lazyload/react-lazy-load-image-component 插件。
相关推荐
bluceli1 小时前
JavaScript WeakMap与WeakSet:内存优化的秘密武器
前端·javascript
陆枫Larry2 小时前
折叠屏“窗口化”下的全屏背景图错位:一次小程序适配的排障思路与最小改动修复
前端
陆枫Larry2 小时前
Art Direction(艺术导向适配)
前端
我叫黑大帅2 小时前
如何让两个Go程序远程调用?
后端·面试·go
Lee川2 小时前
从“手工砌砖”到“魔法蓝图”:响应式驱动界面的诞生与实战
前端·vue.js
与虾牵手2 小时前
Next.js 14 App Router 踩坑实录:5 个让我加班到凌晨的坑 🕳️
前端·javascript·面试
猩球中的木子2 小时前
怎么集成安装VitePlus(Vite+)并使用
前端·vite·前端工程化
李昊哲小课2 小时前
电商系统项目教程
开发语言·前端·javascript
smxgn2 小时前
spring-boot-starter和spring-boot-starter-web的关联
前端