intersectionobserver比window.onscroll更可靠,因其是浏览器底层异步回调,无法被js覆盖或阻塞,支持精确阈值控制(如1px可见即上报),且不受长任务阻塞、页面卡顿或劫持脚本影响。

第三方统计代码(比如 CNZZ、百度统计、神策)一旦被劫持,track 调用可能被篡改、拦截或重定向到恶意域名,导致曝光数据失真甚至泄露用户行为。纯靠 HTTPS + 源码自查不够——劫持常发生在 CDN 中转、运营商缓存、或统计 SDK 自身被污染时。真正可控的曝光统计,必须绕过第三方脚本执行链,用浏览器原生能力捕获真实可见性。
为什么 IntersectionObserver 比 window.onscroll 更可靠
滚动监听依赖 window.onscroll 或 requestAnimationFrame 轮询,容易被劫持脚本覆盖、节流失效、或因页面卡顿漏触发;而 IntersectionObserver 是浏览器底层异步回调,无法被 JS 覆盖或阻塞,且天然支持元素进入视口的精确阈值控制(如 1px 可见即上报)。即使第三方统计 SDK 被注入恶意逻辑,只要你的曝光采集逻辑不依赖它,就能保底生效。
- 劫持者通常只篡改已知统计函数(如
_czc.push、_hmt.push),不会动原生 API -
IntersectionObserver回调在微任务队列中执行,不受长任务阻塞影响 - 不需要监听页面滚动/resize,减少性能开销和误报(如折叠面板展开后元素才可见)
如何避免 observer 实例被第三方 script 清除或篡改
劫持脚本可能遍历全局对象、重写 IntersectionObserver 构造函数,或清空已注册的 observer。关键是在初始化阶段“冻结”实例并隔离作用域:
- 立即执行匿名函数包裹整个插件逻辑,避免变量暴露到全局
- 创建 observer 后,立即将其赋值给一个不可枚举、不可配置的属性:
Object.defineProperty(window, '__expoIO', { value: new IntersectionObserver(...), writable: false, configurable: false }) - 对目标元素打唯一标记(如
data-expo-id),不在 DOM 上直接挂载回调函数,防止被劫持脚本遍历修改 - 禁用
console输出调试信息,避免被恶意脚本 hook 后窃取上报内容
上报时机与防重复策略
仅靠 isIntersecting === true 上报会误触:元素短暂划入又划出、SPA 路由切换后元素复用、服务端渲染首屏直出等场景下,同一元素可能被多次 observe。需结合业务语义做去重:
- 为每个曝光单元设置唯一
data-expo-key(如"product-card-123"),内存中用Set缓存已上报 key,生命周期内不重复上报 - 添加最小可见时长阈值(如 300ms),用
setTimeout+clearTimeout控制,避免滚动快速掠过时误报 - 上报前检查
location.href是否匹配预期域名(防 iframe 嵌套劫持跳转后仍上报) - 优先使用
sendBeacon发送,失败降级为fetch(POST)+keepalive: true
如何验证是否真被劫持了统计
不能只看「有没有上报」,要看「上报内容是否被篡改」。最直接的方法是比对两个独立信道的数据:
- 通道一:你自研的
IntersectionObserver插件,上报字段含expoid、ts、viewportRatio、url - 通道二:同一页面加载未压缩、未合并的原始统计 SDK(如百度统计官网下载的
hm.js),开启 debug 模式,抓包看https://hm.baidu.com/hm.gif?...参数是否被注入额外参数或跳转 - 若两者曝光 UV 差异 >15%,且第三方请求中出现非预期域名(如
ads.xxx.com)、异常 referer、或参数被 base64 编码再解码后含敏感字段,则基本确认被劫持
真正难防的是 DNS 层或运营商缓存劫持——它们连你的 JS 文件都可能返回篡改后的版本。这时候,IntersectionObserver 插件本身也得走 HTTPS 加载,且建议托管在自有域名(而非 cdn.jsdelivr.net 这类公共 CDN),否则 loader 脚本第一步就被掉包了。










