前端怎样按可见比例加载列表项
前端怎样按可见比例加载列表项
列表里有不少卡片,但没有必要一进入页面就填充所有详情。这里把目标限定为:已经存在的卡片,进入视口的面积达到自身面积的 50% 时加载一次;组件卸载后,不再向旧 DOM 写入结果。这不是滚动到底翻页,也不是曝光统计系统。
本文依据 Intersection Observer 的平台说明与规范。官方说明:https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API;规范:https://w3c.github.io/IntersectionObserver/。以下示例用于说明实现方式,未执行浏览器实测。
先弄清楚 50% 的分母
intersectionRatio 大致表示目标元素交集矩形面积与目标边界矩形面积之比,不是目标占整个屏幕的比例,也不保证用户真正看到了内容。默认观察并不负责判断遮挡、透明度等视觉因素。
threshold: 0.5 是通知阈值,不是“回调内所有条目都已经达到一半”的保证。开始观察会产生初始通知,比例向上或向下跨过阈值也可能通知。因此要同时检查 isIntersecting 和 intersectionRatio >= 0.5。

一个可直接接入现有列表的实现
先让卡片有稳定的占位尺寸。下面两条演示数据都在本地,便于把注意力放在观察和清理上;它们不代表网络懒加载节省量。真实项目可替换 loadItem,并在请求中使用传入的 signal。
-
卡片 A
等待进入视口
-
卡片 B
等待进入视口
// 返回清理函数,由组件的卸载钩子调用
function mountLazyItems(list, loadItem) {
const ratio = 0.5;
const controller = new AbortController();
const started = new WeakSet();
let alive = true;
let observer = null;
const items = list.querySelectorAll("[data-item-id]");
async function loadOnce(item) {
// 同一批通知或重复跨阈值都不能触发重复加载
if (!alive || started.has(item)) return;
started.add(item);
if (observer) observer.unobserve(item);
const body = item.querySelector("[data-body]");
body.textContent = "加载中";
try {
const text = await loadItem(item.dataset.itemId, {
signal: controller.signal
});
// 清理后即使请求成功,也不能更新旧卡片
if (!alive) return;
body.textContent = text;
} catch (error) {
if (!alive || controller.signal.aborted) return;
body.textContent = "加载失败,请刷新后重试";
}
}
if ("IntersectionObserver" in window) {
observer = new IntersectionObserver(entries => {
for (const entry of entries) {
if (entry.isIntersecting &&
entry.intersectionRatio >= ratio) {
void loadOnce(entry.target);
}
}
}, {
root: null,
rootMargin: "0px",
threshold: ratio
});
for (const item of items) observer.observe(item);
} else {
// 降级为立即加载,不能承诺仍按可见比例触发
for (const item of items) void loadOnce(item);
}
return function cleanup() {
alive = false;
if (observer) observer.disconnect();
controller.abort();
};
}
// 本地演示加载器;真实请求需要主动使用 signal
const demoData = { a: "卡片 A 的详情", b: "卡片 B 的详情" };
const cleanupCards = mountLazyItems(
document.querySelector("#cards"),
async (id, { signal }) => {
if (signal.aborted) {
throw new DOMException("已取消", "AbortError");
}
return demoData[id] ?? "没有详情";
}
);
// 页面或组件真正卸载时调用 cleanupCards(),不要立即调用
这个实现只观察挂载时已存在的节点。追加卡片时,需要在组件维护的观察器上显式注册新节点,或清理后重新挂载;不要误以为 querySelectorAll 会自动追踪后来插入的节点。若替换整个列表,也应先执行旧的清理函数。
失败策略也刻意保持明确:该次挂载中只尝试一次,显示失败提示,不因反复滚动而无限请求。若需要按钮重试,应另行维护失败状态和重试入口,同时清理按钮监听器。
rootMargin 是提前量,不是阈值的另一种写法
root: null 使用顶层视口作为根。当前示例选择 rootMargin: "0px",让比例相对于未扩展的根边界计算,但祖先裁剪仍可能影响交集。
如果改成 rootMargin: "0px 0px 300px 0px",根边界下方被扩展,用于更早预加载。此时达到 0.5 是相对于扩展后的观察区域,不能把它解释为“屏幕里已经看见卡片的一半”。我会把“按实际几何可见比例加载”和“接近视口先预取”作为两种配置,而不是混用同一个曝光口径。
在独立滚动容器内观察时,root 应明确指向对应容器,并确认目标处于根的后代关系中。负 margin 会收缩根边界,通常意味着更晚满足条件。
卸载时需要处理三件不同的事

