# Safari on macOS

> Safari offers some signals no other browser has and lacks the Chromium-only ones; stock Safari has no exclusions, and with Advanced Fingerprinting Protection five components are left out.

Source: https://whiskerjs.com/browsers/safari-macos/ (last updated 2026-10-26)

> **In plain words:**
>
> Safari on the Mac uses Apple’s WebKit engine. It lacks a few measurements that only Chromium-based browsers offer, such as the device memory and Client Hints, so those come back as “unavailable”. In return it has a few of its own, such as Apple Pay support and Private Click Measurement, and WhiskerJS reads the audio output latency here. Nothing is left out in a normal window. When Safari’s Advanced Fingerprinting Protection adds noise to drawings and sound (always in private browsing), WhiskerJS notices with tiny test reads and leaves those signals out.

## At a glance

- Browser class: `safari` (the vendor is `Apple Computer, Inc.`); `safari_afp` when Advanced Fingerprinting Protection alters canvas or WebGL readback.
- Excluded components: none in stock Safari; 5 under Advanced Fingerprinting Protection (`audio`, `canvas`, `screen_frame`, `screen_resolution`, `webgl_image`, ruleId `safari_afp`).
- 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 only here and on iOS (and, for latency, Android): `apple_pay` with a version, `private_click`, `audio_latency`.
- Lab sample: Mac Safari kept one id across 2 runs at signalsVersion 1.

## What works

