用 intersectionobserver 做广告曝光统计,关键在于加时长校验(≥300ms)和状态隔离,配合 intersectionratio ≥ 0.3、动态 root 设置、阈值分层及首屏直报,避免闪现误判;小程序需适配 relativeto 容器并上调时长阈值至 400–500ms。

直接用 IntersectionObserver 做广告曝光统计,关键不是“能不能监听”,而是“怎么避免把用户滑动时的闪现、误入、局部抖动都算成一次有效曝光”——90% 的线上埋点失真,都源于没加时长校验和状态隔离。
为什么不能只看 isIntersecting === true
广告位常嵌在轮播、Tab、弹窗或局部滚动容器中,isIntersecting 会因防抖延迟、快速滚动、CSS 动画重绘等频繁开关。比如一个 banner 在视口内只停留 80ms,isIntersecting 仍可能触发两次回调。
- 必须配合时间戳判断:在
entry.isIntersecting === true时写入el.dataset.exposureStart = Date.now().toString() - 仅当
entry.intersectionRatio >= 0.3且Date.now() - Number(el.dataset.exposureStart) >= 300才视为有效曝光(300ms 是广告行业常见下限) - 上报后清空
dataset.exposureStart,防止后续重复触发 - 不要依赖
isIntersecting === false来重置状态——它可能根本不触发(如用户直接跳转离开页面)
root 和 rootMargin 必须按广告容器动态指定
全站广告不会都挂在 document.body 下:信息流广告在 .feed-container,侧边栏广告在 .sidebar,弹窗广告在 .modal-content。若统一设 root: null,这些广告将完全无法被正确观测。
- 每个广告模块初始化时,需显式传入其最近的、带
overflow: auto/scroll且有固定高度的父容器作为root -
rootMargin不是为“提前加载”设的,而是为适配容器内边距:比如rootMargin: '20px 0px'可让广告顶部距离容器顶部 20px 就开始计算交叉比 - Tab 切换后新渲染的广告,必须调用
observer.observe(adEl);销毁前(如 Tab 关闭)必须调用observer.unobserve(adEl),否则内存泄漏
阈值分层 + 首屏直报策略
广告曝光逻辑不能一刀切。首屏广告要“见即报”,而瀑布流中第 20 个广告则需更严苛的判定条件,否则数据全是噪声。
- 首屏广告(可通过
getBoundingClientRect().top 初筛)绕过 <code>IntersectionObserver,在 DOM ready 后直接打点,附带isFirstScreen: true - 非首屏广告使用双阈值:
threshold: [0.1, 0.5],仅当intersectionRatio >= 0.5且持续 ≥300ms 才触发主埋点 - 预加载资源(如广告图片)可单独用
rootMargin: '300px'提前触发,但该行为不参与曝光判定——曝光只认实际交叉比,不认 margin - 轮播广告需支持
resetOnLeave: true:每次离开视口后重置状态,允许同一广告多次曝光
小程序环境的 relativeToViewport 陷阱
微信小程序里 IntersectionObserver 默认参照视口,看似省事,但遇到 scroll-view 嵌套广告时极易失效——因为 scroll-view 自身就是滚动上下文,必须显式指定参照目标。
- 广告在普通页面:用
observer.relativeToViewport({ top: 0 }) - 广告在
scroll-view内:必须用observer.relativeTo('.scroll-view-selector', { top: 0 }),且确保该 selector 精确命中滚动容器 - 小程序的回调频率更低(约为 Web 端 1/3),所以时长校验阈值建议上调至 400–500ms,避免漏报
- 不要复用同一个 observer 实例监听跨容器广告——不同
root场景必须新建实例,否则relativeTo行为不可控
最易被忽略的一点:广告 DOM 节点可能被 Vue/React 动态销毁又重建(如列表刷新、AB 测试切换),此时仅靠 unobserve 不够,必须在节点移除前读取并持久化当前状态到 localStorage,否则用户快速滑动+页面重绘,曝光状态就丢了。










