阅读视图

发现新文章,点击刷新页面。

拒绝“首屏爆炸”:用 React 哨兵模式与懒加载打造丝滑列表

拒绝“首屏爆炸”:用 React 哨兵模式与懒加载打造丝滑列表

想象一下,你开了一家名为“无限画廊”的餐厅(也就是你的 Web 应用)。

如果你的做法是:先把菜单上的一万道菜全部做好,堆在门口(首屏加载),然后让顾客自己找想吃的。

结果会怎样?门口堵死了,服务员累瘫了,顾客还没进门就被吓跑了。这就是典型的性能灾难

今天,我们就来聊聊如何用 React 哨兵模式(Infinite Scroll)图片懒加载(Lazy Load) 这两把利器,把你的“餐厅”改造成米其林级别的流畅体验。

️‍♂️ 第一章:守门员——IntersectionObserver 哨兵模式

传统的滚动加载是怎么做的?监听 windowscroll 事件,疯狂计算 (scrollTop + clientHeight) >= scrollHeight。这就像是你雇了个保安,每过一毫秒就问你一次:“到底了吗?到底了吗?到底了吗?” —— 太吵了,而且费脑子(主线程阻塞)。

现代浏览器的救星来了:IntersectionObserver

它的逻辑是:“嘿,浏览器大哥,帮我盯着那个叫‘哨兵’的 <div>。只要它一露脸,你就喊我一声。” 浏览器内部优化极佳,完全不用我们操心性能。

让我们看看你提供的这个“通用哨兵组件”是如何工作的:

// InfiniteScroll.js - 我们的核心守卫
const InfiniteScroll = ({ hasMore, onLoadMore, isLoading, children }) => {
  const sentinelRef = useRef(null); // 这是一个隐形的“间谍”节点

  useEffect(() => {
    // 1. 安全检查:没数据了或者正在加载中,就别折腾了
    if (!hasMore || isLoading) return;

    // 2. 雇佣观察员 (Observer)
    const observer = new IntersectionObserver((entries) => {
      // 3. 只要哨兵出现在视野里(哪怕只露出一像素)
      if (entries[0].isIntersecting) {
        onLoadMore(); // 吹哨子:该上菜了!
      }
    }, { threshold: 0 }); // threshold: 0 意味着“只要看见一点点就算”

    // 4. 告诉观察员盯着谁
    if (sentinelRef.current) {
      observer.observe(sentinelRef.current);
    }

    // 5.  cleanup:组件卸载或更新时,记得解雇观察员,防止内存泄漏
    return () => {
      if (sentinelRef.current) {
        observer.unobserve(sentinelRef.current);
      }
    };
  }, [onLoadMore, hasMore, isLoading]); // 依赖项要写对,不然哨兵会罢工

  return (
    <>
      {children} {/* 这里放你的列表内容 */}
      
      {/* 这是一个高度极小的隐形 div,它就是我们的“哨兵” */}
      <div ref={sentinelRef} className="h-4" />
      
      {isLoading && <div className="text-center py-4">加载中...</div>}
    </>
  );
};

为什么叫它“哨兵”? 因为它混在列表的最底部。当用户滚动页面,列表内容被顶上去,原本藏在底部的“哨兵”就会暴露在视口(Viewport)中。一旦暴露,IntersectionObserver 捕捉到信号,立即触发 onLoadMore,新数据进来,把哨兵继续往下顶。完美闭环!

️ 第二章:视觉欺诈——图片懒加载的艺术

解决了列表的分页,我们还得解决列表里的“胖子”——图片

如果你的列表有 100 项,每项一张图,那就是 100 个 HTTP 请求。用户打开页面的瞬间,带宽直接被占满,白屏时间长得让人想关掉网页。

懒加载的核心思想: “不见兔子不撒鹰”。只有当图片快要进入屏幕时,才给它真正的 src 地址。

虽然原生的 <img loading="lazy" /> 已经很强了,但在 React 生态中,我们通常会结合 react-lazy-load 这样的库,利用它们封装好的 IntersectionObserver 能力,实现更精细的控制(比如提前加载、占位符防抖动)。

实战代码长这样:

import LazyLoad from 'react-lazy-load';

const PostItem = ({ post }) => {
  return (
    <div className="card">
      <h3>{post.title}</h3>
      {/* 
         方案 A: 使用第三方库(推荐用于复杂场景,如瀑布流)
         height 属性很重要,用来撑开高度,防止图片加载前布局塌陷
      */}
      <LazyLoad height={200} offset={100}>
        <img src={post.thumbnail} alt={post.title} className="w-full h-auto" />
      </LazyLoad>

      {/* 
         方案 B: 原生偷懒法 (简单粗暴,兼容性也不错)
         <img src={post.thumbnail} loading="lazy" /> 
      */}
    </div>
  );
};

