React 性能优化:图片懒加载

作者:NEXT06日期:2026/2/17

引言

在现代 Web 应用开发中,首屏加载速度(FCP)和最大内容绘制(LCP)是衡量用户体验的核心指标。随着富媒体内容的普及,图片资源往往占据了页面带宽的大部分。如果一次性加载页面上的所有图片,不仅会阻塞关键渲染路径,导致页面长时间处于“白屏”或不可交互状态,还会浪费用户的流量带宽。

图片懒加载(Lazy Loading)作为一种经典的性能优化策略,其核心思想是“按需加载”:即只有当图片出现在浏览器可视区域(Viewport)或即将进入可视区域时,才触发网络请求进行加载。这一策略能显著减少首屏 HTTP 请求数量,降低服务器压力,并提升页面的交互响应速度。

本文将基于 React 生态,从底层原理出发,深入探讨图片懒加载的多种实现方案,并重点分析如何解决布局偏移(CLS)等用户体验问题。

核心原理剖析

图片懒加载的本质是一个“可见性检测”问题。我们需要实时判断目标图片元素是否与浏览器的可视区域发生了交叉。在技术实现上,主要依赖以下两种依据:

  1. 几何计算:通过监听滚动事件,实时计算元素的位置坐标。核心公式通常涉及 scrollTop(滚动距离)、clientHeight(视口高度)与元素 offsetTop(偏移高度)的比较,或者使用 getBoundingClientRect() 获取元素相对于视口的位置。
  2. API 监测:利用浏览器提供的 IntersectionObserver API。这是一个异步观察目标元素与祖先元素或顶级文档视窗交叉状态的方法,它将复杂的几何计算交由浏览器底层处理,性能表现更优。

方案一:原生 HTML 属性(最简方案)

HTML5 标准为 标签引入了 loading 属性,这是实现懒加载最简单、成本最低的方式。

Jsx

1const NativeLazyLoad = ({ src, alt }) => {
2  return (
3    <img 
4      src={src} 
5      alt={alt} 
6      loading="lazy" 
7      width="300" 
8      height="200"
9    />
10  );
11};
12

分析:

  • 优点:零 JavaScript 代码,完全依赖浏览器原生行为,不会阻塞主线程。
  • 缺点
    • 兼容性:虽然现代浏览器支持度已较高,但在部分旧版 Safari 或 IE 中无法工作。
    • 不可控:开发者无法精确控制加载的阈值(Threshold),也无法在加载失败或加载中注入自定义逻辑(如骨架屏切换)。
    • 功能单一:仅适用于 img 和 iframe 标签,无法用于 background-image。

方案二:传统 Scroll 事件监听(兼容方案)

在 IntersectionObserver 普及之前,监听 scroll 事件是主流做法。其原理是在 React 组件挂载后绑定滚动监听器,在回调中计算图片位置。

React 实现示例:

Jsx

1import React, { useState, useEffect, useRef } from 'react';
2import placeholder from './assets/placeholder.png';
3
4// 简单的节流函数,生产环境建议使用 lodash.throttle
5const throttle = (func, limit) => {
6  let inThrottle;
7  return function() {
8    const args = arguments;
9    const context = this;
10    if (!inThrottle) {
11      func.apply(context, args);
12      inThrottle = true;
13      setTimeout(() => inThrottle = false, limit);
14    }
15  }
16};
17
18const ScrollLazyImage = ({ src, alt }) => {
19  const [imageSrc, setImageSrc] = useState(placeholder);
20  const imgRef = useRef(null);
21  const [isLoaded, setIsLoaded] = useState(false);
22
23  useEffect(() => {
24    const checkVisibility = () => {
25      if (isLoaded || !imgRef.current) return;
26
27      const rect = imgRef.current.getBoundingClientRect();
28      const windowHeight = window.innerHeight || document.documentElement.clientHeight;
29
30      // 设置 100px 的缓冲区,提前加载
31      if (rect.top <= windowHeight + 100) {
32        setImageSrc(src);
33        setIsLoaded(true);
34      }
35    };
36
37    // 必须使用节流,否则滚动时会频繁触发重排和重绘,导致性能灾难
38    const throttledCheck = throttle(checkVisibility, 200);
39
40    window.addEventListener('scroll', throttledCheck);
41    window.addEventListener('resize', throttledCheck);
42    
43    // 初始化检查,防止首屏图片不加载
44    checkVisibility();
45
46    return () => {
47      window.removeEventListener('scroll', throttledCheck);
48      window.removeEventListener('resize', throttledCheck);
49    };
50  }, [src, isLoaded]);
51
52  return <img ref={imgRef} src={imageSrc} alt={alt} />;
53};
54

