Browsers · WhiskerJS 0.1.0

Safari and every iOS browser

On iPhone and iPad every browser runs Apple's WebKit, so WhiskerJS treats them all as the safari class and tells them apart with a few app-specific globals.

In plain words

On an iPhone or iPad, every browser (Safari, Chrome, Firefox, Edge and others) is built on Apple’s WebKit engine. To WhiskerJS they all look like Safari, so they share Safari’s strengths and gaps: no device memory or Client Hints, but Apple-specific signals such as Apple Pay. The apps differ in small ways, for example in a few hidden variables they add to the page, and WhiskerJS reports those so your server can tell the apps apart.

At a glance

  • Browser class: safari, for Safari and for Chrome, Firefox and Edge on iOS; safari_afp when Advanced Fingerprinting Protection alters canvas or WebGL readback (for example in private browsing).
  • Excluded components: none in the safari class; 5 under safari_afp (audio, canvas, screen_frame, screen_resolution, webgl_image).
  • CPU threads: a 4 or an 8 is reported as the bucket 4_8.
  • Unavailable by design: device_memory, heap_limit, ua_client_hints, ua_versions, keyboard_layout.
  • Read here: audio_latency (WebKit classes), touch_support with touch points.
  • Telling apps apart: vendor_flavors.
  • Lab sample: iPhone Chrome (3 runs) and iPhone Safari (3 runs) each kept one id at signalsVersion 1.

Why every iOS browser is classed safari

The class test in src/browser-class.ts runs in a fixed order, and on iOS each step falls through to safari:

  1. Not brave: unless navigator.brave.isBrave is a function.
  2. Not firefox: Firefox on iOS uses FxiOS/ in its user agent, not a Firefox/<number> token, and WebKit has no mozInnerScreenX.
  3. Not chromium: Chrome on iOS uses CriOS/ and Edge on iOS EdgiOS/, not a Chrome/<number> token, and the vendor is not Google Inc..
  4. safari: navigator.vendor is Apple Computer, Inc. in every WebKit browser.

This is the right result for the exclusion table and for the engine check: the code really runs on WebKit. The integrity check engine_mismatch expects WebKit behaviour (a 37-character eval source and productSub 20030107), which these browsers have. And headless_markers does not apply its “desktop Chrome without window.chrome” test to user agents with CriOS, Mobile or Android.

Telling iOS browsers apart

vendor_flavors reports which of ten browser-specific globals exist on the page. Three of them are linked to iOS apps: __crWeb and __gCrWeb (associated with Chrome on iOS) and __firefox__ (associated with Firefox on iOS). Whether an app defines them depends on the app and its version, so treat the value as a hint that differs between apps, not as a guarantee. WhiskerJS checks no Edge-specific global. The component is part of the browser group of the visitorId, which is one reason Safari and Chrome on the same iPhone can have different ids.

What works

  • audio_latency: read because the class is safari or safari_afp (a WebKit class); the AudioContext is closed at once and nothing is played.
  • touch_support: touch points and touch events; screen_resolution with the larger side first, so rotation does not change it.
  • apple_pay and private_click in Safari; in other iOS apps they depend on what the app’s WebKit exposes, and they report the plain value none when the API is missing.
  • Canvas, WebGL, offline audio, fonts, font preferences, Math, codecs, locale and display preferences.

What is unavailable or blocked

Component Status Why
device_memory unavailable / no_api WebKit has no navigator.deviceMemory
heap_limit unavailable / no_api no performance.memory
ua_client_hints, ua_versions unavailable / no_api no navigator.userAgentData, even in Chrome on iOS
keyboard_layout unavailable / no_api no navigator.keyboard

plugins often lists nothing on mobile and then reports none; pdf_viewer is unavailable where navigator.pdfViewerEnabled does not exist. webgpu depends on the iOS version.

What WhiskerJS excludes and why

Nothing in the safari class; that row of the exclusion table is empty. As on the Mac, Advanced Fingerprinting Protection (on in private browsing, and wherever the setting says so) adds noise to canvas, WebGL and Web Audio readback and reports the window size as the screen. When the canvas or WebGL known-answer probe sees that noise, the class is safari_afp and audio, canvas, screen_frame, screen_resolution and webgl_image are excluded with ruleId safari_afp. Noise that no row covers still turns canvas, webgl_image or audio noisy (render_mismatch from the double render, or seeded_noise from the known-answer probes). See Safari on macOS.

hardware_concurrency is read 16 times and a 4 or an 8 is reported as 4_8: WebKit alternates between 4 and 8 for scripts on its known-fingerprinting list, also between page loads, and the bucket keeps the id steady.

Integrity flags to expect

  • iframe_mismatch no longer fires for WebKit’s 0 by 0 screen in a hidden iframe: since signalsVersion 9 screen values are compared only when the iframe reports a screen wider than 0.
  • automation_webdriver in device test farms driven through WebDriver.
  • native_tampering and related lie checks when privacy extensions or other scripts on the page override functions.

