An open API service indexing awesome lists of open source software.

https://github.com/csswizardry/obs.js

Context-aware web performance for everyone
https://github.com/csswizardry/obs.js

battery-api navigator-api sitespeed webperf webperformance

Last synced: 4 months ago
JSON representation

Context-aware web performance for everyone

Awesome Lists containing this project

README

          

Obs.js

# Obs.js: context‑aware web performance for everyone

_Meet your users where they are_

Obs.js uses the Navigator and Battery APIs to get contextual information about
your users’ connection strength and battery status.

You can use this data to adapt your site/app to their environment, or beacon the
data off to an analytics endpoint.

At its simplest, Obs.js will add a suite of classes to your `` element,
e.g.:

```html

```

This means you could do something like this:

```css
/**
* Disable all animations and transitions if a user’s battery is below 5%.
*/
.has-battery-critical,
.has-battery-critical * {
animation: none;
transition: none;
}
```

Or this:

```css
body {
background-image: url('hi-res.jpg');
}

/**
* Show low-resolution images if the user can’t take rich media right now.
*/
.has-delivery-mode-lite body {
background-image: url('lo-res.jpg');
}
```

It also exposes this, and more, information via the `window.obs` object:

```js
{
"config": {
"observeChanges": false
},
"dataSaver": false,
"rttBucket": 50,
"rttCategory": "low",
"downlinkBucket": 10,
"connectionCapability": "strong",
"conservationPreference": "neutral",
"deliveryMode": "rich",
"canShowRichMedia": true,
"shouldAvoidRichMedia": false,
"batteryCritical": false,
"batteryLow": false,
"batteryCharging": true
}
```

This means you could do something like this:

```html

const mediaPlaceholder = document.querySelector('.media-placeholder');

if (window.obs && window.obs.canShowRichMedia) {
// If we can show rich media, load the video with the poster image in place.
const v = document.createElement('video');
v.src = 'video.mp4';
v.poster = 'poster.jpg';
v.autoplay = true;
v.muted = true;
v.playsInline = true;
v.setAttribute('controls', '');
mediaPlaceholder.replaceChildren(v);
} else {
// If not, just show the poster image as an image element.
const img = new Image();
img.src = 'poster.jpg';
img.alt = '';
mediaPlaceholder.replaceChildren(img);
}


```

## Installation

Obs.js **MUST** be placed in an inline `` tag in the `<head>` of your
document, before any other scripts, stylesheets, or HTML that may depend on it.

Copy/paste the following as close to the top of your `<head>` as possible:

```html
<script>
/*! Obs.js 0.2.1 | (c) Harry Roberts, csswizardry.com | MIT */
;(()=>{const e=document.currentScript;if((!e||e.src||e.type&&"module"===e.type.toLowerCase())&&!1===/^(localhost|127\.0\.0\.1|::1)$/.test(location.hostname))return void console.warn("[Obs.js] Skipping: must be an inline, classic <script> in <head>.",e?e.src?"src="+e.src:"type="+e.type:"type=module");const t=document.documentElement,{connection:i}=navigator;window.obs=window.obs||{};const a=!0===(window.obs&&window.obs.config||{}).observeChanges,o=()=>{const e=window.obs||{},i="number"==typeof e.downlinkBucket?e.downlinkBucket:null;e.connectionCapability="low"===e.rttCategory&&null!=i&&i>=8?"strong":"high"===e.rttCategory||null!=i&&i<=5?"weak":"moderate";const a=!0===e.dataSaver||!0===e.batteryLow||!0===e.batteryCritical;e.conservationPreference=a?"conserve":"neutral";const o="weak"===e.connectionCapability||!0===e.dataSaver||!0===e.batteryCritical;e.deliveryMode="strong"!==e.connectionCapability||o||a?o?"lite":"cautious":"rich",e.canShowRichMedia="lite"!==e.deliveryMode,e.shouldAvoidRichMedia="lite"===e.deliveryMode,["strong","moderate","weak"].forEach(e=>{t.classList.remove(`has-connection-capability-${e}`)}),t.classList.add(`has-connection-capability-${e.connectionCapability}`),["conserve","neutral"].forEach(e=>{t.classList.remove(`has-conservation-preference-${e}`)}),t.classList.add(`has-conservation-preference-${e.conservationPreference}`),["rich","cautious","lite"].forEach(e=>{t.classList.remove(`has-delivery-mode-${e}`)}),t.classList.add(`has-delivery-mode-${e.deliveryMode}`)},n=()=>{if(!i)return;const{saveData:e,rtt:a,downlink:n}=i;window.obs.dataSaver=!!e,t.classList.toggle("has-data-saver",!!e);const s=(e=>Number.isFinite(e)?25*Math.ceil(e/25):null)(a);null!=s&&(window.obs.rttBucket=s);const r=(e=>Number.isFinite(e)?e<75?"low":e<=275?"medium":"high":null)(a);r&&(window.obs.rttCategory=r,["low","medium","high"].forEach(e=>t.classList.remove(`has-latency-${e}`)),t.classList.add(`has-latency-${r}`));const c=(l=n,Number.isFinite(l)?Math.ceil(l):null);var l;if(null!=c){window.obs.downlinkBucket=c;const e=c<=5?"low":c>=8?"high":"medium";window.obs.downlinkCategory=e,["low","medium","high"].forEach(e=>t.classList.remove(`has-bandwidth-${e}`)),t.classList.add(`has-bandwidth-${e}`)}"downlinkMax"in i&&(window.obs.downlinkMax=i.downlinkMax),o()};n(),a&&i&&"function"==typeof i.addEventListener&&i.addEventListener("change",n);const s=e=>{if(!e)return;const{level:i,charging:a}=e,n=Number.isFinite(i)?i<=.05:null;window.obs.batteryCritical=n;const s=Number.isFinite(i)?i<=.2:null;window.obs.batteryLow=s,["critical","low"].forEach(e=>t.classList.remove(`has-battery-${e}`)),s&&t.classList.add("has-battery-low"),n&&t.classList.add("has-battery-critical");const r=!!a;window.obs.batteryCharging=r,t.classList.toggle("has-battery-charging",r),o()};if("getBattery"in navigator&&navigator.getBattery().then(e=>{s(e),a&&"function"==typeof e.addEventListener&&(e.addEventListener("levelchange",()=>s(e)),e.addEventListener("chargingchange",()=>s(e)))}).catch(()=>{}),"deviceMemory"in navigator){const e=Number(navigator.deviceMemory),i=Number.isFinite(e)?e:null;window.obs.ramBucket=i;const a=(r=i,Number.isFinite(r)?r<=1?"very-low":r<=2?"low":r<=4?"medium":"high":null);a&&(window.obs.ramCategory=a,["very-low","low","medium","high"].forEach(e=>t.classList.remove(`has-ram-${e}`)),t.classList.add(`has-ram-${a}`))}var r;if("hardwareConcurrency"in navigator){const e=Number(navigator.hardwareConcurrency),i=Number.isFinite(e)?e:null;window.obs.cpuBucket=i;const a=(e=>Number.isFinite(e)?e<=2?"low":e<=5?"medium":"high":null)(i);a&&(window.obs.cpuCategory=a,["low","medium","high"].forEach(e=>t.classList.remove(`has-cpu-${e}`)),t.classList.add(`has-cpu-${a}`))}(()=>{const e=window.obs||{},i=e.ramCategory,a=e.cpuCategory;let o="moderate";"high"!==i&&"medium"!==i||"high"!==a?("very-low"===i||"low"===i||"low"===a)&&(o="weak"):o="strong",e.deviceCapability=o,["strong","moderate","weak"].forEach(e=>{t.classList.remove(`has-device-capability-${e}`)}),t.classList.add(`has-device-capability-${o}`)})()})();
//# sourceURL=obs.inline.js

```