关键点分析:

  1. 节流(Throttle) :scroll 事件触发频率极高,若不加节流,每次滚动都会执行 DOM 查询和几何计算,占用大量主线程资源,导致页面掉帧。
  2. 回流(Reflow)风险:频繁调用 getBoundingClientRect() 或 offsetTop 会强制浏览器进行同步布局计算(Synchronous Layout),这是性能杀手。

方案三:IntersectionObserver API(现代标准方案)

这是目前最推荐的方案。IntersectionObserver 运行在独立线程中,不会阻塞主线程,且浏览器对其进行了内部优化。

React 实现示例:

我们可以将其封装为一个通用的组件 LazyImage。

Jsx

1import React, { useState, useEffect, useRef } from 'react';
2import './LazyImage.css'; // 假设包含样式
3
4const LazyImage = ({ src, alt, placeholderSrc, width, height }) => {
5  const [imageSrc, setImageSrc] = useState(placeholderSrc || '');
6  const [isVisible, setIsVisible] = useState(false);
7  const imgRef = useRef(null);
8
9  useEffect(() => {
10    let observer;
11    
12    if (imgRef.current) {
13      observer = new IntersectionObserver((entries) => {
14        const entry = entries[0];
15        // 当元素进入视口
16        if (entry.isIntersecting) {
17          setImageSrc(src);
18          setIsVisible(true);
19          // 关键:图片加载触发后,立即停止观察,释放资源
20          observer.unobserve(imgRef.current);
21          observer.disconnect();
22        }
23      }, {
24        rootMargin: '100px', // 提前 100px 加载
25        threshold: 0.01
26      });
27
28      observer.observe(imgRef.current);
29    }
30
31    // 组件卸载时的清理逻辑
32    return () => {
33      if (observer) {
34        observer.disconnect();
35      }
36    };
37  }, [src]);
38
39  return (
40    <img
41      ref={imgRef}
42      src={imageSrc}
43      alt={alt}
44      width={width}
45      height={height}
46      className={`lazy-image ${isVisible ? 'loaded' : ''}`}
47    />
48  );
49};
50
51export default LazyImage;
52

优势分析:

  • 高性能:异步检测,无回流风险。
  • 资源管理:通过 unobserve 和 disconnect 及时释放观察者,避免内存泄漏。
  • 灵活性:rootMargin 允许我们轻松设置预加载缓冲区。

进阶:用户体验与 CLS 优化

仅仅实现“懒加载”是不够的。在工程实践中,如果处理不当,懒加载会导致严重的累积布局偏移(CLS, Cumulative Layout Shift) 。即图片加载前高度为 0,加载后撑开高度,导致页面内容跳动。这不仅体验极差,也是 Google Core Web Vitals 的扣分项。

1. 预留空间(Aspect Ratio)

必须在图片加载前确立其占据的空间。现代 CSS 提供了 aspect-ratio 属性,配合宽度即可自动计算高度。

CSS

1/* LazyImage.css */
2.img-wrapper {
3  width: 100%;
4  /* 假设图片比例为 16:9,或者由后端返回具体宽高计算 */
5  aspect-ratio: 16 / 9; 
6  background-color: #f0f0f0; /* 骨架屏背景色 */
7  overflow: hidden;
8  position: relative;
9}
10
11.lazy-image {
12  width: 100%;
13  height: 100%;
14  object-fit: cover;
15  opacity: 0;
16  transition: opacity 0.3s ease-in-out;
17}
18
19.lazy-image.loaded {
20  opacity: 1;
21}
22

2. 结合数据的完整 React 组件

结合后端返回的元数据(如宽高、主色调),我们可以构建一个体验极佳的懒加载组件。

Jsx