双重保障: 你可以同时使用 loading="lazy" 属性和 LazyLoad 组件。前者是给浏览器的指令,后者是 React 层面的兜底,两者结合,稳如老狗。

第三章:终极合体——打造无限流

现在,我们将这两个概念结合起来。InfiniteScroll 负责宏观的节奏(什么时候加载下一页数据),而内部的 LazyLoad 负责微观的体验(图片按需显示)。

使用场景模拟:

  1. Store/State: 维护一个 posts 数组,page 页码,hasMore 是否还有下一页。
  2. UI 层:
    • 外层包裹 <InfiniteScroll ...>
    • 中间是 .map() 渲染出来的文章列表。
    • 每篇文章里的图片都被 <LazyLoad> 包裹。
  3. 交互流程:
    • 用户刷刷刷,看到了第 10 篇文章。
    • 第 10 篇的图片因为快进视口了,自动加载高清大图(懒加载生效)。
    • 用户继续刷到底部,踩到了“哨兵”。
    • onLoadMore 触发,API 请求第 2 页数据。
    • 新数据拼接到 posts 数组,React 重新渲染,列表变长。

避坑指南(老司机的经验)

  • 锁住并发:一定要用 isLoading 状态锁!千万别让用户在数据请求回来的那几百毫秒内,连续触发两次哨兵,导致发了两个一样的 API 请求。
  • 高度塌陷:做图片懒加载时,如果图片没加载出来,容器高度为 0,页面会发生剧烈的跳动(Layout Shift)。解决办法:给图片容器设置固定的宽高比(aspect-ratio)或者预设高度。
  • 路由切换:记得在 useEffect 的清理函数中 observer.disconnect()unobserve。否则当你跳转到详情页再回来时,可能会发现旧的观察器还在后台幽灵般地运行。

总结

前端开发的艺术,往往就在于**“拖延”**。

能晚点加载的代码(Code Splitting),就晚点加载;能晚点请求的数据(Infinite Scroll),就晚点请求;能晚点下载的图片(Lazy Load),就晚点下载。

用好 IntersectionObserver 和 React 的组合模式,让你的应用像丝绸一样顺滑。

🚀 别再让浏览器“负重跑”了!手把手教你用 IntersectionObserver 实现图片懒加载

🚀 别再让浏览器“负重跑”了!手把手教你用 IntersectionObserver 实现图片懒加载


💡 前言:为什么你的网页打开像“PPT”?

想象一下,你正在约会,对面坐着一位美女(用户)。她满怀期待地问你:“嘿,你的网站快吗?”

然后你深吸一口气,开始从背包里往外掏东西:先掏出一张巨大的海报(首屏大图),然后是几百张高清无码的猫咪照片(长列表图片),最后还有几个G的视频……

美女(用户)看着你这一堆乱七八糟的东西,还没等你掏完,她就说了一句:“算了,我们不合适。” 然后转身走了(用户流失,跳出率 100%)。

这就是**不做懒加载(Lazy Load)**的下场。

在传统的网页开发中,我们习惯把所有 <img> 标签的 src 一股脑全写上。浏览器一看:“好家伙,老板发话了,不管三七二十一,全部下载!” 于是,即使用户根本还没滑到页面底部,浏览器就已经累得气喘吁吁,流量在燃烧,内存再尖叫。

今天,我们就来聊聊如何用现代浏览器的“外挂”—— IntersectionObserver,来拯救你那卡顿的网页。


🧐 什么是懒加载?

简单来说,就是**“按需加载”**。

  • 用户看哪里,就加载哪里。
  • 用户还没滑到的图片?先给个“占位符”(比如一张很轻的loading图或者灰色背景)糊弄一下。
  • 等用户快滑到了,再瞬间把真图换上。

这就好比你去自助餐厅,不会一次性把所有菜都堆在桌子上,而是吃一盘,拿一盘


🛠️ 实战:手写一个“丝滑”的懒加载

以前,我们要实现懒加载,得监听 window.onscroll 事件,然后疯狂计算 getBoundingClientRect,还要防抖节流……写起来简直头秃,性能还差。

现在,IntersectionObserver 来了!它是浏览器原生的 API,专门用来监听元素是否进入了视口(可视区域)。它运行在单独的线程中,性能极佳,简直是前端界的“德芙”,纵享丝滑。

