Chrome 标签的五层限流及每层的解决方案
这是每个抓取工程师最终都会遇到的 bug 报告:*"抓取器从无限滚动源获取了 20 项,然后停止。看标签时工作正常,后台时就不行。"* 这种情况的常见解决办法——添加等待、添加滚动、再试一次——都无效,因为问题不出在你的代码上,而在于 **Chrome 的架构**,它在至少五种不同的方式上对后台标签的工作默默设限。 [Scrapewright](https://github.com/singhand-labs/scrapewright) 是我所知道的最完整的这些层次的开源分类及每个层次的对策。它提供了关于 Chrome 内部工作的免费教育,让我来带你看看。
## 第 1 层:页面检查自己是否可见
**机制:** 标签中的 JavaScript 可以读取 `document.visibilityState` 和 `document.hidden`。很多源代码在加载循环中检查“我可见吗”——在切换标签时无限滚动会暂停。这是页面自己的限流,最柔和的一层。
**对策:** 通过 MAIN 线程注入,覆盖可见性属性报告 `visible`,加上 `requestAnimationFrame` 的保持活动循环。简单,默认启用。关键是,这只修复了页面的 **信念**——这并不让 Chrome 的行为有所不同。这就引出了…
## 第 2 层:Chrome 停止为后台标签生成帧
**机制:** 这是严重的问题,也是大多数 "懒加载从未触发" 报告的根本原因。Chrome 仅为 **聚焦窗口的活动标签** 生成合成帧。没有帧 → `IntersectionObserver` 回调(依赖于帧驱动的交互更新)永远不会触发 → 基于滚动位置的加载器无法观察到哨兵进入视野。你可以派发所有想要的事件;观察者机器并不在倾听。这是架构问题,而非计时器问题。
**对策:** 只有一个办法:使标签实际处于活动状态。Scrapewright 在操作期间激活抓取标签("粘性"激活——它在连续操作之间保持活动,而不是在激活/恢复之间反复切换,并在抓取标签关闭时恢复用户上次点击的标签)。这里的工程上讲究的是舞蹈——抑制扩展自身的激活事件,以便它能辨别自己的标签切换与用户的标签切换,存活 MV3 服务工作者的暂停,使用会话存储,处理窗口焦点——所有这些都在 `lib/tab-activation.js` 中。这是该仓库中最仔细的代码之一。
## 第 3 层:Chrome 限制后台计时器/渲染策略
**机制:** 即使有帧,后台标签的计时器也会被限制,并且在某些平台上,基于遮挡的优先级也会降低(`CalculateNativeWinOcclusion` 在 Windows 上,渲染器后台化普遍存在)。
**对策:** 启动标志。该仓库提供了一个CLI(`scrapewright throttle on`),可以重写你的 Chrome 启动器(每个操作系统:`.desktop` 文件、包装应用、快捷方式),以添加 `--disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-renderer-backgrounding --disable-features=CalculateNativeWinOcclusion`。必要但不充分:仅仅使用标志并不能修复第 2 层,这就是为什么那些“已经尝试过标志”的人仍然遇到抓取器失败的问题。
## 第 4 层:页面过滤不受信任的输入
**机制:** 使标签处于活动状态并且帧流动后,你滚动——却什么也加载不出来。因为页面的加载器在滚动事件上检查 `event.isTrusted`,而程序化执行的 `scrollBy` 永远被认为是不受信任的。
**对策:** 这是一个手术性的对策——暂时通过 `chrome.debugger` 附加 CDP,并发出 `Input.dispatchMouseEvent` 滚轮事件,这些事件通过 Chrome 的真实输入管道进入,并带有 `isTrusted: true`。仅限于 `Input.*` 命令(检测表面最小),尝试次数有限,仅在程序化滚动停滞时启用。(这个问题值得单独写一篇文章——该仓库的白皮书为此提供了一篇文章。)
## 第 5 层:操作互相交错时
**机制:** 更微妙——悬浮卡片增效、详细页面钻取(`$openTab` 子标签)、悬浮后消失:每个操作都需要 CDP 输入或等待渲染,并且每个操作都独立需要一个 **活动** 标签。修复一个操作的激活,接下来操作却又会再次碰到第 2 层;负载较重的详细页面在后台标签中安装应用 shell,并从来没有渲染内容——它们上的每次提取都得出确定性为空。
**对策:** 将激活作为 **基础设施**,而不是每个操作。每条接触帧或输入的路径——滚动、悬浮、悬浮消失、子标签加载——都经过相同的激活请求。该仓库的历史明确教训是:相同的机制 → 每个地方相同的基础设施,否则你将为每个功能重新学习。
## 为什么这个分类很重要
这五层是 **独立的机制**,都表现为相同的症状——“后台抓取提取不足”。修复一个问题会显现下一个问题;故障链是简单修复会停滞的原因。如果你想要简单的版本:
| 层 | 实际限制的内容 | 修复 |
|----|----------------|------|
| 1 | 页面 JS 的可见性检查 | 覆盖 `visibilityState`(MAIN 世界) |
| 2 | 合成帧生成 | 真正的标签激活(粘性) |
| 3 | 背景计时器/遮挡 | Chrome 启动标志 |
| 4 | `isTrusted` 过滤 | CDP `Input.dispatchMouseEvent` 滚轮 |
| 5 | 每个操作重新限流 | 将激活作为所有操作的共享基础设施 |
即使你从未抓取任何东西,这个层次结构仍然是关于 Chrome 如何实际调度工作的最佳实用文档之一——白皮书(第 9 节)详细解释了每一层及其暴露的故障故事。
**仓库:** [github.com/singhand-labs/scrapewright](https://github.com/singhand-labs/scrapewright) — GPLv3。可以从 `lib/visibility-keepalive.js`、`lib/tab-activation.js` 和 `lib/scroll-ops.js` 开始,然后在代码打开的情况下阅读白皮书第 9 节。
---
原文链接:[点击查看](https://www.v2ex.com/t/1236316)
## 第 1 层:页面检查自己是否可见
**机制:** 标签中的 JavaScript 可以读取 `document.visibilityState` 和 `document.hidden`。很多源代码在加载循环中检查“我可见吗”——在切换标签时无限滚动会暂停。这是页面自己的限流,最柔和的一层。
**对策:** 通过 MAIN 线程注入,覆盖可见性属性报告 `visible`,加上 `requestAnimationFrame` 的保持活动循环。简单,默认启用。关键是,这只修复了页面的 **信念**——这并不让 Chrome 的行为有所不同。这就引出了…
## 第 2 层:Chrome 停止为后台标签生成帧
**机制:** 这是严重的问题,也是大多数 "懒加载从未触发" 报告的根本原因。Chrome 仅为 **聚焦窗口的活动标签** 生成合成帧。没有帧 → `IntersectionObserver` 回调(依赖于帧驱动的交互更新)永远不会触发 → 基于滚动位置的加载器无法观察到哨兵进入视野。你可以派发所有想要的事件;观察者机器并不在倾听。这是架构问题,而非计时器问题。
**对策:** 只有一个办法:使标签实际处于活动状态。Scrapewright 在操作期间激活抓取标签("粘性"激活——它在连续操作之间保持活动,而不是在激活/恢复之间反复切换,并在抓取标签关闭时恢复用户上次点击的标签)。这里的工程上讲究的是舞蹈——抑制扩展自身的激活事件,以便它能辨别自己的标签切换与用户的标签切换,存活 MV3 服务工作者的暂停,使用会话存储,处理窗口焦点——所有这些都在 `lib/tab-activation.js` 中。这是该仓库中最仔细的代码之一。
## 第 3 层:Chrome 限制后台计时器/渲染策略
**机制:** 即使有帧,后台标签的计时器也会被限制,并且在某些平台上,基于遮挡的优先级也会降低(`CalculateNativeWinOcclusion` 在 Windows 上,渲染器后台化普遍存在)。
**对策:** 启动标志。该仓库提供了一个CLI(`scrapewright throttle on`),可以重写你的 Chrome 启动器(每个操作系统:`.desktop` 文件、包装应用、快捷方式),以添加 `--disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-renderer-backgrounding --disable-features=CalculateNativeWinOcclusion`。必要但不充分:仅仅使用标志并不能修复第 2 层,这就是为什么那些“已经尝试过标志”的人仍然遇到抓取器失败的问题。
## 第 4 层:页面过滤不受信任的输入
**机制:** 使标签处于活动状态并且帧流动后,你滚动——却什么也加载不出来。因为页面的加载器在滚动事件上检查 `event.isTrusted`,而程序化执行的 `scrollBy` 永远被认为是不受信任的。
**对策:** 这是一个手术性的对策——暂时通过 `chrome.debugger` 附加 CDP,并发出 `Input.dispatchMouseEvent` 滚轮事件,这些事件通过 Chrome 的真实输入管道进入,并带有 `isTrusted: true`。仅限于 `Input.*` 命令(检测表面最小),尝试次数有限,仅在程序化滚动停滞时启用。(这个问题值得单独写一篇文章——该仓库的白皮书为此提供了一篇文章。)
## 第 5 层:操作互相交错时
**机制:** 更微妙——悬浮卡片增效、详细页面钻取(`$openTab` 子标签)、悬浮后消失:每个操作都需要 CDP 输入或等待渲染,并且每个操作都独立需要一个 **活动** 标签。修复一个操作的激活,接下来操作却又会再次碰到第 2 层;负载较重的详细页面在后台标签中安装应用 shell,并从来没有渲染内容——它们上的每次提取都得出确定性为空。
**对策:** 将激活作为 **基础设施**,而不是每个操作。每条接触帧或输入的路径——滚动、悬浮、悬浮消失、子标签加载——都经过相同的激活请求。该仓库的历史明确教训是:相同的机制 → 每个地方相同的基础设施,否则你将为每个功能重新学习。
## 为什么这个分类很重要
这五层是 **独立的机制**,都表现为相同的症状——“后台抓取提取不足”。修复一个问题会显现下一个问题;故障链是简单修复会停滞的原因。如果你想要简单的版本:
| 层 | 实际限制的内容 | 修复 |
|----|----------------|------|
| 1 | 页面 JS 的可见性检查 | 覆盖 `visibilityState`(MAIN 世界) |
| 2 | 合成帧生成 | 真正的标签激活(粘性) |
| 3 | 背景计时器/遮挡 | Chrome 启动标志 |
| 4 | `isTrusted` 过滤 | CDP `Input.dispatchMouseEvent` 滚轮 |
| 5 | 每个操作重新限流 | 将激活作为所有操作的共享基础设施 |
即使你从未抓取任何东西,这个层次结构仍然是关于 Chrome 如何实际调度工作的最佳实用文档之一——白皮书(第 9 节)详细解释了每一层及其暴露的故障故事。
**仓库:** [github.com/singhand-labs/scrapewright](https://github.com/singhand-labs/scrapewright) — GPLv3。可以从 `lib/visibility-keepalive.js`、`lib/tab-activation.js` 和 `lib/scroll-ops.js` 开始,然后在代码打开的情况下阅读白皮书第 9 节。
---
原文链接:[点击查看](https://www.v2ex.com/t/1236316)
评论
暂无评论。