Skip to content

Instantly share code, notes, and snippets.

@csswizardry
Last active August 18, 2026 14:09
Show Gist options
  • Select an option

  • Save csswizardry/6dd60f7536835d9c42ff05efd1b75a98 to your computer and use it in GitHub Desktop.

Select an option

Save csswizardry/6dd60f7536835d9c42ff05efd1b75a98 to your computer and use it in GitHub Desktop.
LUX Sidecar – Your SpeedCurve Companion
_ingest/
_assets/
llm-scripts/

LUX Sidecar – Your SpeedCurve Companion

mascot

N.B. LUX Sidecar requires a SpeedCurve RUM account. Sign up for a free trial.

LUX Sidecar adds a small set of browser-derived dimensions to SpeedCurve LUX. It is a companion script, not a replacement for the LUX agent: it sends its values through LUX.addData() and relies on LUX to collect and beacon them.

The script is deliberately zero-config. Include it on pages where the standard LUX snippet has already established window.LUX.addData(), and it will record the values that the browser makes available for that navigation. Where LUX is not present, it remains inert.

Installation

Load your usual SpeedCurve snippet first, then load LUX Sidecar. The external LUX agent may load before or after Sidecar; the inline snippet must come first so that LUX.addData() is available.

<!-- Your standard SpeedCurve LUX snippet goes here. -->
<script>[]</script>

<script
  src=https://cdn.speedcurve.com/js/lux.js?id=YOUR_ID
  async
  crossorigin=anonymous
></script>
<script src=path/to/lux-sidecar.js defer></script>

The supplied file is a classic browser script; no module loader, build step, or initialisation call is required.

Data Added

LUX Sidecar adds the following custom data when its underlying browser APIs and navigation values are available:

SpeedCurve limits all concatenated custom-data names and values to 500 characters. Sidecar therefore keeps descriptive semantic names in its source while sending compact beacon keys. Configure each SpeedCurve variable using its beacon key and retain the semantic name as its human-readable label.

Semantic name Beacon key Value Meaning
rtt rtt Number (ms) The Network Information API’s current navigator.connection.rtt estimate.
downlink dl Number (Mbps) The browser’s rounded, connection-wide estimate of downlink speed. It is not the measured throughput of the document navigation.
saveData sd Boolean Whether the user has asked the browser to reduce data usage.
cpuTier cpu Number (tier) The browser-reported static CPU performance tier from the early CPU Performance API proposal: 0 means unknown; the current proposed tiers are 14, with higher values representing more capable hardware, and future tiers may be added. The Chrome design sketch is not approved to ship and proposes HTTPS-only availability. It describes expected hardware capability rather than current CPU load. Configure it as a SpeedCurve dimension, not a metric.
fromCache fc Boolean true when the navigation’s transferSize is zero; false when it is greater than zero. No value is added for other values.
protocol p String The application-layer protocol used for the navigation, such as h2 or h3. The value may be empty or may describe the connection to an intermediary rather than the origin.
contentEncoding ce String The browser-reported content coding for the document navigation, such as br, gzip, or zstd. Empty or unavailable values are omitted.
responseStatus rs Number The final HTTP response status exposed for the document navigation.
viaSW sw Boolean Whether the navigation was intercepted by a service worker.
swStartupTime sws Number (ms) Approximate time spent starting or activating the service worker before its fetch event was dispatched. This is not total service-worker request-processing time.
frombfCache bfc Boolean Whether the pageshow event reports that the view was restored from the back/forward cache.
fromPrerender pr Boolean Whether the document is currently prerendering, or its navigation has a non-zero activationStart.
compressionDelta cd Number (fraction) The proportion of HTML body bytes saved by content encoding, rounded to at most two decimal places: 0.4 means 40% smaller, 0 means unchanged, and -0.15 means 15% larger. This excludes response headers and is unavailable when the browser masks or omits the body sizes.
uno uno Number (ms) Unattributed Navigation Overhead: the part of time to first byte not covered by the named redirect, DNS, connection, or request-to-response-start phases.
ttlb ttlb Number (ms) Time from navigation start to responseEnd: the complete document-response time, beyond first byte.

uno and ttlb are rounded to the nearest millisecond and added only when the result is finite and non-negative.

Interpreting UNO

