• 请不要在回答技术问题时复制粘贴 AI 生成的内容
aaa69532
V2EX  ›  程序员

geek-five-layer-throttling-stack

  •  
  •   aaa69532 · 5h 40m ago · 199 views

    Five Layers of Chrome's Tab Throttling, and a Fix for Each

    Here's a bug report every scraping engineer meets eventually: "The scraper gets 20 items from an infinite-scroll feed, then stops. Works fine when I watch the tab. Broken when it's in the background." The usual responses — add waits, add scrolls, retry harder — fail, because the failure isn't in your code. It's in Chrome's architecture, which is quietly hostile to background-tab work in at least five distinct ways. Scrapewright contains, as far as I know, the most complete open-source taxonomy of these layers and a countermeasure for each. It's a free education in Chrome internals; let me walk it.

    Layer 1: The page checks its own visibility

    Mechanism: JavaScript in the tab can read document.visibilityState and document.hidden. Tons of feed code gates its loading loop on "am I visible" — infinite scroll that pauses when you switch tabs. This is the page's own throttle, the softest layer.

    Countermeasure: a MAIN-world injection that overrides the visibility properties to report visible, plus a requestAnimationFrame keep-alive loop. Cheap, default-on. Crucially, this fixes only the page's belief — it does not make Chrome do anything differently. Which brings us to…

    Layer 2: Chrome stops producing frames for background tabs

    Mechanism: the hard one, and the root cause of most "lazy-load never fires" reports. Chrome produces compositor frames only for the active tab of the focused window. No frames → IntersectionObserver callbacks (which fire on frame-driven intersection updates) never fire → scroll-position-driven loaders never observe the sentinel entering view. You can dispatch all the events you like; the observer machinery isn't listening. This is architectural, not a timer.

    Countermeasure: there is exactly one: make the tab actually active. Scrapewright activates the scrape tab for the duration of an operation ("sticky" activation — it stays active across back-to-back operations instead of activate/restore churn, and restores the user's last-clicked tab when the scrape tab closes). The engineering here is the choreography — suppressing the extension's own activation events so it can tell its own tab switches from the user's, surviving MV3 service-worker suspension via session storage, handling window focus — all in lib/tab-activation.js. It's some of the most careful code in the repo.

    Layer 3: Chrome throttles background timers/rendering policy

    Mechanism: even with frames, background tabs get throttled timers and, on some platforms, occlusion-based de-prioritization (CalculateNativeWinOcclusion on Windows, renderer backgrounding generally).

    Countermeasure: launch flags. The repo ships a CLI (scrapewright throttle on) that rewrites your Chrome launcher (per-OS: .desktop file, wrapper app, shortcut) to add --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-renderer-backgrounding --disable-features=CalculateNativeWinOcclusion. Necessary-but-not-sufficient: flags alone don't fix layer 2, which is why people who "already tried the flags" still had broken scrapers.

    Layer 4: The page filters untrusted input

    Mechanism: with the tab active and frames flowing, you scroll — and nothing loads. Because the page's loader checks event.isTrusted on scroll events, and programmatic scrollBy is spec-level untrusted forever.

    Countermeasure: the surgical one — transiently attach CDP via chrome.debugger and issue Input.dispatchMouseEvent wheel events, which enter through Chrome's real input pipeline and arrive with isTrusted: true. Only Input.* commands (minimal detection surface), capped attempts, deployed only when programmatic scroll stalls. (This deserves its own article — and the repo's whitepaper gives it one.)

    Layer 5: When ops interleave

    Mechanism: subtler — hover-card enrichment, detail-page drill-down ($openTab sub-tabs), dismiss-after-hover: each of these performs CDP input or waits for rendering, and each one independently needs an active tab. Fix activation for one op and the next op still runs into layer 2 again; JS-heavy detail pages mount an app shell in a background tab and then never render the content — every extraction on them returns deterministically empty.

    Countermeasure: activation as infrastructure, not per-op. Every path that touches frames or input — scroll, hover, hover-dismiss, sub-tab loading — routes through the same activation request. The lesson the repo's history makes explicit: same mechanism → same infrastructure, everywhere, or you re-learn it per feature.

    Why this taxonomy matters

    The five layers are independent mechanisms that all manifest as the same symptom — "background scraping under-retrieves." Fixing one reveals the next; the failure chain is why naive fixes stall out. If you want the short version burned in:

    Layer What's actually throttled Fix
    1 Page JS's visibility check Override visibilityState (MAIN world)
    2 Compositor frame production Real tab activation (sticky)
    3 Background timers/occlusion Chrome launch flags
    4 isTrusted filtering CDP Input.dispatchMouseEvent wheel
    5 Per-op re-throttling Activation as shared infra for every op

    Even if you never scrape anything, this stack is one of the best practical documents on how Chrome actually schedules work — the whitepaper (§9) explains each layer with the failure story that uncovered it.

    Repo: github.com/singhand-labs/scrapewright — GPLv3. Start in lib/visibility-keepalive.js, lib/tab-activation.js, and lib/scroll-ops.js, then read whitepaper §9 with the code open.

    No Comments Yet
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Solana   ·   899 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 22:00 · PVG 06:00 · LAX 15:00 · JFK 18:00
    ♥ Do have faith in what you're doing.