AI 生成的网页为什么越滚越卡?从 IntersectionObserver 到图片懒加载优化
前言
现在使用 AI 编程助手开发图片展示网站非常方便。
例如,只需要提出需求:
“帮我开发一个图片展示页面,支持卡片布局、滚动浏览和手机适配。”
AI 很快就能生成一个看起来不错的网站。
但是,当图片数量从十几张增加到几百张时,问题就可能出现:
- 页面首次打开速度明显变慢。
- 浏览器同时发出大量图片请求。
- 用户还没看到的图片也在提前下载。
- 页面滚动时偶尔出现卡顿。
- 手机网络下流量消耗较大。
这些问题不一定是 CSS 写得不好,也可能是图片加载策略不合理。
本文从一个常见的图片列表场景出发,介绍如何使用浏览器原生懒加载能力和 IntersectionObserver,优化图片较多的网页。
一、为什么图片多了,网页就容易变慢?
假设一个网页展示 100 张图片,每张图片平均大小为 500 KB。
那么,仅图片资源的总大小就可能达到约 50 MB。
如果页面初始加载时便让所有图片开始请求,浏览器和网络连接都可能承受较大压力。
例如,下面这种写法:
<img src="images/photo-1.jpg" alt="风景图片 1">
<img src="images/photo-2.jpg" alt="风景图片 2">
<img src="images/photo-3.jpg" alt="风景图片 3">
对于数量较少的图片,这样写完全没问题。
但如果是一个很长的图片列表,用户最初可能只看到前几张,其他图片暂时并不需要。
因此,一个优化思路是:
优先加载用户当前需要看到的内容,其他图片等接近可视区域时再加载。
这就是图片懒加载的基本原理。
二、最简单的方法:loading="lazy"
现代浏览器原生支持图片懒加载。
只需要给图片增加一个属性:
<img
src="images/photo-1.jpg"
alt="山间风景"
loading="lazy"
width="600"
height="400"
>
其中:
loading="lazy"
表示允许浏览器延迟加载暂时不需要的图片。
相比自己编写大量滚动监听代码,这种方式更加简单,也更容易维护。
不过,需要注意三个问题。
第一,懒加载不意味着图片必须进入屏幕以后才开始下载。
浏览器可能根据网络情况、图片位置等因素提前请求接近可视区域的资源。
第二,首屏关键图片不应该全部设置为 lazy。
例如网站顶部的大幅主视觉图片,如果延迟加载,反而可能影响用户看到主要内容的速度。
第三,应该尽量明确图片尺寸。
例如设置:
width="600"
height="400"
这样浏览器在图片实际下载完成前,就可以根据比例预留显示空间,减少布局移动。
三、什么时候需要 IntersectionObserver?
原生 loading="lazy" 已经能解决许多普通图片列表的需求。
但有些场景需要更灵活的控制。
例如:
- 希望图片距离屏幕还有一段距离时就开始加载。
- 需要在卡片进入可视区域时执行动画。
- 需要统计某个元素是否进入视口。
- 希望加载前展示自定义占位状态。
这时可以使用浏览器提供的 IntersectionObserver。
它能够观察某个元素与指定根区域的交叉情况。
与不断通过滚动事件检查每张图片的位置相比,IntersectionObserver 通常更适合实现这类可见性观察逻辑。
例如:
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
console.log("元素进入观察区域");
}
}
});
这里的:
entry.isIntersecting
用于判断观察目标是否与观察区域发生交叉。
不过,它不是精确的像素级可见性跟踪工具,也不保证回调在某个绝对时刻立即执行。
四、实战:使用 IntersectionObserver 实现图片懒加载
下面制作一个简单的图片展示页面。
功能包括:
- 展示图片卡片。
- 对屏幕外的图片延迟加载。
- 图片接近屏幕时提前开始下载。
- 加载成功后移除占位状态。
- 加载失败时显示提示。
项目结构:
lazy-image-demo/
├── index.html
├── style.css
├── script.js
└── images/
├── photo-1.jpg
├── photo-2.jpg
└── photo-3.jpg
图片文件需要自行准备,建议使用经过压缩的图片,并确保拥有使用权限。
1. 编写 HTML
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1"
>
<title>图片懒加载演示</title>
<link rel="stylesheet" href="style.css">
<script src="script.js" defer></script>
</head>
<body>
<main>
<h1>图片懒加载演示</h1>
<p>向下滚动,观察图片的加载过程。</p>
<section class="gallery">
<figure class="photo-card">
<img
class="lazy-image"
data-src="images/photo-1.jpg"
alt="风景图片一"
width="600"
height="400"
>
<figcaption>风景图片一</figcaption>
</figure>
<figure class="photo-card">
<img
class="lazy-image"
data-src="images/photo-2.jpg"
alt="风景图片二"
width="600"
height="400"
>
<figcaption>风景图片二</figcaption>
</figure>
<figure class="photo-card">
<img
class="lazy-image"
data-src="images/photo-3.jpg"
alt="风景图片三"
width="600"
height="400"
>
<figcaption>风景图片三</figcaption>
</figure>
</section>
</main>
</body>
</html>
这里有一个关键设计:
我们没有立即给图片设置 src,而是把真实图片地址保存在:
data-src="images/photo-1.jpg"
这样,在 JavaScript 为图片赋予 src 之前,浏览器不会因为这个属性直接开始下载对应图片。
2. 编写 CSS
创建 style.css:
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 32px 16px;
font-family: system-ui, sans-serif;
background: #f8fafc;
color: #0f172a;
}
main {
max-width: 700px;
margin: auto;
}
.gallery {
display: grid;
gap: 28px;
}
.photo-card {
margin: 0;
overflow: hidden;
border-radius: 14px;
background: white;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
}
.lazy-image {
display: block;
width: 100%;
height: auto;
aspect-ratio: 3 / 2;
object-fit: cover;
background: #e2e8f0;
}
.photo-card figcaption {
padding: 14px;
}
.lazy-image.is-error {
background: #fee2e2;
}
这里使用 aspect-ratio 为图片预留空间。
可以减少图片加载前后卡片尺寸变化的问题。
需要注意,图片必须具有合适的宽高比例,否则可能出现裁剪或留白;是否裁剪取决于实际布局设置。
3. 编写 JavaScript
创建 script.js:
const images = document.querySelectorAll(".lazy-image");
function loadImage(img) {
if (img.dataset.loading === "true") {
return;
}
const src = img.dataset.src;
if (!src) {
return;
}
img.dataset.loading = "true";
img.addEventListener(
"load",
() => {
img.classList.add("is-loaded");
img.removeAttribute("data-src");
},
{ once: true }
);
img.addEventListener(
"error",
() => {
img.classList.add("is-error");
img.alt = `图片加载失败:${img.alt}`;
img.removeAttribute("data-src");
},
{ once: true }
);
img.src = src;
}
if ("IntersectionObserver" in window) {
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
loadImage(entry.target);
observer.unobserve(entry.target);
}
}
},
{
root: null,
rootMargin: "200px 0px",
threshold: 0
}
);
images.forEach((img) => observer.observe(img));
} else {
images.forEach(loadImage);
}
这个示例主要使用了三个关键技术。
首先,使用 IntersectionObserver 监听图片位置。
当图片与观察区域相交时,才调用加载函数。
其次,使用 rootMargin 提前加载。
rootMargin: "200px 0px"
表示将观察区域在垂直方向上向外扩展 200 像素。
因此,图片还没有真正进入可视区域时,也可能触发加载。
这样可以降低用户滚动到图片位置后才开始等待下载的概率。
最后,使用 unobserve() 停止不必要的观察。
图片开始加载后,不需要继续反复观察它的位置。
如果浏览器不支持 IntersectionObserver,代码会直接加载所有图片,保证基本功能可用。
需要说明:这个实现侧重懒加载机制,没有提供重新尝试加载失败图片的功能。如果面向正式产品,还可以增加重试按钮及错误占位图。
五、为什么不建议直接使用 scroll 事件实现?
有些 AI 生成的代码会使用:
window.addEventListener("scroll", () => {
// 检查所有图片的位置
});
然后在每次滚动时,遍历所有图片并调用:
getBoundingClientRect()
来判断图片是否出现在屏幕内。
这种方式虽然能够工作,但如果滚动事件处理过于频繁,或者在每次滚动时读取大量布局信息,就可能增加主线程工作量。
特别是在图片很多、DOM 结构复杂的页面中,需要谨慎处理。
如果确实需要使用滚动事件,通常应当考虑节流,并减少不必要的布局读取。
但对于单纯的图片可见性判断,IntersectionObserver 往往是更简洁的选择。
六、如何测试图片是否真的实现懒加载?
只看网页能否显示图片,并不能说明懒加载已经成功。
可以通过浏览器开发者工具验证。
第一步:打开 Network 面板。
在 Chrome 或其他 Chromium 浏览器中,按 F12,切换到 Network。
第二步:筛选图片请求。
选择 Img 类型,观察图片资源的请求情况。
第三步:刷新页面。
检查是否只有首屏或接近首屏的图片开始请求。
需要注意,由于我们设置了 200 像素的预加载范围,首屏之外的部分图片也可能提前加载,这是正常现象。
第四步:缓慢向下滚动。
观察新的图片请求是否在接近可视区域时开始出现。
第五步:测试加载失败。
故意把一张图片路径写错,观察页面是否出现错误状态。
通过这些测试,可以判断代码是否真正按照预期执行,而不是仅仅在 HTML 中写了一个 lazy 字样。
七、懒加载是不是图片优化的全部?
当然不是。
图片网页的性能还受到很多因素影响。
| 优化方向 | 主要解决的问题 |
|---|---|
| 图片压缩 | 降低下载体积 |
| WebP / AVIF | 在适合的场景下改善图片压缩效率 |
srcset / sizes |
为不同屏幕提供合适尺寸 |
loading="lazy" |
延迟加载暂时不需要的图片 |
IntersectionObserver |
自定义可见性触发逻辑 |
| 指定图片尺寸 | 减少布局偏移 |
| CDN | 缩短资源传输路径、改善分发能力 |
实际项目中,往往需要组合使用多种优化方法。
例如,一张 8 MB 的原始图片,即使通过懒加载延迟了请求,在真正显示时仍然需要下载大量数据。
因此,懒加载不能替代图片压缩。
另一个值得注意的问题是:
图片懒加载并不能解决超长页面中大量 DOM 节点造成的性能压力。
如果页面包含成千上万个复杂卡片,可能还需要考虑列表虚拟化或分页加载。
八、AI 编程助手开发图片网站时,应该增加哪些要求?
如果只告诉 AI:
“开发一个图片展示网站,要求加载流畅。”
它可能只关注页面样式和动画效果。
为了获得更可靠的结果,可以明确提出:
图片列表必须考虑首屏性能。非首屏图片优先使用浏览器原生懒加载;需要自定义提前加载距离时可使用 IntersectionObserver。明确图片宽高或宽高比,避免布局跳动。不要在每次 scroll 事件中遍历所有图片并反复强制计算布局。必须通过浏览器 Network 面板验证实际请求时机。
如果项目还要求支持手机访问,可以进一步要求:
根据设备屏幕大小使用合适分辨率的图片,避免手机端直接下载超大尺寸原图。
这样的描述比“优化性能”更容易执行和验证。
九、总结
AI 编程助手能够快速生成图片展示网页,但页面能够正常显示,不代表加载策略已经合理。
当图片数量增加时,浏览器可能面临更大的网络、内存和渲染压力。
通过本文的实践,可以总结出几个重要原则:
- 首先考虑浏览器原生
loading="lazy"。 - 需要自定义加载时机时,再使用
IntersectionObserver。 - 使用
rootMargin可以提前加载即将进入视口的图片。 - 图片开始加载后,应停止不必要的观察。
- 为图片预留空间,减少页面布局变化。
- 使用 Network 面板验证实际加载行为。
- 图片压缩、响应式尺寸与懒加载应结合使用。
我认为,AI 生成网页时,真正需要关注的不只是页面能否显示出来,还应该考虑用户实际访问时的网络和设备条件。
一个体验良好的图片网站,不是让所有图片尽快开始下载,而是让用户需要的图片尽快出现。
- 点赞
- 收藏
- 关注作者
评论(0)