Tim Vereecke coined the term Unattributed Navigation Overhead to describe TTFB sub-parts that may or cannot be directly attributed due to privacy constraints. Oftentimes, though not always, this is cross-origin redirects. Thus, UNO itself is a residual and derived measurement which represents everything observed but not directly attributable. For each Navigation Timing entry, Sidecar calculates it as:

(responseStart − startTime)
− (redirectEnd − redirectStart)
− (domainLookupEnd − domainLookupStart)
− (connectEnd − connectStart)
− (responseStart − requestStart)

Unavailable or protected phase timestamps contribute zero to their phase, so their elapsed time remains in the residual. This makes UNO useful for exposing time that the browser includes in initial-document TTFB but cannot attribute to the named phases it exposes. Calculate it per navigation before aggregating; subtracting separately calculated percentiles would not represent the same thing.

Browser Support And Caveats

The script uses Navigation Timing and, when exposed by the browser, the Network Information API and prerendering state. Missing data is expected: for example, rtt is not added if navigator.connection.rtt is unavailable, and no navigation-derived values are added where there is no Navigation Timing entry.

contentEncoding is a nascent Resource Timing addition with limited browser support. Sidecar feature-detects it and does not infer a value when the browser does not expose one.

fromCache is a practical transfer-size heuristic, not a comprehensive cache taxonomy. It distinguishes zero-byte transfers from transfers that required network bytes; it does not attempt to classify every cache or revalidation outcome.

Prerendered views can include work that happened before the user activated the page. Use fromPrerender to segment those records when interpreting navigation timings such as UNO and TTLB.

Licence

LUX Sidecar is released under the MIT License. The licence text is retained in lux-sidecar.js.

