-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathobs.js
More file actions
374 lines (294 loc) · 12.9 KB
/
obs.js
File metadata and controls
374 lines (294 loc) · 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
;(() => {
/**
* Obs.js uses the Navigator and Battery APIs to get realtime network and
* battery status of your user’s device. You can use this information to
* adapt to their context.
*/
/**
* Immediately disallow the inclusion of Obs.js as an external script, or as
* an inline `type=module`, except on localhost. This file should not be run
* asynchronously. It should not be placed externally either: that would kill
* performance and that’s the exact opposite of what we’re trying to achieve.
*/
const obsSrc = document.currentScript;
if (!obsSrc || obsSrc.src || (obsSrc.type && obsSrc.type.toLowerCase() === 'module')) {
if (/^(localhost|127\.0\.0\.1|::1)$/.test(location.hostname) === false) {
console.warn(
'[Obs.js] Skipping: must be an inline, classic <script> in <head>.',
obsSrc ? (obsSrc.src ? 'src=' + obsSrc.src : 'type=' + obsSrc.type) : 'type=module'
);
return;
}
}
// Attach Obs.js classes to the `<html>` element.
const html = document.documentElement;
// Grab the `connection` property from `navigator`.
const { connection } = navigator;
// Store state in a global `window.obs` object for reuse later.
window.obs = window.obs || {};
// Make Obs.js configurable.
const obsConfig = (window.obs && window.obs.config) || {};
// Passively listen to changes in network or battery condition without page
// refresh. Defaults to false.
const observeChanges = obsConfig.observeChanges === true;
// Helper function:
//
// Bucket RTT into the nearest upper 25ms. E.g. an RTT of 108ms would be put
// into the 125ms bucket. Think of 125ms as being 101–125ms.
const bucketRTT = rtt =>
Number.isFinite(rtt) ? Math.ceil(rtt / 25) * 25 : null;
// Helper function:
//
// Categorise the observed RTT into CrUX’s High, Medium, Low latency
// thresholds: https://developer.chrome.com/blog/crux-2025-02#rtt_tri-bins
const categoriseRTT = rtt =>
Number.isFinite(rtt)
? (rtt < 75 ? 'low' : rtt <= 275 ? 'medium' : 'high')
: null;
// Helper function:
//
// Bucket downlink to 1Mbps steps. This coarsens the reported `downlink` by
// a factor of 40. Chromium-based browsers commonly cap around ~10Mbps for
// privacy reasons, so you may not ever see values above ~10.
// https://caniuse.com/mdn-api_networkinformation_downlink
const bucketDownlink = d =>
Number.isFinite(d) ? Math.ceil(d) : null;
// Helper function:
// Categorise device memory (GB) into tiers. Again, user agents may cap this
// for privacy reasons.
const categoriseDeviceMemory = gb =>
Number.isFinite(gb)
? (gb <= 1 ? 'very-low' : gb <= 2 ? 'low' : gb <= 4 ? 'medium' : 'high')
: null;
// Helper function:
// Categorise logical CPU cores into tiers.
const categoriseCpuCores = cores =>
Number.isFinite(cores)
? (cores <= 2 ? 'low' : cores <= 5 ? 'medium' : 'high')
: null;
// Combine hardware signals (CPU + memory) into a device-capability Stance.
//
// Exposes on `window.obs`:
//
// * ramCategory: ‘very-low’|‘low’|‘medium’|‘high’
// * cpuCategory: 'low’|‘medium’|‘high'
// * deviceCapability: ‘weak’|‘moderate’|‘strong’
const recomputeDeviceCapability = () => {
const o = window.obs || {};
const mem = o.ramCategory;
const cpu = o.cpuCategory;
let deviceCap = 'moderate';
const memWeak = mem === 'very-low' || mem === 'low';
const memStrong = mem === 'high' || mem === 'medium';
const cpuWeak = cpu === 'low';
const cpuStrong = cpu === 'high';
if (memStrong && cpuStrong) deviceCap = 'strong';
else if (memWeak || cpuWeak) deviceCap = 'weak';
o.deviceCapability = deviceCap;
['strong','moderate','weak'].forEach(t => {
html.classList.remove(`has-device-capability-${t}`);
});
html.classList.add(`has-device-capability-${deviceCap}`);
};
// Combine network capability (RTT + bandwidth) and user/device preferences
// (Save-Data, low battery) into a delivery ‘Stance’.
//
// Exposes on `window.obs`:
//
// * connectionCapability: 'strong'|'moderate'|'weak'
// * conservationPreference: 'conserve'|'neutral'
// * deliveryMode: 'rich'|'cautious'|'lite'
// * canShowRichMedia: boolean
// * shouldAvoidRichMedia: boolean
const recomputeDelivery = () => {
const o = window.obs || {};
// Classify connection strength based on RTT and downlink.
const bw = typeof o.downlinkBucket === 'number' ? o.downlinkBucket : null;
const lowRTT = o.rttCategory === 'low';
const highRTT = o.rttCategory === 'high';
const highBW = bw != null && bw >= 8; // 1Mbps buckets
const lowBW = bw != null && bw <= 5;
o.connectionCapability = (lowRTT && highBW)
? 'strong'
: (highRTT || lowBW)
? 'weak'
: 'moderate';
// Classify resource conservation based on Save-Data and battery level.
// N.B.: ‘critical’ is a subset of ‘low’ and should also be considered.
const conserve = (o.dataSaver === true)
|| (o.batteryLow === true)
|| (o.batteryCritical === true);
o.conservationPreference = conserve ? 'conserve' : 'neutral';
// Delivery mode:
// * ‘lite’ if the link is weak OR Data Saver is on OR battery is critical
// * ‘cautious’ if battery is low (but not critical)
// * ‘rich’ only when strong and not conserving
const forcedLite =
o.connectionCapability === 'weak' ||
o.dataSaver === true ||
o.batteryCritical === true;
const rich = o.connectionCapability === 'strong' && !forcedLite && !conserve;
o.deliveryMode = rich
? 'rich'
: forcedLite
? 'lite'
: 'cautious';
// Assign delivery Stances into convenient booleans,
// e.g.: `if(canShowRichMedia) { … }`
// We only trigger this for ‘lite’ and ‘rich’ scenarios: we don’t currently
// do anything for ‘cautious.
o.canShowRichMedia = (o.deliveryMode !== 'lite');
o.shouldAvoidRichMedia = (o.deliveryMode === 'lite');
// Add classes to the `<html>` element for each of our connection-capability
// Stances.
['strong','moderate','weak'].forEach(t => {
html.classList.remove(`has-connection-capability-${t}`);
});
html.classList.add(`has-connection-capability-${o.connectionCapability}`);
// Add classes to the `<html>` element for each of our conservation Stances.
// E.g. `<html class="has-conservation-preference-conserve">`
// Remove any leftover classes from previous run.
['conserve','neutral'].forEach(t => {
html.classList.remove(`has-conservation-preference-${t}`);
});
html.classList.add(`has-conservation-preference-${o.conservationPreference}`);
// Add classes to the `<html>` element for each of our delivery Stances.
// E.g. `<html class="has-delivery-mode-rich">`
// Remove any leftover classes from previous run.
['rich','cautious','lite'].forEach(t => {
html.classList.remove(`has-delivery-mode-${t}`);
});
html.classList.add(`has-delivery-mode-${o.deliveryMode}`);
};
// Run this function on demand to grab fresh data from the Network Information
// API.
const refreshConnectionStatus = () => {
if (!connection) return;
// We need to know about Data Saver mode, latency estimates, and bandwidth
// estimates.
const { saveData, rtt, downlink } = connection;
// Add a class to the `<html>` element if someone has Data Saver mode
// enabled.
window.obs.dataSaver = !!saveData;
html.classList.toggle('has-data-saver', !!saveData);
// Get latency information from `rtt`. Bucket it into our predefined
// thresholds.
const rttBucket = bucketRTT(rtt);
if (rttBucket != null) window.obs.rttBucket = rttBucket;
// Add high, medium, low latency classes to the `<html>` element.
// E.g. `<html class="has-latency-low">`
const rttCategory = categoriseRTT(rtt);
if (rttCategory) {
window.obs.rttCategory = rttCategory;
// Remove any prior latency class then add the current one.
['low', 'medium', 'high']
.forEach(l => html.classList.remove(`has-latency-${l}`));
html.classList.add(`has-latency-${rttCategory}`);
}
// Get bandwidth information from `downlink`. Bucket it into our preference
// thresholds.
const downlinkBucket = bucketDownlink(downlink);
if (downlinkBucket != null) {
window.obs.downlinkBucket = downlinkBucket; // 1‑Mbps
// Add low, medium, or high bandwidth classes to the `<html>` element.
// low = ≤5 Mbps, medium = 6–7 Mbps, high = ≥8 Mbps
// E.g. `<html class="has-bandwidth-high">`
const downlinkCategory =
downlinkBucket <= 5 ? 'low' :
downlinkBucket >= 8 ? 'high' : 'medium';
window.obs.downlinkCategory = downlinkCategory;
['low', 'medium', 'high']
.forEach(b => html.classList.remove(`has-bandwidth-${b}`));
html.classList.add(`has-bandwidth-${downlinkCategory}`);
}
// Obs.js doesn’t currently do anything with it, but we capture the maximum
// estimated `downlink` while we’re here.
if ('downlinkMax' in connection) {
window.obs.downlinkMax = connection.downlinkMax;
}
// Update delivery Stance combining capability and conservation preferences.
recomputeDelivery();
};
// Run the connection function as soon as Obs.js loads.
refreshConnectionStatus();
// If configured, listen out for network condition changes and rerun the
// function in response.
if (observeChanges && connection && typeof connection.addEventListener === 'function') {
connection.addEventListener('change', refreshConnectionStatus);
}
// Run this function on demand to grab fresh data from the Battery API.
const refreshBatteryStatus = battery => {
if (!battery) return;
// Get battery level and charging status.
const { level, charging } = battery;
// The API doesn’t report Low Power Mode or similar. Therefore, treat ≤5% as
// ‘critical’ and ≤20% as ‘low’.
const critical = Number.isFinite(level) ? level <= 0.05 : null;
window.obs.batteryCritical = critical;
const low = Number.isFinite(level) ? level <= 0.2 : null;
window.obs.batteryLow = low;
// Add battery classes (subset model): at ≤5% we want BOTH low and critical.
// First remove leftovers, then add any that apply.
// E.g. `<html class="has-battery-low has-battery-critical">`
['critical','low'].forEach(t => html.classList.remove(`has-battery-${t}`));
if (low) html.classList.add('has-battery-low');
if (critical) html.classList.add('has-battery-critical');
// Add a class to the `<html>` element if the device is currently charging.
// E.g. `<html class="has-battery-charging">`
const isCharging = !!charging;
window.obs.batteryCharging = isCharging;
html.classList.toggle('has-battery-charging', isCharging);
// Update delivery Stance combining capability and preferences.
recomputeDelivery();
};
// The Battery API returns a Promise: get to work on it once it resolves.
if ('getBattery' in navigator) {
navigator.getBattery()
.then(battery => {
// Run the battery function immediately.
refreshBatteryStatus(battery);
// If configured, listen out for battery changes and rerun the function
// in response.
if (observeChanges && typeof battery.addEventListener === 'function') {
battery.addEventListener('levelchange', () => refreshBatteryStatus(battery));
battery.addEventListener('chargingchange', () => refreshBatteryStatus(battery));
}
})
// Fail silently otherwise.
.catch(() => { /* no‑op */ });
}
// Device Memory (GB) → very-low/low/medium/high
//
// Exposes on `window.obs`:
// * ramBucket: number|null
if ('deviceMemory' in navigator) {
const memRaw = Number(navigator.deviceMemory);
const memGB = Number.isFinite(memRaw) ? memRaw : null;
window.obs.ramBucket = memGB;
const memCat = categoriseDeviceMemory(memGB);
if (memCat) {
window.obs.ramCategory = memCat;
['very-low','low','medium','high']
.forEach(t => html.classList.remove(`has-ram-${t}`));
html.classList.add(`has-ram-${memCat}`);
}
}
// CPU logical cores → low/medium/high
//
// Exposes on `window.obs`:
// * cpuBucket: number|null
if ('hardwareConcurrency' in navigator) {
const coresRaw = Number(navigator.hardwareConcurrency);
const cores = Number.isFinite(coresRaw) ? coresRaw : null;
window.obs.cpuBucket = cores;
const cpuCat = categoriseCpuCores(cores);
if (cpuCat) {
window.obs.cpuCategory = cpuCat;
['low','medium','high']
.forEach(t => html.classList.remove(`has-cpu-${t}`));
html.classList.add(`has-cpu-${cpuCat}`);
}
}
// Compute the device-capability stance once the static hardware signals are in.
recomputeDeviceCapability();
})();