# requestAnimationFrame background tab throttling, a story

> Background tab throttling stops requestAnimationFrame. Our test tab was in the background, two detectors never answered, and Page.bringToFront fixed both.

Source: https://mosaicbrowser.com/blog/requestanimationframe-background-tab

## A detector that never answered because its tab was in the background

Background tab throttling stops requestAnimationFrame. Our test tab was in the background, two detectors never answered, and Page.bringToFront fixed both.

![Background tab versus Foreground tab, each name in its own tile: Never ready in 140s, then ready in 8.2s](https://mosaicbrowser.com/covers/requestanimationframe-background-tab.webp)

Chromium does not run `requestAnimationFrame` callbacks in a background tab, and background tab throttling holds that tab's timers to about one call a second. We knew that as a fact about browsers. We did not know it was a fact about our own test harness, and two detectors sat at `unread` because of it, one of them for weeks. One call to `Page.bringToFront` before each navigation made both of them answer within seconds.

## Two rows that would not read

Our detection suite drives a profile through a catalogue of public detectors and reads each one's verdict off the page. Most rows read cleanly. Two did not: `deviceandbrowserinfo` and `fingerprint-scan` kept coming back `unread`, which in our catalogue means the page loaded and no verdict ever rendered.

The runner printed its own explanation every time: the raw detection JSON had not rendered, it arrives a few seconds after load, raise `settleMs` if this persists. It persisted. We raised the budget from 20 seconds to 45, then to 140. Nothing changed. A wait that does not help at 45 seconds is unlikely to start helping at 140, and in hindsight that was the moment to stop raising it.

## Every configuration, ruled out one at a time

The obvious suspect was the browser. So we replayed the launcher's exact command line, all 68 flags, and changed one thing per row:

```
stock Chrome for Testing 153            isBot: false in 5s
our engine, bare                        isBot: false in 5s
our engine, the launcher's 68 flags     isBot: false in 10s
each flag group dropped in turn         isBot: false
the launcher's three CDP overrides      isBot: false
a launcher-started profile, any tab     isBot: false in 5s
this runner, four consecutive runs      right URL, complete, no verdict at 45s

```

Every browser configuration answered, quickly, with a clean result. Only the runner failed, and it failed the same way four times running. So it was never the engine and never the profile. The difference lived in the harness.

Mosaic is a desktop app

Email yourself the link and open it on your computer.

[How it works: Machines that exist](https://mosaicbrowser.com/features/real-fingerprints)

## Asking the page what it had fetched

`readyState: complete` says the document and its subresources finished loading. It says nothing about whether a script on the page went on to do its job. These detectors deliver their verdict over the network, so the useful question was whether the collector had ever sent anything.

`performance.getEntriesByType('resource')` lists every request the page made. It needs no DevTools domain, so reading it cannot itself be the thing a detector notices. On a passing run and a failing one, the lists looked like this:

```
passing   …/fingerprint_bot_test, 4x api/network-probe, api/ingest-fingerprint
failing   none of them

```

The failing page had loaded and then done nothing. Its collector had never finished, so it never posted its fingerprint, so there was never a verdict to render. Raising the timeout had been waiting for a request that was never going to be made.

## The tab was hidden

The cause was one line of Chromium behaviour and one line of our own code. Our runner creates the tab it tests in, then creates a blank scratch tab after it. The newer tab becomes the active one. So every page in the catalogue had been loaded into a background tab, with animation frames paused and timers throttled.

Most detectors did not care. Their collectors finish without waiting on a frame, or finish slowly enough to get there anyway. The two that broke were the two that depend on the slowest collector: `deviceandbrowserinfo` and `fingerprint-scan` both load `fingerprint-scan.com/v1/fp.js`, and that script is the slowest collector in the catalogue. In a throttled tab it never finished.

We added `Page.bringToFront` before each navigation:

```
fingerprint-scan       never ready in 140s   ->  ready in 8.2s, pass
deviceandbrowserinfo   never ready in 45s    ->  ready in 4.4s, pass

```

We chose the call over reordering the two tab creations. Reordering would have worked, and it would have been undone by the next change that opened a tab later. `Page.bringToFront` says what the test needs at the line that needs it.

## How requestAnimationFrame background tab throttling catches automation

If you drive Chrome yourself, through Puppeteer, Playwright or raw CDP, the same trap is easy to walk into. Opening a second page, a popup, or a devtools target can push the page you care about into the background without anything telling you.

Three checks catch it:

1. Read `document.visibilityState` in the page you are testing. `hidden` means animation frames are not running.
2. Watch for a `visibilitychange` event during the run. A page that goes hidden half-way through a test has measured two different browsers.
3. Before navigating a tab you need to be live, send `Page.bringToFront` to it.

None of this is specific to fingerprinting. Any page that animates, polls, or spreads work across frames behaves differently in a background tab, and a test that reads such a page from the background is measuring Chromium's power saving, not the site.

## What we kept from it

A timeout that does not help is a question about the page, not the clock. When raising the budget changes nothing, the thing you are waiting for is probably not coming.

Ask what the page fetched. The resource list separated "slow" from "never started" in one call, after four rounds of configuration bisection had separated nothing. It is part of the suite's diagnostic dump now.

And a harness that measures a browser is part of the browser it measures. The runner was not measuring a profile; it was measuring a tab Chromium had deliberately slowed down, and reporting that as the site failing to answer. Stock Chrome for Testing answering in five seconds was the same kind of control as the one in [the control is Google's own build](https://mosaicbrowser.com/blog/the-control-is-googles-build), and this time it pointed at the harness rather than the engine. The full catalogue the suite reads is in [our list of 49 detectors](https://mosaicbrowser.com/blog/forty-nine-detectors), and how we run it unattended is in [automating fingerprint tests](https://mosaicbrowser.com/blog/automating-fingerprint-tests). Another detector that looked broken and was not, for a different reason, is [the fingerprint.com demo that would not load](https://mosaicbrowser.com/blog/fingerprint-demo-not-loading).

## Questions

### Does requestAnimationFrame run in a background tab?

No. Chromium stops calling requestAnimationFrame callbacks while a tab is hidden, and holds setTimeout and setInterval in that tab to roughly one run a second. Anything built on animation frames waits until the tab is visible again.

### How do I check whether my automated tab is in the background?

Read document.visibilityState in the page. A foreground tab says visible; a background one says hidden. If an automation tool opens a second tab after the one it tests, the newer tab is usually the active one.

### How do I bring a tab to the front over the DevTools protocol?

Send Page.bringToFront to that tab's session before you navigate it. It makes the tab the active one in its window, so animation frames and timers run at their normal rate.

### Why would a fingerprint script depend on requestAnimationFrame?

Collectors use it to wait for a rendered frame, to time rendering, and to spread work out so the page stays responsive. Paused frames mean the collector never finishes, so it never sends its result.

## Related

[49 browser fingerprint detectors, and how we read them](https://mosaicbrowser.com/blog/forty-nine-detectors): Our browser fingerprint detectors list: 49 public test pages in five groups, 40 read by a script and 9 kept as reference because they print no verdict.

[Running 49 fingerprint tests unattended, and what that takes](https://mosaicbrowser.com/blog/automating-fingerprint-tests): To automate browser fingerprint testing, tell a failed profile from a dead site, an unreadable page and your own bug. Our runner names each across 49 rows.

[fingerprint.com wouldn't load through our proxy, and that was a result](https://mosaicbrowser.com/blog/fingerprint-demo-not-loading): Through one residential exit the FingerprintJS demo hung; from the same Mac with no proxy it loaded. Our runner blamed itself. The exit was why.

[The control that settles "is it our patches?"](https://mosaicbrowser.com/blog/the-control-is-googles-build): Unpatched Chrome for Testing got the same IPHey verdict as our engine, so our patches were not the cause. Its voice list still differs from Chrome's.

[Why navigator.webdriver is true, and why false proves little](https://mosaicbrowser.com/blog/why-navigator-webdriver-true): navigator.webdriver is true because automation set it on purpose. We measured a debug-attached Chrome reading true and still scoring 100 on IPHey.



[Headless is not a way to hide, it is a way to be seen](https://mosaicbrowser.com/blog/headless-is-a-signal): Headless Chrome names itself HeadlessChrome in its user agent and drops the real GPU. Why our background work runs headed, off-screen, instead.

[setUserAgentOverride without metadata wipes your client hints](https://mosaicbrowser.com/blog/setuseragentoverride-clears-hints): Without userAgentMetadata, setUserAgentOverride left our page with empty brands while a worker reported both. We set the User-Agent at launch instead.

## Mosaic, on your own computer

The browser this blog is about: its own Chromium, fingerprints from real machines and a proxy exit per profile. Free holds 3 profiles. Pro is $49 a month for any number.

Mosaic is a desktop app

Email yourself the link and open it on your computer.