Or download the [latest minified
version](https://github.com/csswizardry/Obs.js/releases/latest).

### Listen for Changes

If you have long-lived pages or a single-page app, you can instruct Obs.js to
listen for changes to the connection and battery status by setting the following
config:

```html
window.obs = { config: { observeChanges: true } }

// Obs.js

```

The default is `false`, which means Obs.js will only run once on each page load.
This is sufficient for most non-SPA sites.

## Statuses and Stances

The information provided by Obs.js is split into two categories: **Statuses**
and **Stances**.

* A **Status** is a factual piece of information, such as whether the user has
enabled Data Saver, or whether their battery is charging, or if they are on
a high latency connection.
* A **Stance** is an opinion derived from Statuses. For example, if the user has
enabled Data Saver or their battery is low, we might say they have
a **conservation preference** of `conserve`, meaning they might prefer to save
resources.

You can use either Statuses or Stances in your CSS or JavaScript.

## Available CSS Classes and JS Properties

Obs.js exposes the following classes under the following conditions:

| Class | Meaning | Computed/derived from |
| --------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `.has-data-saver` | User enabled Data Saver | `navigator.connection.saveData === true` |
| `.has-battery-critical` | Battery ≤ 5% | `battery.level ≤ 0.05` (added **alongside** `.has-battery-low`) |
| `.has-battery-low` | Battery ≤ 20% | `battery.level ≤ 0.2` |
| `.has-battery-charging` | On charge | `battery.charging === true` |
| `.has-latency-low` | Low RTT | `rtt < 75ms` |
| `.has-latency-medium` | Medium RTT | `75–275ms` |
| `.has-latency-high` | High RTT | `> 275ms` |
| `.has-bandwidth-low` | Low estimated bandwidth | `downlinkCategory === 'low'` (i.e. `downlinkBucket ≤ 5`Mbps) |
| `.has-bandwidth-medium` | Mid estimated bandwidth | `downlinkCategory === 'medium'` (i.e. `downlinkBucket` 6–7Mbps) |
| `.has-bandwidth-high` | High estimated bandwidth | `downlinkCategory === 'high'` (i.e. `downlinkBucket ≥ 8`Mbps) |
| `.has-connection-capability-weak` | Transport looks weak | `rttCategory === 'high'` **or** `downlinkCategory === 'low'` |
| `.has-connection-capability-moderate` | Transport middling | Anything not strong/weak |
| `.has-connection-capability-strong` | Transport looks strong | `rttCategory === 'low'` **and** `downlinkCategory === 'high'` |
| `.has-conservation-preference-conserve` | Frugality signal present | `dataSaver === true` **or** `batteryLow === true` |
| `.has-conservation-preference-neutral` | No frugality signal | Battery isn’t low and Data Saver is not enabled |
| `.has-delivery-mode-lite` | Be frugal/lightweight | `connectionCapability === 'weak'` **or** `dataSaver === true` **or** `batteryCritical === true` |
| `.has-delivery-mode-cautious` | Be careful/middle weight | Otherwise (not `rich`/`lite`). E.g. `batteryLow === true` (without `dataSaver`/`batteryCritical`) or `connectionCapability === 'moderate'`. |
| `.has-delivery-mode-rich` | Allow rich/heavy media | `connectionCapability === 'strong'` **and** `dataSaver !== true` **and** `batteryCritical !== true` |
| `.has-ram-very-low` | Very low RAM tier | `ramCategory === 'very-low'` (typically `ramBucket ≤ 1`GB) |
| `.has-ram-low` | Low RAM tier | `ramCategory === 'low'` (typically `ramBucket ≤ 2`GB and > 1) |
| `.has-ram-medium` | Medium RAM tier | `ramCategory === 'medium'` (typically `ramBucket ≤ 4`GB and > 2) |
| `.has-ram-high` | High RAM tier | `ramCategory === 'high'` (typically `ramBucket > 4`GB) |
| `.has-cpu-low` | Few logical cores | `cpuCategory === 'low'` (≤ 2 cores) |
| `.has-cpu-medium` | Moderate logical cores | `cpuCategory === 'medium'` (3–5 cores) |
| `.has-cpu-high` | Many logical cores | `cpuCategory === 'high'` (≥ 6 cores) |
| `.has-device-capability-weak` | Hardware looks weak | `cpuCategory === 'low'` **or** `ramCategory` is `'very-low'`/`'low'` |
| `.has-device-capability-moderate` | Hardware middling | Anything not strong/weak |
| `.has-device-capability-strong` | Hardware looks strong | `cpuCategory === 'high'` **and** `ramCategory` is `'medium'` **or** `'high'` |

These classes are automatically added to the `` element.

Obs.js also stores the following properties on the `window.obs` object:

| Property | Type | Meaning | Computed/derived from | Notes |
| ------------------------ | ------------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `config.observeChanges` | boolean | Attach change listeners | **Default `false`**; set by you _before_ Obs.js runs | Opt-in for SPAs or long-lived pages |
| `dataSaver` | boolean | User enabled Data Saver | `navigator.connection.saveData` | — |
| `rttBucket` | number (ms) | RTT bucketed to **ceil** 25ms | `navigator.connection.rtt` | Undefined if Connection API missing |
| `rttCategory` | `'low'` \| `'medium'` \| `'high'` | CrUX tri-bin | Derived from RTT (`<75`, `75–275`, `>275`) | Drives latency classes |
| `downlinkBucket` | number (Mbps) | Downlink bucketed to **ceil** 1Mbps | `navigator.connection.downlink` | Thresholds: `≤5`, `6–7`, `≥8` |
| `downlinkCategory` | `'low'` \| `'medium'` \| `'high'` | Bandwidth category | From `downlinkBucket` (≤ 5 → low, 6–7 → medium, ≥ 8 → high) | Mirrors `.has-bandwidth-*` classes |
| `downlinkMax` | number (Mbps) | Max estimated downlink (if exposed) | `navigator.connection.downlinkMax` | Informational only |
| `connectionCapability` | `'strong'` \| `'moderate'` \| `'weak'` | Transport assessment | From `rttCategory` + `downlinkCategory` (low/high signals) | Strong = low RTT **and** high BW; Weak = high RTT **or** low BW |
| `conservationPreference` | `'conserve'` \| `'neutral'` | Frugality signal | `dataSaver === true` **or** `batteryLow === true` | — |
| `deliveryMode` | `'rich'` \| `'cautious'` \| `'lite'` | How ‘heavy’ you should go | From `connectionCapability`, `dataSaver`, `batteryLow`, `batteryCritical` | **rich** if strong and not (`dataSaver` or `batteryCritical`); **lite** if weak **or** `dataSaver` **or** `batteryCritical`; else **cautious** (e.g. `batteryLow`/`moderate`) |
| `canShowRichMedia` | boolean | Convenience: `deliveryMode !== 'lite'` | Derived from `deliveryMode` | Shorthand for ‘go big’ |
| `shouldAvoidRichMedia` | boolean | Convenience: `deliveryMode === 'lite'` | Derived from `deliveryMode` | Shorthand for ‘be frugal’ |
| `batteryCritical` | boolean \| null | Battery ≤ 5% | Battery API | `true` when battery level is ≤ 5%; **also** `batteryLow === true` |
| `batteryLow` | boolean \| null | Battery ≤ 20% | Battery API | `true` when battery level is ≤ 20%; `null` if unknown |
| `batteryCharging` | boolean \| null | On charge | Battery API | `null` if unknown |
| `ramBucket` | number (GB) | Coarse device RAM bucket | `navigator.deviceMemory` (UA-rounded) | Typical values: 0.5, 1, 2, 4, 8 |
| `ramCategory` | `'very-low'` \| `'low'` \| `'medium'` \| `'high'` | RAM tier | From `ramBucket` | Adds `.has-ram-*` classes |
| `cpuBucket` | number (cores) | 1-core bucket (integer cores) | `navigator.hardwareConcurrency` | Prefer `cpuCategory` for segmentation |
| `cpuCategory` | `'low'` \| `'medium'` \| `'high'` | CPU tier | From cores (≤ 2 = low, 3–5 = medium, ≥ 6 = high) | Adds `.has-cpu-*` classes |
| `deviceCapability` | `'strong'` \| `'moderate'` \| `'weak'` | Device capability stance | From `ramCategory` and `cpuCategory` | **strong** when CPU is **high** **and** RAM is **medium/high**; **weak** when RAM is **very-low/low** **or** CPU is **low**; otherwise **moderate**. Adds matching classes. |

## Unsupported Browsers

Most of these APIs are only available in Chromium browsers. This means you need
to decide how to handle notable absentees like iOS yourself: Obs.js does not
make opinionated decisions for you.

Your choices are:

1. Always ship the rich version to Safari, or;
2. Always ship the lite version to Safari.

You can write your `if`s and `else`s to accommodate either.

```js
if (window.obs?.shouldAvoidRichMedia === true) {
// Serve lite version to slow supportive browsers.
} else {
// Serve rich version to fast supportive browsers and Safari.
}
```

```js
if (window.obs?.canShowRichMedia === true) {
// Serve rich version to fast supportive browsers.
} else {
// Serve lite version to slow supportive browsers and Safari.
}
```

The choice is yours.