真正可靠的方案是结合visualViewport API、safe-area-inset环境变量和滚动事件委托动态计算卡片是否滑出安全视口:通过visualViewport.pageTop与height实时获取有效可视区域,叠加env(safe-area-inset-*)获取安全区偏移,仅对滚动影响范围内的卡片做判定,并用节流与批量更新保障性能。

在 H5 瀑布流中,判断卡片是否滑出“安全视口”(即当前用户真正可见、未被状态栏/导航栏/键盘遮挡、且未滚动出可视区域的部分),不能只依赖 window.innerHeight 或 getBoundingClientRect() 的原始结果——因为这些值在 iOS/Android 上受软键盘弹出、地址栏收起、安全区缩进、100vh 失效等干扰极大。真正可靠的方案,是结合 视觉视口 API(visualViewport) + 安全区环境变量(env(safe-area-inset-*)) + 事件委托监听滚动,动态计算每张卡片相对于“当前有效可视区域”的位置。
用 visualViewport 精确获取真实可视高度与偏移
传统 window.innerHeight 在 iOS 键盘弹出时几乎不变,而 visualViewport.height 和 visualViewport.pageTop 能实时反映用户真正看到的区域尺寸和滚动偏移:
- 监听
visualViewport.resize事件,捕获每次键盘弹起/收起、横竖屏切换导致的可视区域变化 - 计算“安全视口顶部边界”:
topBound = visualViewport.pageTop + getSafeInsetTop() - 计算“安全视口底部边界”:
bottomBound = visualViewport.pageTop + visualViewport.height - getSafeInsetBottom() - 其中
getSafeInsetTop()可通过parseInt(getComputedStyle(document.documentElement).getPropertyValue('env(safe-area-inset-top)', '0px'))获取
给瀑布流容器绑定一次滚动委托,避免重复监听
不要给每张卡片单独绑 scroll 或 intersectionObserver——瀑布流卡片数量多、动态加载频繁,性能差且易内存泄漏。正确做法是:
- 在瀑布流外层容器(如
class="waterfall")上用addEventListener('scroll', handler, {passive: true}) - handler 中不遍历全部卡片,而是只检查「最近几次滚动影响范围内的卡片」:比如缓存上一次触发时的
scrollTop,本次只处理scrollTop ± 2 * visualViewport.height区间内的卡片 - 卡片需带
data-index或data-id,便于后续精准标记状态(如data-in-safeview="false")
卡片是否“已滑出安全视口”的判定逻辑
一张卡片是否还处于可用的安全可视范围内,需同时满足两个条件:
-
物理可见性:卡片的
getBoundingClientRect().top < visualViewport.height且.bottom > 0(即至少部分在视觉视口内) -
安全区兼容性:卡片的
top必须 ≥safeTop,bottom必须 ≤safeBottom;若卡片设置了position: sticky或固定定位,还需额外减去safe-area-inset-top偏移再比对 - 只要任一条件不满足,就认为它“已滑出安全视口”,可触发清理动作(如暂停视频、卸载 iframe、释放 canvas 上下文)
补充:防抖 + 批量更新提升性能
滚动事件高频触发,直接逐帧计算会卡顿。建议:
- 用
requestIdleCallback或节流(300ms)包裹判定逻辑 - 把“滑出”状态变更收集起来,统一在下一帧批量更新 DOM 属性或触发事件(如
dispatchEvent(new CustomEvent('card-out-of-safeview', {detail: {id}}))) - 对已滑出的卡片,延迟 500ms 再执行资源释放,防止快速来回滚动误判

