/*! © Harry Roberts, csswizardry.com — released under the MIT License. */
/**
* MIT License
*
* Copyright (c) Harry Roberts
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Additional browser-derived data for SpeedCurve RUM.
*/
(() => {
const lux = window.LUX;
// Remain inert on pages where SpeedCurve has not been installed.
if (!lux || typeof lux.addData !== 'function') return;
// Keep source identifiers descriptive while limiting their beacon cost.
const CUSTOM_DATA_KEYS = {
roundTripTime: 'rtt',
downlink: 'dl',
saveData: 'sd',
cpuTier: 'cpu',
fromCache: 'fc',
protocol: 'p',
contentEncoding: 'ce',
responseStatus: 'rs',
viaServiceWorker: 'sw',
serviceWorkerStartupTime: 'sws',
fromBackForwardCache: 'bfc',
fromPrerender: 'pr',
compressionDelta: 'cd',
unattributedNavigationOverhead: 'uno',
timeToLastByte: 'ttlb',
};
const { connection } = navigator;
// Surface the browser’s current estimate of network round-trip latency.
if (connection && 'rtt' in connection) {
lux.addData(CUSTOM_DATA_KEYS.roundTripTime, connection.rtt);
}
// Surface the browser’s rounded estimate of connection downlink speed.
if (connection && 'downlink' in connection) {
const { downlink } = connection;
if (
typeof downlink === 'number' &&
Number.isFinite(downlink) &&
downlink >= 0
) {
lux.addData(CUSTOM_DATA_KEYS.downlink, downlink);
}
}
// Record whether the user has asked the browser to reduce data usage.
if (connection && 'saveData' in connection) {
const { saveData } = connection;
if (typeof saveData === 'boolean') {
lux.addData(CUSTOM_DATA_KEYS.saveData, saveData);
}
}
// Surface the browser-reported static CPU performance tier.
if ('cpuPerformance' in navigator) {
const { cpuPerformance } = navigator;
if (
typeof cpuPerformance === 'number' &&
Number.isFinite(cpuPerformance) &&
Number.isInteger(cpuPerformance) &&
cpuPerformance >= 0
) {
lux.addData(CUSTOM_DATA_KEYS.cpuTier, cpuPerformance);
}
}
const navigation = performance.getEntriesByType('navigation')[0];
if (!navigation) return;
// Separate locally served documents from those that required network
// transfer.
const { transferSize } = navigation;
if (transferSize === 0) {
lux.addData(CUSTOM_DATA_KEYS.fromCache, true);
} else if (transferSize > 0) {
lux.addData(CUSTOM_DATA_KEYS.fromCache, false);
}
// Preserve the application-layer protocol reported for the navigation.
if ('nextHopProtocol' in navigation) {
const { nextHopProtocol } = navigation;
if (typeof nextHopProtocol === 'string' && nextHopProtocol.length > 0) {
lux.addData(CUSTOM_DATA_KEYS.protocol, nextHopProtocol);
}
}
// Preserve the content coding reported for the navigation response.
if ('contentEncoding' in navigation) {
const { contentEncoding } = navigation;
if (typeof contentEncoding === 'string' && contentEncoding.length > 0) {
lux.addData(CUSTOM_DATA_KEYS.contentEncoding, contentEncoding);
}
}
// Record the final HTTP response status exposed for the navigation.
if ('responseStatus' in navigation) {
const { responseStatus } = navigation;
if (
typeof responseStatus === 'number' &&
Number.isFinite(responseStatus) &&
Number.isInteger(responseStatus) &&
responseStatus > 0
) {
lux.addData(CUSTOM_DATA_KEYS.responseStatus, responseStatus);
}
}
// Identify navigations intercepted by a service worker.
if ('workerStart' in navigation) {
const { workerStart } = navigation;
if (typeof workerStart === 'number' && Number.isFinite(workerStart)) {
if (workerStart > 0) {
lux.addData(CUSTOM_DATA_KEYS.viaServiceWorker, true);
} else if (workerStart === 0) {
lux.addData(CUSTOM_DATA_KEYS.viaServiceWorker, false);
}
}
}
// Approximate the time spent starting or activating the service worker.
if ('workerStart' in navigation) {
const { fetchStart, workerStart } = navigation;
if (
typeof fetchStart === 'number' &&
Number.isFinite(fetchStart) &&
typeof workerStart === 'number' &&
Number.isFinite(workerStart) &&
workerStart > 0 &&
fetchStart >= workerStart
) {
const swStartupTime = Math.round(fetchStart - workerStart);
if (Number.isFinite(swStartupTime) && swStartupTime >= 0) {
lux.addData(CUSTOM_DATA_KEYS.serviceWorkerStartupTime, swStartupTime);
}
}
}
// Keep restored views distinct from conventional navigations in RUM analysis.
window.addEventListener('pageshow', (event) => {
lux.addData(CUSTOM_DATA_KEYS.fromBackForwardCache, event.persisted);
});
// Preserve prerender history so pre-activation timings remain interpretable.
lux.addData(
CUSTOM_DATA_KEYS.fromPrerender,
document.prerendering || navigation.activationStart > 0
);
// Measure the proportion of HTML body bytes saved by content encoding.
const { decodedBodySize, encodedBodySize } = navigation;
if (
typeof decodedBodySize === 'number' &&
Number.isFinite(decodedBodySize) &&
decodedBodySize > 0 &&
typeof encodedBodySize === 'number' &&
Number.isFinite(encodedBodySize) &&
encodedBodySize > 0
) {
const compressionDelta =
Math.round((1 - encodedBodySize / decodedBodySize) * 100) / 100;
if (Number.isFinite(compressionDelta)) {
lux.addData(CUSTOM_DATA_KEYS.compressionDelta, compressionDelta);
}
}
// Unattributed Navigation Overhead (UNO): TTFB not covered by named phases.
// https://calendar.perfplanet.com/2024/uno/
const span = (end, start) => Math.max(0, end - start);
const uno = Math.round(
(navigation.responseStart - navigation.startTime) -
span(navigation.redirectEnd, navigation.redirectStart) -
span(navigation.domainLookupEnd, navigation.domainLookupStart) -
span(navigation.connectEnd, navigation.connectStart) -
span(navigation.responseStart, navigation.requestStart)
);
if (Number.isFinite(uno) && uno >= 0) {
lux.addData(CUSTOM_DATA_KEYS.unattributedNavigationOverhead, uno);
}
// Capture the full document response time, beyond the first byte as captured
// by TTFB.
if (navigation.responseEnd && navigation.startTime >= 0) {
const ttlb = Math.round(navigation.responseEnd - navigation.startTime);
if (Number.isFinite(ttlb) && ttlb >= 0) {
lux.addData(CUSTOM_DATA_KEYS.timeToLastByte, ttlb);
}
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment