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_afpwhen Advanced Fingerprinting Protection alters canvas or WebGL readback (for example in private browsing). - Excluded components: none in the
safariclass; 5 undersafari_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_supportwith 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:
- Not
brave: unlessnavigator.brave.isBraveis a function. - Not
firefox: Firefox on iOS usesFxiOS/in its user agent, not aFirefox/<number>token, and WebKit has nomozInnerScreenX. - Not
chromium: Chrome on iOS usesCriOS/and Edge on iOSEdgiOS/, not aChrome/<number>token, and the vendor is notGoogle Inc.. safari:navigator.vendorisApple 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 issafariorsafari_afp(a WebKit class); theAudioContextis closed at once and nothing is played.touch_support: touch points and touch events;screen_resolutionwith the larger side first, so rotation does not change it.apple_payandprivate_clickin Safari; in other iOS apps they depend on what the app’s WebKit exposes, and they report the plain valuenonewhen 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_mismatchno 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_webdriverin device test farms driven through WebDriver.native_tamperingand 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.
| Component | Status | Basis | Note |
|---|---|---|---|
apple_pay | varies | code | Safari has ApplePaySession; in other iOS browsers it depends on the app. none where it is missing. |
audio | varies | code | Excluded (safari_afp) when Advanced Fingerprinting Protection is on: always in private browsing, and in all browsing where the setting says so. Otherwise ok. |
audio_latency | ok | code | Read for browser class safari; the context is closed at once. |
browser_class | ok | code | Class safari, for every iOS browser (Apple vendor, no Chrome/ token); safari_afp when Advanced Fingerprinting Protection alters canvas or WebGL readback. |
canvas | varies | code | Excluded (safari_afp) when Advanced Fingerprinting Protection is on: always in private browsing, and in all browsing where the setting says so. Otherwise ok. |
canvas_readback | varies | code | exact, or altered with Advanced Fingerprinting Protection (always in private browsing). |
color_depth | ok | code | |
color_gamut | ok | code | |
content_blockers | ok | code | none, or the bait names a content blocker hid. |
cpu_architecture | ok | code | |
device_memory | unavailable | lab | Seen unavailable on iPhone Safari and iPhone Chrome in the lab sample; no iOS browser offers it. |
dynamic_range | ok | code | |
font_preferences | ok | code | |
fonts | ok | code | |
fonts_canvas | ok | code | |
forced_colors | ok | code | |
hardware_concurrency | ok | code | Read 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_limit | unavailable | code | performance.memory is a Chromium API. Reports unavailable / no_api. |
intl_locale | ok | code | |
keyboard_layout | unavailable | code | No navigator.keyboard here: unavailable / no_api. |
languages | ok | code | |
math | ok | code | |
media_codecs | ok | code | |
noise_profile | ok | code | exact in a normal window; seeded for the APIs Advanced Fingerprinting Protection randomises. |
pdf_viewer | varies | code | unavailable where navigator.pdfViewerEnabled does not exist. |
permissions | ok | code | |
pixel_ratio | ok | code | |
platform | ok | code | |
plugins | varies | code | Mobile builds often list no plugins and then report the plain value none. |
prefers_contrast | ok | code | |
private_click | varies | code | Depends on whether the app's WebKit exposes Private Click Measurement; none otherwise. |
reduced_motion | ok | code | |
screen_frame | varies | code | Excluded (safari_afp) when Advanced Fingerprinting Protection is on: always in private browsing, and in all browsing where the setting says so. Otherwise ok. |
screen_resolution | varies | code | Excluded (safari_afp) when Advanced Fingerprinting Protection is on: always in private browsing, and in all browsing where the setting says so. Otherwise ok. |
speech_voices | ok | code | Digest of the voice list, or none; some browsers fill the list late. |
storage | ok | code | |
time_zone | ok | code | |
touch_support | ok | code | |
ua_client_hints | unavailable | code | navigator.userAgentData (Client Hints) is a Chromium API. Reports unavailable / no_api. |
ua_versions | unavailable | code | navigator.userAgentData (Client Hints) is a Chromium API. Reports unavailable / no_api. |
vendor | ok | code | |
vendor_flavors | ok | code | |
webgl_image | varies | code | Excluded (safari_afp) when Advanced Fingerprinting Protection is on: always in private browsing, and in all browsing where the setting says so. Otherwise ok. |
webgl_params | ok | code | Where WebGL 1 is available; unavailable / no_context without it. |
webgl_renderer | ok | code | Often a generic string such as Apple GPU. |
webgl_vendor | ok | code | |
webgpu | varies | code | Depends 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.