从零打造 AI 全栈应用(七):图片懒加载的原理与工程化实践

7 阅读4分钟

本文是「从零打造 AI 全栈应用」系列第 篇。

在上一篇中,我们完成了 NestJS + Prisma 的企业级 API 架构搭建。从这一篇开始,我们会回到性能层,解决一个在真实项目中必须做、而且极容易被低估的问题

图片懒加载(Image Lazy Loading)

如果你做的是:

  • 信息流 / 文章列表
  • AI 内容生成平台
  • 图文混排、卡片流页面

那么图片懒加载不是“优化项”,而是上线前的基本要求


一、为什么图片一定要做懒加载?

先说结论:

列表页不做图片懒加载 = 性能事故。

1️⃣ 浏览器加载图片的真实成本

<img src="xxx.jpg" />

这行代码的背后意味着:

  • 一次 HTTP 请求
  • 占用浏览器并发连接数
  • 图片解码 + 内存占用

如果首页有 30 张图:

  • HTML 还没解析完
  • CSS / JS 还没执行
  • 图片请求已经打满并发

👉 首屏直接变慢

2️⃣ 首屏加载的核心原则

首屏优先,非首屏延后。

因此图片加载必须遵守:

  • 首屏图片:可以加载
  • viewport 之外的图片:绝对不要加载

这正是图片懒加载要解决的问题。


二、传统方案:onscroll + 节流(为什么不推荐)

早期懒加载的写法通常是:

  • 监听 scroll 事件
  • 计算元素位置
  • 判断是否进入视窗
  • 手动替换 src

问题在于:

  • scroll 触发频率极高
  • 需要手动节流 / 防抖
  • 计算逻辑复杂且易错

这是典型的“能跑,但不该在现代项目里用”的方案。


三、现代标准解法:IntersectionObserver

1️⃣ IntersectionObserver 是什么?

它是浏览器原生提供的一个 API,核心思想是:

观察某个元素,是否进入视口(viewport)。

关键词拆解:

  • Observer:观察者(设计模式)
  • Intersection:与视窗产生交集

浏览器帮你完成了:

  • 滚动监听
  • 位置计算
  • 性能优化

我们只需要关心:

元素“出现了”这一刻。


四、原生图片懒加载完整实现

1️⃣ HTML 结构设计

<img
  class="lazy"
  src="placeholder.png"
  data-src="real-image.jpg"
/>

设计要点:

  • src:占位图(体积小)
  • data-src:真实图片地址

2️⃣ JS:实例化 IntersectionObserver

const images = document.querySelectorAll('.lazy');

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      observer.unobserve(img);
    }
  });
});

images.forEach(img => observer.observe(img));

3️⃣ 这段代码做了什么?

  • 浏览器自动监听滚动
  • 元素进入 viewport
  • isIntersecting === true
  • 替换真实图片地址
  • 加载完成后取消观察

没有 scroll,没有节流,性能几乎零成本。


五、为什么一定要 unobserve

这是一个非常容易被忽略的点。

如果不取消观察:

  • Observer 会一直持有引用
  • 页面越滚越多,观察对象越多
  • 长列表会产生内存浪费

👉 进入一次,加载一次,然后立刻释放。

这是专业写法。


六、React 项目中的图片懒加载

在真实项目中,我们通常不会手写原生逻辑,而是:

  • 用成熟组件
  • 统一行为
  • 减少心智负担

1️⃣ react-lazy-load 的本质

pnpm i react-lazy-load

这个库本质上:

内部仍然是 IntersectionObserver。

你获得的是:

  • 组件化封装
  • 更好的可读性
  • 与 React 生命周期自然融合

七、实战示例

<LazyLoad className="w-full h-full">
  <img
    loading="lazy"
    src={post.thumbnail}
    className="w-full h-full object-cover"
  />
</LazyLoad>

为什么这里是“双保险”?

1️⃣ LazyLoad 组件

  • 控制是否渲染
  • IntersectionObserver 触发

2️⃣ loading="lazy"

  • 浏览器原生兜底
  • 即使 JS 失效,也不会一次性加载

工程级优化的特点就是:多层防线。


八、为什么列表页一定要做懒加载?

以文章列表为例:

  • 首屏只展示 3~5 条
  • 但接口一次返回 20 条
  • 如果全部图片同时加载

结果就是:

  • 首屏白屏时间变长
  • 滚动明显卡顿
  • 移动端直接掉帧

懒加载不是“优化体验”,而是“保证可用”。


九、面试官视角:你应该怎么回答?

如果我在面试中问你:

“你们项目是怎么做图片懒加载的?”

一个合格的回答应该包括:

  • 为什么不能一次性加载
  • IntersectionObserver 的原理
  • 占位图 + data-src 思路
  • React 中的组件化封装
  • loading="lazy" 的兜底作用

能讲到这一层,说明你不是“背八股”,而是真的做过性能优化。


总结

图片懒加载,是一个:

  • 代码不多
  • 价值极高
  • 非常容易被忽略

真正专业的前端,一定会主动去做的优化点