- **Safari-specific components**: [`apple_pay`](https://whiskerjs.com/signals/apple-pay/) reports `version=<n>;can_pay=<1 or 0>`, found by asking `supportsVersion()` from 20 down; `can_pay` is `blocked` where `canMakePayments()` throws (insecure or cross-origin contexts). [`private_click`](https://whiskerjs.com/signals/private-click/) reads `attributionSourceId` from a new, unattached `<a>` element. [`audio_latency`](https://whiskerjs.com/signals/audio-latency/) reads `AudioContext.baseLatency` and closes the context at once.
- **Graphics**: canvas and WebGL are read as elsewhere. The [`webgl_renderer`](https://whiskerjs.com/signals/webgl-renderer/) string is often generic (for example `Apple GPU`), which tells less than on other browsers.
- **[`hardware_concurrency`](https://whiskerjs.com/signals/hardware-concurrency/)**: read 16 times; a 4 or an 8 is reported as `4_8`, because WebKit alternates between 4 and 8 for scripts on its known-fingerprinting list (Safari 26, Safari Technology Preview 254), also between page loads. Other values are exact.
- Fonts, font preferences, offline audio, Math, codecs, plugins, locale, storage presence and the display preference queries.

## What is unavailable or blocked

These report `unavailable` with ruleId `no_api` because Safari does not offer the API:

| Component | Missing API |
| --- | --- |
| [`device_memory`](https://whiskerjs.com/signals/device-memory/) | `navigator.deviceMemory` |
| [`heap_limit`](https://whiskerjs.com/signals/heap-limit/) | `performance.memory` |
| [`ua_client_hints`](https://whiskerjs.com/signals/ua-client-hints/) | `navigator.userAgentData` |
| [`ua_versions`](https://whiskerjs.com/signals/ua-versions/) | `navigator.userAgentData` |
| [`keyboard_layout`](https://whiskerjs.com/signals/keyboard-layout/) | `navigator.keyboard` |

Because a non-ok status counts the same in every run, these do not make the id unstable: they are simply “missing” every time. [`webgpu`](https://whiskerjs.com/signals/webgpu/) depends on the Safari and macOS version; it is `unavailable` where `navigator.gpu` is missing.

## What WhiskerJS excludes and why

Nothing in stock Safari. The `safari` row of the exclusion table is empty.

Safari’s Advanced Fingerprinting Protection (AFP) is on in private browsing, and in all browsing where the Safari setting (or a newer Safari’s default) says so. It adds noise to canvas, WebGL and Web Audio readback and reports the window size as the screen. WhiskerJS does not read the setting: it classes a Safari as `safari_afp` when the canvas or WebGL [known-answer probe](https://whiskerjs.com/signals/noise-profile/) came back altered. Then:

| Component | Why it is left out |
| --- | --- |
| [`audio`](https://whiskerjs.com/signals/audio/) | Web Audio readback gets noise |
| [`canvas`](https://whiskerjs.com/signals/canvas/) | canvas readback gets noise |
| [`screen_frame`](https://whiskerjs.com/signals/screen-frame/) | the window size is reported as the screen |
| [`screen_resolution`](https://whiskerjs.com/signals/screen-resolution/) | the window size is reported as the screen |
| [`webgl_image`](https://whiskerjs.com/signals/webgl-image/) | WebGL readback gets noise |

All five report `excluded` with ruleId `safari_afp`. Stock Safari reads the known answers back exactly and stays `safari`. Wherever noise is not covered by a row, [`canvas`](https://whiskerjs.com/signals/canvas/), [`webgl_image`](https://whiskerjs.com/signals/webgl-image/) and [`audio`](https://whiskerjs.com/signals/audio/) still catch it: two renders that differ give `noisy` / `render_mismatch`, and a failed known-answer probe gives `noisy` / `seeded_noise`. [`canvas_readback`](https://whiskerjs.com/signals/canvas-readback/) then reports `altered`.

## Integrity flags to expect

- [`iframe_mismatch`](https://whiskerjs.com/docs/integrity/#iframe_mismatch) used to fire in stock Safari and in Playwright’s WebKit build, because WebKit gives a hidden (`display: none`) iframe a 0 by 0 screen. Since signalsVersion 9 the screen values are compared only when the iframe reports a screen wider than 0, so this no longer happens.
- [`engine_mismatch`](https://whiskerjs.com/docs/integrity/#engine_mismatch) checks that a `safari` or `safari_afp` class behaves like WebKit: the printed source of `eval` must be 37 characters and `navigator.productSub` `20030107`. A user-agent switcher can raise it.
- [`native_tampering`](https://whiskerjs.com/docs/integrity/#native_tampering) and [`prototype_lie`](https://whiskerjs.com/docs/integrity/#prototype_lie) with extensions that change browser functions.
- [`vm_renderer`](https://whiskerjs.com/docs/integrity/#vm_renderer) when macOS runs as a virtual machine guest (`Apple Paravirtual device`). [`ua_ch_mismatch`](https://whiskerjs.com/docs/integrity/#ua_ch_mismatch) is never checked: Safari has no `navigator.userAgentData`. [`media_query_mismatch`](https://whiskerjs.com/docs/integrity/#media_query_mismatch) does not run for the `safari_afp` class.
- Advanced Fingerprinting Protection’s noise is never a flag: it shows in `noise_profile` and the excluded components.

## Early lab observations

An early, small sample at signalsVersion 1, not a benchmark: on a Mac, Safari kept **one id across 2 runs**, different from the ids of the other three pairs. `device_memory` was `unavailable`, by design. The sample used the first collector (30 components), not the current 47.

## Testing it yourself

Run the [live demo](https://whiskerjs.com/demo/) in a normal Safari window and then in a private window, and compare `browser_class`, `noise_profile` and the canvas, WebGL and audio statuses. In the private window the class should say `safari_afp` and five components `excluded`, which is why the visitorId differs between the two modes. The [lab mode](https://whiskerjs.com/demo/#lab) copies one line per run. Nothing is sent anywhere.

## Support table for Safari on macOS

Expected status of every component in this browser at signalsVersion 9, from [the support matrix](https://whiskerjs.com/browsers/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`](https://whiskerjs.com/signals/apple-pay/) | ok | code | version=<n>;can_pay=<1, 0 or blocked>; can_pay is blocked in insecure or cross-origin contexts. |
| [`audio`](https://whiskerjs.com/signals/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`](https://whiskerjs.com/signals/audio-latency/) | ok | code | Read for browser class safari; the context is closed at once. |
| [`browser_class`](https://whiskerjs.com/signals/browser-class/) | ok | code | Class safari; safari_afp when Advanced Fingerprinting Protection alters canvas or WebGL readback. |
| [`canvas`](https://whiskerjs.com/signals/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`](https://whiskerjs.com/signals/canvas-readback/) | varies | code | exact, or altered with Advanced Fingerprinting Protection (always in private browsing). |
| [`color_depth`](https://whiskerjs.com/signals/color-depth/) | ok | code |   |
| [`color_gamut`](https://whiskerjs.com/signals/color-gamut/) | ok | code |   |
| [`content_blockers`](https://whiskerjs.com/signals/content-blockers/) | ok | code | none, or the bait names a content blocker hid. |
| [`cpu_architecture`](https://whiskerjs.com/signals/cpu-architecture/) | ok | code |   |
| [`device_memory`](https://whiskerjs.com/signals/device-memory/) | unavailable | lab | Seen unavailable on Mac Safari in the lab sample; Safari does not offer navigator.deviceMemory. |
| [`dynamic_range`](https://whiskerjs.com/signals/dynamic-range/) | ok | code |   |
| [`font_preferences`](https://whiskerjs.com/signals/font-preferences/) | ok | code |   |
| [`fonts`](https://whiskerjs.com/signals/fonts/) | ok | code |   |
| [`fonts_canvas`](https://whiskerjs.com/signals/fonts-canvas/) | ok | code |   |
| [`forced_colors`](https://whiskerjs.com/signals/forced-colors/) | ok | code |   |
| [`hardware_concurrency`](https://whiskerjs.com/signals/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`](https://whiskerjs.com/signals/heap-limit/) | unavailable | code | performance.memory is a Chromium API. Reports unavailable / no_api. |
| [`intl_locale`](https://whiskerjs.com/signals/intl-locale/) | ok | code |   |
| [`keyboard_layout`](https://whiskerjs.com/signals/keyboard-layout/) | unavailable | code | No navigator.keyboard here: unavailable / no_api. |
| [`languages`](https://whiskerjs.com/signals/languages/) | ok | code |   |
| [`math`](https://whiskerjs.com/signals/math/) | ok | code |   |
| [`media_codecs`](https://whiskerjs.com/signals/media-codecs/) | ok | code |   |
| [`noise_profile`](https://whiskerjs.com/signals/noise-profile/) | ok | code | exact in a normal window; seeded for the APIs Advanced Fingerprinting Protection randomises. |
| [`pdf_viewer`](https://whiskerjs.com/signals/pdf-viewer/) | ok | code |   |
| [`permissions`](https://whiskerjs.com/signals/permissions/) | ok | code |   |
| [`pixel_ratio`](https://whiskerjs.com/signals/pixel-ratio/) | ok | code |   |
| [`platform`](https://whiskerjs.com/signals/platform/) | ok | code |   |
| [`plugins`](https://whiskerjs.com/signals/plugins/) | ok | code |   |
| [`prefers_contrast`](https://whiskerjs.com/signals/prefers-contrast/) | ok | code |   |
| [`private_click`](https://whiskerjs.com/signals/private-click/) | ok | code | Safari Private Click Measurement exposes attributionSourceId. |
| [`reduced_motion`](https://whiskerjs.com/signals/reduced-motion/) | ok | code |   |
| [`screen_frame`](https://whiskerjs.com/signals/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`](https://whiskerjs.com/signals/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`](https://whiskerjs.com/signals/speech-voices/) | ok | code | Digest of the voice list, or none; some browsers fill the list late. |
| [`storage`](https://whiskerjs.com/signals/storage/) | ok | code |   |
| [`time_zone`](https://whiskerjs.com/signals/time-zone/) | ok | code |   |
| [`touch_support`](https://whiskerjs.com/signals/touch-support/) | ok | code |   |
| [`ua_client_hints`](https://whiskerjs.com/signals/ua-client-hints/) | unavailable | code | navigator.userAgentData (Client Hints) is a Chromium API. Reports unavailable / no_api. |
| [`ua_versions`](https://whiskerjs.com/signals/ua-versions/) | unavailable | code | navigator.userAgentData (Client Hints) is a Chromium API. Reports unavailable / no_api. |
| [`vendor`](https://whiskerjs.com/signals/vendor/) | ok | code |   |
| [`vendor_flavors`](https://whiskerjs.com/signals/vendor-flavors/) | ok | code |   |
| [`webgl_image`](https://whiskerjs.com/signals/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`](https://whiskerjs.com/signals/webgl-params/) | ok | code | Where WebGL 1 is available; unavailable / no_context without it. |
| [`webgl_renderer`](https://whiskerjs.com/signals/webgl-renderer/) | ok | code | Often a generic string such as Apple GPU. |
| [`webgl_vendor`](https://whiskerjs.com/signals/webgl-vendor/) | ok | code |   |
| [`webgpu`](https://whiskerjs.com/signals/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 is device_memory unavailable in Safari?

Safari does not offer navigator.deviceMemory; only Chromium-based browsers do. WhiskerJS reports it as unavailable with ruleId no_api, by design. The early lab sample showed exactly this on Mac Safari.

### Does Safari's private browsing change the id?

Yes, once. Private browsing turns on Advanced Fingerprinting Protection, which adds noise to canvas, WebGL and Web Audio readback and reports the window size as the screen. WhiskerJS's known-answer probes notice the noise, class the browser safari_afp and leave audio, canvas, screen_frame, screen_resolution and webgl_image out. The id then differs from the normal window's id, but stays the same from one private session to the next.

### Why does hardware_concurrency say 4_8?

WebKit gives scripts on its known-fingerprinting list a core count that alternates between 4 and 8, also between page loads. WhiskerJS reports a WebKit 4 or 8 as the one bucket 4_8 so the id does not flap. Other values are reported exactly.

### Does WhiskerJS show an Apple Pay sheet?

No. It only checks whether ApplePaySession exists, the highest supported version and canMakePayments(). No payment sheet is shown and no request is made.

## Related pages

- [Safari and every iOS browser](https://whiskerjs.com/browsers/safari-ios/): The same engine on iPhone and iPad.
- [apple_pay](https://whiskerjs.com/signals/apple-pay/): Apple Pay presence and version.
- [private_click](https://whiskerjs.com/signals/private-click/): Private Click Measurement.
- [noise_profile](https://whiskerjs.com/signals/noise-profile/): The known-answer probes that recognise Advanced Fingerprinting Protection.
- [Canvas noise](https://whiskerjs.com/glossary/#canvas-noise): Why browsers alter readback.
- [Browser support matrix](https://whiskerjs.com/browsers/support-matrix/): All browsers side by side.