Early lab observations

An early, small sample at signalsVersion 1, not a benchmark:

Device and browser Runs Ids seen
iPhone, Chrome 3 1
iPhone, Safari 3 1

Each kept one id across its runs, and the two ids differed from each other and from the two Mac ids in the sample. device_memory was unavailable on both, including Chrome on iPhone, because Chrome there runs on WebKit. The sample used 30 components, not the current 46.

Testing it yourself

Open the live demo in two iOS browsers on the same device and compare the structuredId groups: the browser group is where they are most likely to differ. The lab mode copies one line per run. Nothing is sent anywhere.

Support table for Safari and every iOS browser

Expected status of every component in this browser at signalsVersion 9, from the support matrix (updated 2026-10-26). Basis "code" means expected from the library code and documentation, not yet measured; "lab" means seen in the early lab sample.

ComponentStatusBasisNote
apple_payvariescodeSafari has ApplePaySession; in other iOS browsers it depends on the app. none where it is missing.
audiovariescodeExcluded (safari_afp) when Advanced Fingerprinting Protection is on: always in private browsing, and in all browsing where the setting says so. Otherwise ok.
audio_latencyokcodeRead for browser class safari; the context is closed at once.
browser_classokcodeClass safari, for every iOS browser (Apple vendor, no Chrome/ token); safari_afp when Advanced Fingerprinting Protection alters canvas or WebGL readback.
canvasvariescodeExcluded (safari_afp) when Advanced Fingerprinting Protection is on: always in private browsing, and in all browsing where the setting says so. Otherwise ok.
canvas_readbackvariescodeexact, or altered with Advanced Fingerprinting Protection (always in private browsing).
color_depthokcode
color_gamutokcode
content_blockersokcodenone, or the bait names a content blocker hid.
cpu_architectureokcode
device_memoryunavailablelabSeen unavailable on iPhone Safari and iPhone Chrome in the lab sample; no iOS browser offers it.
dynamic_rangeokcode
font_preferencesokcode
fontsokcode
fonts_canvasokcode
forced_colorsokcode
hardware_concurrencyokcodeRead 16 times; a 4 or an 8 is reported as the bucket 4_8, because WebKit alternates between them for scripts on its known-fingerprinting list.
heap_limitunavailablecodeperformance.memory is a Chromium API. Reports unavailable / no_api.
intl_localeokcode
keyboard_layoutunavailablecodeNo navigator.keyboard here: unavailable / no_api.
languagesokcode
mathokcode
media_codecsokcode
noise_profileokcodeexact in a normal window; seeded for the APIs Advanced Fingerprinting Protection randomises.
pdf_viewervariescodeunavailable where navigator.pdfViewerEnabled does not exist.
permissionsokcode
pixel_ratiookcode
platformokcode
pluginsvariescodeMobile builds often list no plugins and then report the plain value none.
prefers_contrastokcode
private_clickvariescodeDepends on whether the app's WebKit exposes Private Click Measurement; none otherwise.
reduced_motionokcode
screen_framevariescodeExcluded (safari_afp) when Advanced Fingerprinting Protection is on: always in private browsing, and in all browsing where the setting says so. Otherwise ok.
screen_resolutionvariescodeExcluded (safari_afp) when Advanced Fingerprinting Protection is on: always in private browsing, and in all browsing where the setting says so. Otherwise ok.
speech_voicesokcodeDigest of the voice list, or none; some browsers fill the list late.
storageokcode
time_zoneokcode
touch_supportokcode
ua_client_hintsunavailablecodenavigator.userAgentData (Client Hints) is a Chromium API. Reports unavailable / no_api.
ua_versionsunavailablecodenavigator.userAgentData (Client Hints) is a Chromium API. Reports unavailable / no_api.
vendorokcode
vendor_flavorsokcode
webgl_imagevariescodeExcluded (safari_afp) when Advanced Fingerprinting Protection is on: always in private browsing, and in all browsing where the setting says so. Otherwise ok.
webgl_paramsokcodeWhere WebGL 1 is available; unavailable / no_context without it.
webgl_rendererokcodeOften a generic string such as Apple GPU.
webgl_vendorokcode
webgpuvariescodeDepends on browser version, operating system and GPU; needs a secure context. unavailable / no_api without navigator.gpu, no_context without an adapter.

Questions and answers

Why does Chrome on my iPhone show browser_class safari?

On iOS every browser uses WebKit. Chrome on iOS reports the Apple vendor string and its user agent carries CriOS instead of a Chrome/ token, so the class test matches safari. That is expected, not a bug.

Do Safari and Chrome on the same iPhone get the same id?

Not necessarily. In the early lab sample, iPhone Chrome and iPhone Safari each kept their own id, and the ids differed. vendor_flavors is one component that can differ between iOS browsers.

Why is device_memory unavailable on every iPhone browser?

No iOS browser offers navigator.deviceMemory, because WebKit does not. The lab sample showed it unavailable on iPhone Safari and iPhone Chrome.