← All articles

Why I Stopped Letting Websites Download Gemini Nano Into Every Browser Profile

6 min read browser-engineproxiesprofilesfingerprinting

A synced browser profile's backup went from 56 MB to 2.74 GB compressed overnight. Nobody had logged into anything new. Unpacked, the cookies, the local storage, the IndexedDB of the one shop the account actually used came to 93 MB. The other 3.3 GB was Google's.

Directory in the profile Size What it is
OptGuideOnDeviceModel/.../weights.bin 2863 MB Gemini Nano
SODA + SODALanguagePacks 244 MB On-device speech recognition
screen_ai 122 MB OCR and accessibility
TranslateKit 82 MB Offline translation
WasmTtsEngine, OnDeviceHeadSuggestModel, ... ~35 MB Smaller components
Default (the actual logged-in state) 93 MB The profile

That profile was being backed up by a tool that packs everything in the user-data directory except a blacklist, and the blacklist had never heard of these folders. So every time the browser closed, it uploaded 2.7 GB. Opening it on a second machine meant a 30-minute download before the window appeared.

Our own sync packs a whitelist, so these folders never left the machine. I checked anyway, and the downloads were happening on our side too. Across 2,068 profiles on my own laptop there were 82 copies of the speech models (about 3.5 GB) and 83 copies of the translation model. Each one downloaded separately, by a different profile, through that profile's proxy.

That last part is the one that matters.

It is not Chrome being greedy. It is the page.

My first assumption was that Chrome was fetching these in the background. The small ones are: WasmTtsEngine and OnDeviceHeadSuggestModel show up in almost every profile, and they are under a megabyte.

The big ones are not background downloads. A web page asks for them.

await Translator.create({ sourceLanguage: 'en', targetLanguage: 'de' })
await LanguageModel.create()

I ran those two lines on example.com after a single click. A minute later chrome://components had two new entries: Chrome TranslateKit de-en and nano_v3_cpu_component. The folders appeared in the profile and the download started.

On a normal Chrome this is a feature. You have one profile, you use a site that wants on-device AI, you pay the download once, it is shared.

In a browser that runs hundreds of isolated profiles, each with its own residential proxy billed by the gigabyte, it is something else. Any page you open can decide to spend 2.8 GB of your proxy traffic, once per profile, and write it to your disk once per profile. You never asked for it, and you do not see it happen.

The two obvious switches, and why neither works

There are two command-line switches that look like they should fix this. I measured both against the unmodified browser. I checked what a page sees through availability() for each API, then triggered create() and looked at what got installed.

No switch Disable the on-device model features --disable-component-update
LanguageModel.availability() downloadable unavailable downloadable
Summarizer.availability() downloadable unavailable downloadable
Translator, LanguageDetector, SpeechRecognition downloadable downloadable downloadable
Page-triggered download still happens yes not measured yes
Entries in chrome://components (before any create()) 21 21 2

Turning off the features changes what the page sees. LanguageModel and Summarizer flip from downloadable to unavailable. Every profile we launch would report on-device AI as impossible, whatever GPU it claims to have. Something that is identical across a whole fleet and different from ordinary Chrome is a signal we would be adding ourselves.

--disable-component-update is worse, and less obviously so. The APIs still say downloadable, which looks fine. But it does not stop page-triggered installs: both components were registered and downloading, exactly as before. What it does stop is everything else. The certificate revocation list, Origin Trials, the subresource filter, PKI metadata: 21 components kept up to date became 2. You lose the updates you want and keep the download you were trying to prevent.

So this cannot be a launch flag. It has to live in the engine.

What we changed

The rule we gave ourselves: the page must not be able to tell. Everything it can observe stays byte-for-byte what it was on the unmodified build. The only thing that changes is that the bytes never get fetched.

The small background components (WasmTtsEngine, OnDeviceHeadSuggestModel) are left alone. A real Chrome has them, they cost almost nothing, and removing them would be a difference with no upside.

How we checked it

Old build against new build on macOS: same persona, same launch arguments, same click, same two create() calls, three minutes of waiting afterwards:

Unmodified build New build
availability() for all five APIs, before create() downloadable downloadable
LanguageModel.availability() after create() downloading downloading
Translator.availability() after three minutes available (the download finished) downloadable
downloadprogress events from Translator.create() in 20 s 283, reaching 34% 0
Model bytes written to TranslateKit / OptGuideOnDeviceModel ~80 MB and climbing 0
Entries in chrome://components after create() (21 plus the three the page asked for) 24 24
Ordinary component updates (revocation list, subresource filter, PKI metadata...) normal normal

Every value a page can read before it asks for a model is identical. After it asks, it sees a download that has started and is not making progress, and nothing comes down the wire. The certificate revocation list and the rest kept updating the way they always have.

If you run your own browser fleet

Three things are worth checking today, whatever you run:

  1. Look at the root of your user-data directories, not just Default. du -sh */user-data/* | sort -h across your profiles will tell you in a minute whether this is already costing you.
  2. If you sync or back up profiles, use a whitelist. Chrome adds new top-level folders every few releases. A blacklist is always one release behind, and the thing it misses might be 2.8 GB.
  3. Don't reach for --disable-component-update. It looks like the fix and it isn't one. It keeps the download you care about and silently stops the security updates you don't think about.

The general lesson is the one this project keeps teaching us. A flag that makes a problem go away almost always leaves something a page can see. The fix goes one layer lower, where the page cannot tell anything happened.

Try it on the free tier.

Unlimited local profiles, no credit card. Check it against the detectors yourself.