Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Context-aware web performance for everyone

License

NotificationsYou must be signed in to change notification settings

csswizardry/Obs.js

Repository files navigation

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 aboutyour users’ connection strength and battery status.

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

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

<htmlclass="has-latency-low             has-bandwidth-high             has-battery-charging             has-connection-capability-strong             has-conservation-preference-neutral             has-delivery-mode-rich">

This means you could do something like this:

/** * 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:

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

It also exposes this, and more, information via thewindow.obs object:

{"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:

<!--  - Fetch low-resolution poster/placeholder image regardless.  --><linkrel=preloadas=imagehref=poster.jpg><divclass=media-placeholderstyle="background-image: url(poster.jpg);"><script>constmediaPlaceholder=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.constv=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.constimg=newImage();img.src='poster.jpg';img.alt='';mediaPlaceholder.replaceChildren(img);}</script></div>

Installation

Obs.jsMUST be placed in an inline<script> tag in the<head> of yourdocument, 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:

<script>/*! Obs.js 0.2.1 | (c) Harry Roberts, csswizardry.com | MIT */;(()=>{conste=document.currentScript;if((!e||e.src||e.type&&"module"===e.type.toLowerCase())&&!1===/^(localhost|127\.0\.0\.1|::1)$/.test(location.hostname))returnvoidconsole.warn("[Obs.js] Skipping: must be an inline, classic <script> in <head>.",e?e.src?"src="+e.src:"type="+e.type:"type=module");constt=document.documentElement,{connection:i}=navigator;window.obs=window.obs||{};consta=!0===(window.obs&&window.obs.config||{}).observeChanges,o=()=>{conste=window.obs||{},i="number"==typeofe.downlinkBucket?e.downlinkBucket:null;e.connectionCapability="low"===e.rttCategory&&null!=i&&i>=8?"strong":"high"===e.rttCategory||null!=i&&i<=5?"weak":"moderate";consta=!0===e.dataSaver||!0===e.batteryLow||!0===e.batteryCritical;e.conservationPreference=a?"conserve":"neutral";consto="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);consts=(e=>Number.isFinite(e)?25*Math.ceil(e/25):null)(a);null!=s&&(window.obs.rttBucket=s);constr=(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}`));constc=(l=n,Number.isFinite(l)?Math.ceil(l):null);varl;if(null!=c){window.obs.downlinkBucket=c;conste=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"ini&&(window.obs.downlinkMax=i.downlinkMax),o()};n(),a&&i&&"function"==typeofi.addEventListener&&i.addEventListener("change",n);consts=e=>{if(!e)return;const{level:i,charging:a}=e,n=Number.isFinite(i)?i<=.05:null;window.obs.batteryCritical=n;consts=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");constr=!!a;window.obs.batteryCharging=r,t.classList.toggle("has-battery-charging",r),o()};if("getBattery"innavigator&&navigator.getBattery().then(e=>{s(e),a&&"function"==typeofe.addEventListener&&(e.addEventListener("levelchange",()=>s(e)),e.addEventListener("chargingchange",()=>s(e)))}).catch(()=>{}),"deviceMemory"innavigator){conste=Number(navigator.deviceMemory),i=Number.isFinite(e)?e:null;window.obs.ramBucket=i;consta=(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}`))}varr;if("hardwareConcurrency"innavigator){conste=Number(navigator.hardwareConcurrency),i=Number.isFinite(e)?e:null;window.obs.cpuBucket=i;consta=(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}`))}(()=>{conste=window.obs||{},i=e.ramCategory,a=e.cpuCategory;leto="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</script>

Or download thelatest minifiedversion.

Listen for Changes

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

<script>window.obs={config:{observeChanges:true}}</script><script>// Obs.js</script>

The default isfalse, 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:StatusesandStances.

  • AStatus is a factual piece of information, such as whether the user hasenabled Data Saver, or whether their battery is charging, or if they are ona high latency connection.
  • AStance is an opinion derived from Statuses. For example, if the user hasenabled Data Saver or their battery is low, we might say they haveaconservation preference ofconserve, meaning they might prefer to saveresources.

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:

ClassMeaningComputed/derived from
.has-data-saverUser enabled Data Savernavigator.connection.saveData === true
.has-battery-criticalBattery ≤ 5%battery.level ≤ 0.05 (addedalongside.has-battery-low)
.has-battery-lowBattery ≤ 20%battery.level ≤ 0.2
.has-battery-chargingOn chargebattery.charging === true
.has-latency-lowLow RTTrtt < 75ms
.has-latency-mediumMedium RTT75–275ms
.has-latency-highHigh RTT> 275ms
.has-bandwidth-lowLow estimated bandwidthdownlinkCategory === 'low' (i.e.downlinkBucket ≤ 5Mbps)
.has-bandwidth-mediumMid estimated bandwidthdownlinkCategory === 'medium' (i.e.downlinkBucket 6–7Mbps)
.has-bandwidth-highHigh estimated bandwidthdownlinkCategory === 'high' (i.e.downlinkBucket ≥ 8Mbps)
.has-connection-capability-weakTransport looks weakrttCategory === 'high'ordownlinkCategory === 'low'
.has-connection-capability-moderateTransport middlingAnything not strong/weak
.has-connection-capability-strongTransport looks strongrttCategory === 'low'anddownlinkCategory === 'high'
.has-conservation-preference-conserveFrugality signal presentdataSaver === trueorbatteryLow === true
.has-conservation-preference-neutralNo frugality signalBattery isn’t low and Data Saver is not enabled
.has-delivery-mode-liteBe frugal/lightweightconnectionCapability === 'weak'ordataSaver === trueorbatteryCritical === true
.has-delivery-mode-cautiousBe careful/middle weightOtherwise (notrich/lite). E.g.batteryLow === true (withoutdataSaver/batteryCritical) orconnectionCapability === 'moderate'.
.has-delivery-mode-richAllow rich/heavy mediaconnectionCapability === 'strong'anddataSaver !== trueandbatteryCritical !== true
.has-ram-very-lowVery low RAM tierramCategory === 'very-low' (typicallyramBucket ≤ 1GB)
.has-ram-lowLow RAM tierramCategory === 'low' (typicallyramBucket ≤ 2GB and > 1)
.has-ram-mediumMedium RAM tierramCategory === 'medium' (typicallyramBucket ≤ 4GB and > 2)
.has-ram-highHigh RAM tierramCategory === 'high' (typicallyramBucket > 4GB)
.has-cpu-lowFew logical corescpuCategory === 'low' (≤ 2 cores)
.has-cpu-mediumModerate logical corescpuCategory === 'medium' (3–5 cores)
.has-cpu-highMany logical corescpuCategory === 'high' (≥ 6 cores)
.has-device-capability-weakHardware looks weakcpuCategory === 'low'orramCategory is'very-low'/'low'
.has-device-capability-moderateHardware middlingAnything not strong/weak
.has-device-capability-strongHardware looks strongcpuCategory === 'high'andramCategory is'medium'or'high'

These classes are automatically added to the<html> element.

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

PropertyTypeMeaningComputed/derived fromNotes
config.observeChangesbooleanAttach change listenersDefaultfalse; set by youbefore Obs.js runsOpt-in for SPAs or long-lived pages
dataSaverbooleanUser enabled Data Savernavigator.connection.saveData
rttBucketnumber (ms)RTT bucketed toceil 25msnavigator.connection.rttUndefined if Connection API missing
rttCategory'low' |'medium' |'high'CrUX tri-binDerived from RTT (<75,75–275,>275)Drives latency classes
downlinkBucketnumber (Mbps)Downlink bucketed toceil 1Mbpsnavigator.connection.downlinkThresholds:≤5,6–7,≥8
downlinkCategory'low' |'medium' |'high'Bandwidth categoryFromdownlinkBucket (≤ 5 → low, 6–7 → medium, ≥ 8 → high)Mirrors.has-bandwidth-* classes
downlinkMaxnumber (Mbps)Max estimated downlink (if exposed)navigator.connection.downlinkMaxInformational only
connectionCapability'strong' |'moderate' |'weak'Transport assessmentFromrttCategory +downlinkCategory (low/high signals)Strong = low RTTand high BW; Weak = high RTTor low BW
conservationPreference'conserve' |'neutral'Frugality signaldataSaver === trueorbatteryLow === true
deliveryMode'rich' |'cautious' |'lite'How ‘heavy’ you should goFromconnectionCapability,dataSaver,batteryLow,batteryCriticalrich if strong and not (dataSaver orbatteryCritical);lite if weakordataSaverorbatteryCritical; elsecautious (e.g.batteryLow/moderate)
canShowRichMediabooleanConvenience:deliveryMode !== 'lite'Derived fromdeliveryModeShorthand for ‘go big’
shouldAvoidRichMediabooleanConvenience:deliveryMode === 'lite'Derived fromdeliveryModeShorthand for ‘be frugal’
batteryCriticalboolean | nullBattery ≤ 5%Battery APItrue when battery level is ≤ 5%;alsobatteryLow === true
batteryLowboolean | nullBattery ≤ 20%Battery APItrue when battery level is ≤ 20%;null if unknown
batteryChargingboolean | nullOn chargeBattery APInull if unknown
ramBucketnumber (GB)Coarse device RAM bucketnavigator.deviceMemory (UA-rounded)Typical values: 0.5, 1, 2, 4, 8
ramCategory'very-low' |'low' |'medium' |'high'RAM tierFromramBucketAdds.has-ram-* classes
cpuBucketnumber (cores)1-core bucket (integer cores)navigator.hardwareConcurrencyPrefercpuCategory for segmentation
cpuCategory'low' |'medium' |'high'CPU tierFrom cores (≤ 2 = low, 3–5 = medium, ≥ 6 = high)Adds.has-cpu-* classes
deviceCapability'strong' |'moderate' |'weak'Device capability stanceFromramCategory andcpuCategorystrong when CPU ishighand RAM ismedium/high;weak when RAM isvery-low/lowor CPU islow; otherwisemoderate. Adds matching classes.

Unsupported Browsers

Most of these APIs are only available in Chromium browsers. This means you needto decide how to handle notable absentees like iOS yourself: Obs.js does notmake 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 yourifs andelses to accommodate either.

if(window.obs?.shouldAvoidRichMedia===true){// Serve lite version to slow supportive browsers.}else{// Serve rich version to fast supportive browsers and Safari.}
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.


[8]ページ先頭

©2009-2025 Movatter.jp