GoogleChrome / GoogleChrome/lighthouse
Third-party scripts detect Lighthouse via its injected globals (__nativePromise / __nativePerformance) to evade measurement
- Dominant language
- JavaScript
- Stars
- 30.8k
- Forks
- 9.8k
- Avg merge
- 1d 14h
- Merged PRs (30d)
- 20
Description
**Summary**
#15829 documents scripts detecting Lighthouse via the user agent, `navigator.platform`, and the `moto g power` string. While benchmarking chat widgets I found a technique that isn't on that list and appears undocumented: detecting Lighthouse through the globals it injects into every audited page.
`ExecutionContext.cacheNativesOnNewDocument()` (core/gather/driver/execution-context.js) stashes native references on the page as `window.__nativePromise`, `window.__nativeURL`, `window.__nativePerformance`, `window.__nativeFetch`, etc., via `Page.addScriptToEvaluateOnNewDocument`. Those names are stable and only exist on pages Lighthouse is auditing — which makes them a perfect fingerprint that survives UA spoofing.
**In the wild**
Crisp's chat widget loader (`https://client.crisp.chat/l.js`) gates initialization on exactly this check. Reproduce:
```
curl -s https://client.crisp.chat/l.js | grep -o 'window.__nativePerformance&&window.__nativePromise&&(t=!1)'
window.__nativePerformance&&window.__nativePromise&&(t=!1)
```
When both globals exist, the loader sets its "don't load" flag and the widget (~264KB beyond the loader) never initializes — so any Lighthouse run measures a page without the widget, regardless of user agent. The same loader also carries `"Lighthouse"`, `"GTmetrix"`, and `"HeadlessChrome"` in a UA blocklist, which still catches PSI (it appends `Chrome-Lighthouse`, per #15829).
Timeline via archive.org snapshots of the same file: the `"Lighthouse"` UA entry appears between the Oct and Nov 2020 snapshots; after #14384 removed the UA marker (Sept 2022), the `__nativePerformance` check appears between the April and May 2023 snapshots. Snapshot of the current file: https://web.archive.org/web/20260815204046/https://client.crisp.chat/l.js
I verified empirically: headful Chrome, clean emulated UA, Lighthouse 12.8.2 → widget transfers ~3KB (loader only). Same page in a non-Lighthouse browser → ~267KB and a rendered widget.
**Suggested mitigation**
Randomize or namespace the cached-native global names per run (e.g. a random suffix generated at session start), or move them off `window` into the isolated world Lighthouse already uses for most evaluation. The stable, documented-in-source names are what make this fingerprint reliable.
**Disclosure**
I build a competing chat widget, which is how I ran into this — happy to share the full benchmark methodology if useful.
Contributor guide
Research direction
Start in core/gather/driver/execution-context.js at ExecutionContext.cacheNativesOnNewDocument() and trace how the cached-native globals are created through Page.addScriptToEvaluateOnNewDocument. Reproduce the behavior with the Crisp loader and clean user-agent setup described in the issue. Done means the cached references remain usable while stable Lighthouse-specific globals no longer provide this fingerprint.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- performance
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100