net-watch
Concepts

Browser support

The support floor, which platform APIs net-watch uses, and the browser quirks it handles for you.

net-watch runs in Chrome and Edge 90, Firefox 90, and Safari and iOS 15, and anything newer. Every platform API in core is checked against that floor. An API that a floor browser lacks is either not used, or used only as a hint with a fallback.

The tests run in real browsers, not a simulated DOM: the suite runs in Vitest browser mode on Playwright's Chromium, Firefox and WebKit.

What works where

APIChrome / EdgeFirefoxSafari / iOSRole in net-watch
navigator.onLine, online / offline eventsYesYesYesRequired. Layer 1
visibilitychange, pageshowYesYesYesRequired. Suspects that trigger a check. pageshow covers cache restores on iOS
fetch with HEAD, no-cors, no-storeYesYesYesRequired. The probe
AbortController, performance.now()YesYesYesRequired. Probe timeout and round-trip time
PerformanceObserver for resource entriesYesYesYesHint. Source of downlink
PerformanceResourceTiming.transferSizeYesYes16.4 and laterHint. Below 16.4 it reads 0, so no samples and downlink stays undefined
navigator.connectionYesNoNoHint. Every field falls back to undefined (saveData to false)
CSS :where()88 and later78 and later14 and laterBanner defaults with zero specificity, so your CSS wins
AbortSignal.timeout103 and later100 and later16 and laterNot used

"Required" APIs exist in every floor browser, and the core depends on them. "Hint" APIs enrich the state when present. Their absence never breaks detection, it only leaves a field undefined.

What each field needs

FieldChrome / EdgeFirefoxSafari / iOS
online, sinceYesYesYes
quality, rttYesYesYes, once a sample exists
downlinkYesYes16.4 and later
effectiveType, saveDataYesundefined, falseundefined, false
typeChrome on Android and ChromeOS onlyundefinedundefined

The state reference has the full table.

Quirks the library handles

You do not need to work around these, but knowing them explains some of the design.

Network errors have different messages

A failed fetch throws a TypeError in every engine, with a different message: Failed to fetch in Chromium, NetworkError when attempting to fetch resource. in Firefox, Load failed in Safari. Matching on the message would break in two of three browsers, so watchFetch checks err instanceof TypeError and never reads the text.

iOS freezes timers in background tabs

Safari on iOS freezes JavaScript timers in background tabs. A retry timer set while offline may not fire on time, or until the tab is visible again. net-watch does not rely on timers alone. It re-checks on visibilitychange when the tab becomes visible and on pageshow when a page is restored from the back/forward cache.

Virtual adapters keep navigator.onLine true

Docker, VPN clients and Hyper-V install virtual network adapters. With them present, navigator.onLine can stay true after you switch Wi-Fi off. That is the main reason a pingUrl exists, and the reason net-watch only trusts false. To test your offline UI, use DevTools, Network, Offline. Switching Wi-Fi off on a development machine can mislead you.

No AbortSignal.timeout

AbortSignal.timeout() arrived in Safari 16, and the floor is Safari 15. The probe uses an AbortController and a setTimeout instead, which works everywhere.

Syntax target

The package builds to es2020, so newer syntax such as ??= is lowered for the floor browsers.

Not supported

  • Browsers below the floor. They may work, but they are not tested.
  • Non-browser runtimes. Importing the package works anywhere, because it is SSR-safe, but detection only runs where window exists. For React Native, use @react-native-community/netinfo.

Module formats

@net-watch/core, @net-watch/react and @net-watch/vue ship both ES modules and CommonJS, so import and require() both work, including in Jest's CommonJS mode and on Node versions that cannot require() ES modules. @net-watch/angular is ES modules only, like every Angular library. Types are included for both.

Use one format per app. If a bundle loads both the ESM and the CommonJS copy of @net-watch/core, sharedWatcher() creates one watcher per copy.

See also

On this page