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.
Summary
#15829 documents scripts detecting Lighthouse via the user agent,
navigator.platform, and themoto g powerstring. 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 aswindow.__nativePromise,window.__nativeURL,window.__nativePerformance,window.__nativeFetch, etc., viaPage.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: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 appendsChrome-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__nativePerformancecheck appears between the April and May 2023 snapshots. Snapshot of the current file: https://web.archive.org/web/20260815204046/https://client.crisp.chat/l.jsI 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
windowinto 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.