来看代码(结合你提供的示例):

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>图片的懒加载</title>
  <style>
    * { margin: 0; padding: 0; }
    /* 搞几个大盒子把图片挤下去,模拟长页面 */
    .box { height: 200vh; background-color: #eee; } 
    img { width: 100%; height: 400px; object-fit: cover; display: block; }
  </style>
</head>
<body>

  <!-- 占位用的大盒子 -->
  <div class="box"></div>

  <!-- 
    1. src: 先放一张极小的默认图(或者loading图),防止页面布局塌陷
    2. data-src: 把真正的高清大图藏在这里,浏览器不会主动加载它
  -->
  <img class="lazy" 
       src="https://img10.360buyimg.com/wq/jfs/t24601/190/890984006/4559/731564fc/5b7f9b7bN3ccd29ab.png" 
       data-src="https://img.36krcdn.com/hsossms/20260119/v2_53cad3f2226f48e2afc1942de3ab74e4@5888275@ai_oswg1141728oswg1053oswg495_img_png~tplv-1marlgjv7f-ai-v3:960:400:960:400:q70.jpg?x-oss-process=image/format,webp" 
       alt="AI生图1">

  <div class="box"></div>

  <img class="lazy" 
       src="https://img10.360buyimg.com/wq/jfs/t24601/190/890984006/4559/731564fc/5b7f9b7bN3ccd29ab.png" 
       data-src="https://img.36krcdn.com/hsossms/20260117/v2_1e74add07bb94971845c777e0ce87a49@000000@ai_oswg421938oswg1536oswg722_img_000~tplv-1marlgjv7f-ai-v3:960:400:960:400:q70.jpg?x-oss-process=image/format,webp" 
       alt="AI生图2">

  <script>
    // 1. 选中所有需要懒加载的图片
    const images = document.querySelectorAll('.lazy');

    // 2. 创建观察者实例
    // 浏览器提供的观察者模式,自动观察,性能杠杠的
    const observer = new IntersectionObserver((entries) => {
      // entries 是一个数组,包含所有被观察元素的状态
      entries.forEach(entry => {
        // isIntersecting 为 true 表示元素进入了视口
        if (entry.isIntersecting) {
          const img = entry.target; // 获取目标元素 (DOM节点)
          const original_img = img.dataset.src; // 获取 data-src 里的真图地址
          
          console.log('抓到你了!正在加载:', original_img);
          
          // 3. 偷梁换柱:把真图地址赋值给 src
          img.src = original_img;
          
          // 4. 加载完后,告诉观察者:“这人看过了,不用盯着了”
          // 停止观察该元素,节省性能
          observer.unobserve(img);
        }
      })
    })

    // 5. 开始观察!
    images.forEach(img => observer.observe(img));
  </script>
</body>
</html>

🔍 代码核心知识点解析

别光顾着复制粘贴,我们来拆解一下这里的“骚操作”:

  1. data-src 的妙用 🎭 HTML 标准规定,<img> 标签只要有 src 属性,浏览器就会立刻发起请求。为了阻止这个行为,我们把真实的图片链接藏在自定义属性 data-src 里。浏览器:“哦,这只是一个叫 data-src 的字符串,我不认识,不加载。”

  2. IntersectionObserver API 👁️ 这是主角。

    • 传统做法:你得不停地问浏览器“图片在哪?滚动条在哪?算一下距离……”(强制重排/重绘,累死CPU)。
    • IntersectionObserver:你告诉浏览器“帮我盯着这张图,它出来了叫我一声”。浏览器在底层异步处理这些计算,完全不影响页面渲染帧率。
  3. entry.isIntersecting ✅ 这是一个布尔值。true 代表元素和视口有交集(出现了),false 代表没交集(消失了)。我们在 true 的时候才去加载图片。

  4. observer.unobserve(img) 🛑 这点很重要!图片加载完了,任务就结束了。如果不取消观察,浏览器还会一直盯着这张已经加载好的图片,纯属浪费资源。用完即弃,才是好代码。


🤔 为什么要这么做?(必要性)

你可能会问:“我就几张图,直接加载不行吗?”

  • 首屏速度(First Contentful Paint):用户打开网页,只关心第一屏。如果后台在偷偷下载第10屏的图片,首屏加载就会变慢。懒加载能让首屏飞起来。
  • 节省带宽:很多用户可能根本不会滑到底部。你加载了100张图,他只看了10张。懒加载帮他省了90%的流量,他会感谢你的(特别是用5G流量看视频的时候)。
  • 减少内存占用:浏览器同时处理几百个网络请求和渲染几百张大图,内存容易爆炸,导致页面卡顿甚至崩溃。

📌 总结

懒加载是现代 Web 开发的标配

以前我们用 jQuery 写插件,后来用原生 JS 算坐标,现在我们有了 IntersectionObserver。技术总是在进步,我们要学会用更优雅、性能更好的方式去解决问题。

下次再有人问你网站为什么这么快,你可以淡淡地喝一口咖啡,说:“哦,我只是让我的图片学会了‘按需出现’而已。”

Happy Coding! ☕️


(本文代码已在 Chrome/Firefox/Edge 等现代浏览器测试通过。IE 用户?请出门右转不送,或者加个 Polyfill 吧。)

❌