1const AdvancedLazyImage = ({ data }) => {
2  // data 结构示例: { url: '...', width: 800, height: 600, basicColor: '#a44a00' }
3  const imgRef = useRef(null);
4  const [isLoaded, setIsLoaded] = useState(false);
5
6  useEffect(() => {
7    const observer = new IntersectionObserver(([entry]) => {
8      if (entry.isIntersecting) {
9        const img = entry.target;
10        // 使用 dataset 获取真实地址,或者直接操作 state
11        img.src = img.dataset.src;
12        
13        img.onload = () => setIsLoaded(true);
14        observer.unobserve(img);
15      }
16    });
17
18    if (imgRef.current) observer.observe(imgRef.current);
19
20    return () => observer.disconnect();
21  }, []);
22
23  return (
24    <div 
25      className="img-container"
26      style={{
27        // 核心:使用 aspect-ratio 防止 CLS
28        aspectRatio: [`${data.width} / ${data.height}`](https://xplanc.org/primers/document/zh/03.HTML/EX.HTML%20%E5%85%83%E7%B4%A0/EX.data.md),
29        // 核心:使用图片主色调作为占位背景,提供渐进式体验
30        backgroundColor: data.basicColor 
31      }}
32    >
33      <img
34        ref={imgRef}
35        data-src={data.url} // 暂存真实地址
36        alt="Lazy load content"
37        style={{
38          opacity: isLoaded ? 1 : 0,
39          transition: 'opacity 0.5s ease'
40        }}
41      />
42    </div>
43  );
44};
45

方案对比与场景选择

方案实现难度性能兼容性适用场景
原生属性 (loading="lazy")中 (现代浏览器)简单的 CMS 内容页、对交互要求不高的场景。
Scroll 监听低 (需节流)高 (全兼容)必须兼容 IE 等老旧浏览器,或有特殊的滚动容器逻辑。
IntersectionObserver极高高 (需 Polyfill)现代 Web 应用、无限滚动列表、对性能和体验有高要求的场景。

结语

图片懒加载是前端性能优化的基石之一。从早期的 Scroll 事件监听,到如今标准化的 IntersectionObserver API,再到原生 HTML 属性的支持,技术在不断演进。

在 React 项目中落地懒加载时,我们不能仅满足于“功能实现”。作为架构师,更应关注性能损耗(如避免主线程阻塞)、资源管理(及时销毁 Observer)以及用户体验(防止 CLS、优雅的过渡动画)。通过合理利用 aspect-ratio 和占位策略,我们可以让懒加载不仅“快”,而且“稳”且“美”。


React 性能优化:图片懒加载》 是转载文章,点击查看原文


相关推荐


【Linux】进程信号(上半)
Lsir10110_2026/2/8

当我们想要强行终止掉前台进程的时候,只需要按下Ctrl+c即可,但是Ctrl+c是如何精准杀掉前台进程的? 一、信号概念 1.如何理解信号 假设点了一份外卖,外卖员到了楼下会给你发信息或者打电话,那么这通电话或者这个信息就是信号,也就是用来提醒你需要去做某事的一种通知手段。那么对照Linux系统,信号就是内核向进程发送的通知,提醒进程需要去完成某个任务。 2.信号的特点 对照外卖例子,当点完外卖,我们肯定不会一直在门口等着外卖员,而是先忙手中的事情,比如打游戏,那么这个过程就叫做异步


JSyncQueue——一个开箱即用的鸿蒙异步任务同步队列
江澎涌2026/1/31

零、JSyncQueue JSyncQueue 是一个开箱即用的鸿蒙异步任务同步队列。 项目地址:github.com/zincPower/J… 一、JSyncQueue 有什么作用 在鸿蒙应用开发中,有时需要让多个异步任务按顺序执行,例如状态的转换处理,如果不加控制,会因为执行顺序混乱而产生一些莫名其妙的问题。 所以 JSyncQueue 提供了一个简洁的解决方案: 保证顺序执行:所有任务严格按照入队顺序执行,即使任务内部有异步操作也能保证顺序 两种执行模式:支持 "立即执行" 和 "延时执


Python 线程局部存储:threading.local() 完全指南
哈里谢顿2026/1/21

一句话总结: threading.local() 是 Python 标准库提供的「线程局部存储(Thread Local Storage, TLS)」方案,让同一段代码在不同线程里拥有各自独立的变量空间,从而避免加锁,也避免了层层传参的狼狈。 1. 为什么需要线程局部存储? 在多线程环境下,如果多个线程共享同一个全局变量,就必须: 加锁 → 代码变复杂、性能下降; 或者层层传参 → 代码臃肿、可维护性差。 有些场景只想让线程各自持有一份副本,互不干扰: Web 服务:每个请求线程绑定自


绘制K线第二章:背景网格绘制
佛系打工仔2026/1/13

绘制K线第二章:背景网格绘制 在第一章的基础上,我们简单修饰一下,补充一个背景九宫格的绘制功能。这个功能可以让K线图更加清晰易读,帮助用户快速定位价格和时间。 二、网格配置 确定网格的行数和列数 在绘制网格之前,我们需要确定: 几行:将高度分成几等份(对应价格轴) 几列:将宽度分成几等份(对应时间轴) 例如:4列5行,表示宽度分成4等份,高度分成5等份。 在Config中配置 为了灵活配置网格,我们在 KLineConfig 中添加了两个字段: data class KLineConfig(


Linux系统安全及应用(账号权限管理、登录控制、弱口令、端口扫描)
晚风吹人醒.2026/1/5

目录 1. 账号管理与权限控制         1.1 基本安全措施:                 1.1.1 账号管理和文件权限                 1.1.2 密码安全控制                 1.1.3历史命令和自动注销         1.2 用户切换与提权: 2. 系统引导与登录控制         2.1 开关机安全控制:                 2.1.1 GRUB                 2.1.2 限制更改GRUB


算法竞赛中的数据结构:图
喜欢吃燃面2025/12/27

目录 一.图的基本概念1.图的定义2.图、树、线性表的联系与区别2.1 核心联系2.2 核心区别 二.图的分类1.按边的方向分类2.按边的权重分类3 .按顶点和边的数量分类4 .按连通性分类(针对无向图)5 .按强连通性分类(针对有向图)6 .其他特殊类型7.顶点的度(补充)8.路径及相关长度概念(补充)8.1 路径8.2 路径长度(无权图)8.3 带权路径长度(带权图)8.4 核心区别对比 三.邻接矩阵1.邻接矩阵【注意】 四.邻接表五.链式前向星


ZooKeeper+Kafka
吉良吉影1232025/12/18

目录 一、Zookeeper 1.1 Zookeeper 概述 1.2 Zookeeper 工作机制 1.3 ZooKeeper 特点 1.4 Zookeeper 数据结构 1.5 ZooKeeper 应用场景 1.6 Zookeeper 选举机制 1.6.1 第一次启动选举机制 1.6.2 非第一次启动选举机制 Leader 的作用 1. 处理所有写请求(核心职责) 2. 主导 Leader 选举 3. 管理集群数据同步 4. 维护集群状态 Follower


编程界 语言神 : 赶紧起来学 Rust 了!
Pomelo_刘金2025/12/10

大家对 Rust 的印象 没接触过的: 编程界语言神 整天重构这重构那 还要 要干掉 c++ ?! 稍微了解过的: 学习曲线: 但实际上是: 第一个高峰是 借用检查器,第二个是异步,第三个是unsafe,第四个是宏怎么玩? 开始接触之后 编译器不让我写代码,怎么写都报错 写 rust 代码像是在跟 rust 编译器谈对象 , 我只是传个参数,你跟我讲所有权、借用、生命周期?” 写的代码上线之后,还不错哦 “别的语言项目上线流程” 内容: 编译 ✔ 测试(偶尔挂一两条)✔ 上线后:半


单片机手搓掌上游戏机(十六)—pico运行fc模拟器之程序修改烧录
Bona Sun2025/11/30

我们来山寨picosystem,毕竟79刀,有些地方还是要简化修改的。 到: https://github.com/fhoedemakers/PicoSystem_InfoNes 下载zip或者git clone都可以。 解压缩,用vscode 打开文件夹   修改的地方:  首先是那个VSYNC,也就是8引脚的一个输入信号,我能买到的st7789上都没有这个引脚,看了一下代码 就是等待它的下降沿,也就知道该刷下一屏了。  其实没多大作用,我孤陋寡闻,还没见过屏幕撕裂,


Gemini 3.1 Pro 正式发布:一次低调更新,还是谷歌的关键反击?
IvanCodes2026/2/25

今天凌晨,谷歌发布了新一代模型——Gemini 3.1 Pro 没有大型发布会,没有提前预热,甚至连宣传节奏都显得克制。 很多人会把它看作 Gemini 3 的小版本升级,但从目前披露的测试数据和演示能力来看,这更像是一次结构性强化,而不是简单的参数迭代。 如果说 Gemini 3 是谷歌重新回到核心竞争区间的标志,那么 Gemini 3.1 Pro,则明显带着更强的实战优化意味。 它在几个关键方向上给出了非常明确的信号:谷歌不只是追赶者。 性能升级:从可用到强势竞争 这次升

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 XYZ博客