Quick start
Add the monitors to a browser app, send the main-thread metrics to an OpenTelemetry backend, and stop the monitors.
Note
This page tells you how to add the monitors to a browser app. You need an OpenTelemetry setup for the page. The examples use otel-ts, but each OpenTelemetry Meter and Logger works.
Before you start
You need:
- an app that a bundler builds, for example Vite. The bundler must support
new Worker(new URL(...), { type: "module" }). - an OTLP/HTTP endpoint, for example the Grafana stack on your computer.
Install
Install the package from npm:
npm install @mark1russell7/lag
The package is an ES module with TypeScript types. @mark1russell7/lag has the monitors, and @mark1russell7/lag/worker has createLagWorker(). The API reference lists all entry points. The package page on npm has a short example with the OpenTelemetry SDK.
otel-ts is not on npm. To use it as in these examples, install it from GitHub:
npm install github:mark1russell7/otel-ts
NoteThe Vite dev server
The dev server of Vite optimizes the dependencies into one folder. Then it cannot find the file of the worker. Exclude the worker export from the optimization in vite.config.ts: optimizeDeps: { exclude: ["@mark1russell7/lag/worker"] }. vite build needs no setting.
Start the monitors
import { createInstanceId, init } from "@mark1russell7/otel-ts";
import { createBrowserDeps, createOtelEventSink, createOtelLoggerAdapter, setupAllMonitors } from "@mark1russell7/lag";
import { createLagWorker } from "@mark1russell7/lag/worker";
const endpoint = "http://localhost:4318";
const serviceInstanceId = createInstanceId();
const otel = init({
serviceName : "shop",
serviceInstanceId,
endpoint,
histogramAggregation : "exponential",
});
const worker = createLagWorker();
const monitors = setupAllMonitors(createBrowserDeps(window, {
meter : otel.getMeter("lag"),
logger : createOtelLoggerAdapter(otel.getLogger("lag")),
events : createOtelEventSink(otel.getLogger("lag-events")),
worker,
workerHangReport : {
url : `${endpoint}/v1/logs`,
resource : { "service.name" : "shop", "service.instance.id" : serviceInstanceId },
},
pageContext : () => ({ "session.id" : otel.getSessionId() }),
}));
// Record the pending values of the monitors before each export
otel.onBeforeFlush(() => monitors.flush());
The code does these steps:
init()starts the OpenTelemetry SDK with a newservice.instance.id. Histograms use the exponential aggregation (native histograms in Mimir).createLagWorker()starts the Web Worker of the worker-lag monitor.createBrowserDeps(window, options)gets the browser APIs. It examines each API, thus each browser gets the monitors that it can support.setupAllMonitors()starts the monitors and gives their handles.onBeforeFlushmakes sure that the export at page hide contains the final values of the page view.
The worker sends its hang reports without the OpenTelemetry SDK, because the main thread cannot operate during a hang. workerHangReport.resource gives the reports the same service.instance.id as the page. pageContext gives them the session ID.
Warning
Connect monitors.flush() to the exporter. The otel-ts library flushes after the monitors recorded the end of the page, because both use the lifecycle tracker that the page shares. An exporter that listens to pagehide itself can flush before the monitors record their final values in Chromium. Refer to page lifecycle.
What the monitors need
setupAllMonitors() starts a monitor only when its dependencies exist:
| Dependency | Monitors |
|---|---|
Timers, document, window (always necessary) | Lifecycle, DriftLag, MacrotaskLag, timer throttle |
PerformanceObserver | Long animation frames, Event Timing, layout shift, page-view vitals |
requestAnimationFrame | Frame timing |
requestIdleCallback (not in Safari) | Idle availability |
MessageChannel and queueMicrotask | Scheduling fairness |
worker and performance | Worker lag |
worker and a cross-origin-isolated page | Shared-memory liveness |
performance and the wall clock | Clock reliability, clock drift |
createBrowserDeps finds each of these in the browser. To change a dependency, give your own object of type AllMonitorDeps to setupAllMonitors().
Send the metrics to a backend
Start the Grafana stack on your computer:
cd node_modules/@mark1russell7/grafana-infra
docker compose up -d
Then open Grafana at http://localhost:3000. The dashboards page tells you what each panel shows.
Stop the monitors
monitors.stop();
worker.terminate();
stop() stops each monitor in the reverse sequence of their start. It releases each timer, listener and observer. The worker is yours: stop it with worker.terminate().
Next steps
- Read measurement validity to know which samples the monitors discard, and why.
- Read the monitor overview to select the metrics for your dashboards.
- Open the playground to see the monitors with synthetic load.