unobserve(item) 停止观察一张已经开始加载的卡片,配合 WeakSet 去重;disconnect() 解除整个观察器的目标关联;AbortController.abort() 通知支持 signal 的异步操作取消。三者不能互相替代。
此外,alive 标记防止清理后继续写 DOM。即使自定义加载器忽略 signal,或者异步操作已经完成,也要在结果写入前再检查组件是否还有效。观察回调应保持轻量,不要把大量同步解析或布局计算塞在回调里。
预期现象与两个容易踩的边界
预期行为是:初始不足 50% 的卡片仍保留占位文字;达到条件后变成“加载中”,随后显示详情;再次移出再进入不会重复加载。这是阅读代码得到的预期,不是实测记录。初始已经满足条件的卡片可以直接开始加载,不必等待用户滚动一次。
第一,特别高的卡片可能永远无法在视口里展示一半面积。若卡片高度明显大于视口,应改用更小比例、固定尺寸的触发区域,或采用接近视口预加载方案,不能坚持所有卡片统一 0.5。
第二,占位布局如果在图片加载后大幅改变,比例也会变。为媒体预留尺寸,同时避免加载详情后尺寸来回震荡。IntersectionObserver 提供异步阈值通知,不是连续、逐像素的滚动采样器。
这类需求的关键不在监听多少次 scroll,而在先定义比例口径:用 threshold 接收跨阈值通知,用回调条件决定是否加载,用清理函数结束观察与异步结果的生命周期。
前端怎样用 AbortSignal 统一移除事件监听
- 上一篇
- 前端怎样用 AbortSignal 统一移除事件监听
- 下一篇
- 前端 PerformanceNavigationTiming 怎样计算首字节时间
-
- 文章 · 前端 | 42分钟前 | typescript ·
- TypeScript 怎样让索引访问显式处理 undefined
- 404浏览 收藏
-
- 文章 · 前端 | 42分钟前 |
- 前端怎样按语言边界切分用户文本
- 410浏览 收藏
-
- 文章 · 前端 | 44分钟前 | 前端 · javascript · JavaScript 深拷贝 嵌套对象 structuredClone
- 前端怎样深拷贝含嵌套对象的数据
- 312浏览 收藏
-
- 文章 · 前端 | 47分钟前 |
- CSS color-mix 怎样生成主题的中间色
- 236浏览 收藏
-
- 文章 · 前端 | 50分钟前 |
- CSS 容器查询怎样让卡片按父容器适配
- 174浏览 收藏
-
- 文章 · 前端 | 52分钟前 | 前端 PerformanceObserver CLS LayoutShift layout-shift
- 前端怎样读取布局偏移条目
- 153浏览 收藏
-
- 文章 · 前端 | 54分钟前 |
- 前端 PerformanceNavigationTiming 怎样计算首字节时间
- 465浏览 收藏
-
- 文章 · 前端 | 57分钟前 | 前端 · javascript · 事件监听 AbortController AbortSignal addEventListener 前端事件清理
- 前端怎样用 AbortSignal 统一移除事件监听
- 376浏览 收藏
-
- 文章 · 前端 | 59分钟前 |
- 前端怎样查询当前 Web Lock 持有者
- 467浏览 收藏
-
- 文章 · 前端 | 1小时前 |
- 前端怎样监听其他标签页的 localStorage 变化
- 473浏览 收藏
-
- 文章 · 前端 | 1小时前 | 前端 Fetch keepalive sendBeacon pagehide
- 前端 fetch 怎样在页面卸载时发送小请求
- 401浏览 收藏
-
- 文章 · 前端 | 1小时前 | javascript · 前端 响应体 Fetch API bodyUsed Response.clone
- 前端 Response 读取后怎样复制给两个消费者
- 217浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 525次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 582次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 609次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 558次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 379次使用
-
- JavaScript函数定义及示例详解
- 2025-05-11 502浏览
-
- 智能体安全引领产业升级——国内AI安全产品市场深度分析
- 2026-08-21 501浏览
-
- CSS变量简化按钮悬停效果技巧
- 2026-05-31 501浏览
-
- JavaScript符号类型详解与应用
- 2026-05-31 501浏览
-
- HTML剪贴板复制粘贴怎么用
- 2026-05-26 501浏览

