Skip to the content

Research

The research behind the design of the library, from browser support and clocks to experiments, Web Vitals, RUM products, OpenTelemetry and the writing standard.

These notes collect the facts that the design of @mark1russell7/lag depends on. The research read specifications, the source code of engines and vendors, and documentation on 2026-10-07. We also measured some facts in this repository. Each note names its sources with links, and the sources page lists all of them. A note marks each fact that the research did not verify, and each inference of the research.

The purpose of the research

A monitor of main-thread lag uses browser APIs that behave differently in each engine and each operating system. The documentation of these APIs is frequently older than the code of the engines. Thus the research has three purposes:

  • Give the support and the limits of each API on a known date, with the source of each fact.
  • Find the facts that make a measurement incorrect, for example the clocks, the timer policy and the page lifecycle.
  • Compare the design of the library with the design of the other tools that measure real users.

How the research changed the library

FindingChange in the libraryNote
On an idle page, a chain of 5 ms timers measures the timer granularity of the browser and the operating system.DriftLag subtracts a calibrated baseline of the recent steps.Local experiments
A sustained load of equal tasks makes all timer steps longer, thus it looks like a new timer granularity.A probe of message tasks must show an idle thread before the baseline of DriftLag increases.Local experiments (E5)
A setTimeout(0) from a timer callback gets the clamp of nested timers.MacrotaskLag and SchedulingFairness start each measurement in a message task.Clocks and timers
Safari calculates timeOrigin again at each read, and Chromium gives a worker a constant clock offset.createAbsoluteClock reads the origin one time, and WorkerClockSync measures the offset.Clocks and timers
performance.now() stops during sleep, except on Windows, and no web event shows a suspend.ClockDriftMonitor classifies suspends and steps. A suspend of the clock-drift monitor and a late worker timer each add a suspend interval.Clocks and timers
Hidden and frozen pages throttle or stop timers, rAF and idle callbacks.The probes pause, and the measurement conditions discard the samples of these intervals.Browser support
A monitor on the main thread sees a block only after it ends. A tab that closes during a hang does not report it.A worker sends heartbeats, detects hangs, reports them, and keeps a hang journal in IndexedDB.Real user monitoring
In WebKit and Safari, a dedicated, a shared and a service worker complete their IndexedDB writes and their fetches only when the main thread operates.No change in the code. A hang journal in a service worker is no way out, because its writes also wait. The pages give the limit, and a browser test fails if an engine changes.Local experiments (E4)
In WebKit and Safari, also OPFS, the Cache API, XMLHttpRequest, WebSocket and BroadcastChannel of a worker wait for the main thread. In Firefox, XMLHttpRequest and WebSocket of a worker wait.A browser test keeps the behavior of each engine. No API of a worker can keep a hang in WebKit.Local experiments (E6)
Another page of the origin operates during a hang. The Web Lock of a hung page stays held, and the browser releases it when the page ends. Firefox and Safari on macOS let a closed hung page operate again before it closes.The peer hang watch: another page reports a page that the browser stopped during a hang, and a page that closes at the end of a hang reports the hang itself.Local experiments (E7)
Many browsers without a writer identity break Mimir, and neither delta temporality nor a collector fixes it.otel-ts gives each SDK instance a random service.instance.id and cumulative temporality. The library makes only counters and histograms.OpenTelemetry metrics in the browser
In Chromium, the order of the pagehide listeners cannot protect the final values.AllMonitorHandles.flush() records the final values in the onBeforeFlush hook of otel-ts.Local experiments
The web-vitals library gives the rules of the Core Web Vitals.PageViewVitals uses the same rules for each page view.Web Vitals algorithms
A controlled language gives rules that a person and a tool can check.The site uses ASD-STE100 as a style target, with the checker @mark1russell7/ste-lint.Writing standard

The notes

  • Browser support: all three engines report INP and LCP, but LoAF, layout shifts, soft navigations and the Page Lifecycle events exist only in Chromium.
  • Clocks and timers: performance.now() stops during sleep except on Windows, and timer probes measure the timer policy of the system, not only the load.
  • Local experiments: on an idle page, the calibration decreased the lag of DriftLag from 15.8 ms to 211 ms down to less than 0.5 ms. In WebKit and Safari, the IndexedDB writes and the fetches of a worker wait while the main thread is blocked, also in a service worker.
  • Web Vitals algorithms: PageViewVitals uses the rules of web-vitals 6.2.3, with four main differences that the note explains.
  • Real user monitoring: all surveyed RUM products record events first and watch the main thread from the main thread, without a worker.
  • OpenTelemetry metrics in the browser: without a random service.instance.id for each SDK instance, many browsers write one series and break the metrics in Mimir.
  • Writing standard: the site uses ASD-STE100 Issue 9 as a style target, and its automated check is only an approximation.
  • Sources: one bibliography gives all sources of the notes, grouped by topic, with their dates.

Add a note

Do these steps to add a research note:

  1. Add an .mdx file in packages/site/content/research. The path of the file is the path of the page.
  2. Add the frontmatter: title, description (one sentence), order (a number) and status.
  3. Start with a short summary. Then use headings of level 2 for the sections.
  4. Write the text in the style of the writing standard.
  5. Name each source in the text, with a link.
  6. Add each source to the sources page, with its date.
  7. Write "not verified" for each fact that you did not verify, and "inference" for each inference.
  8. Set status: draft until each fact is verified. Then set status: final.
  9. Put the data of a component in a *.data.ts file next to the page, and import it in the page.
  10. Add the note to the list on this page, with one sentence on its main finding.
  11. In packages/site, start npx tsc -p tsconfig.json and npx vitest run --project node. Correct each error.
  12. At the root of the repository, start pnpm lint:ste with the path of the note.

lag: Main-thread responsiveness monitoring for browser apps, exported as OpenTelemetry metrics.

To change a page, edit its file in packages/site/content/. The writing style guide tells you how.

An AI model (Claude, from Anthropic) wrote most of the text and the code of this site and of the library, under the direction of the author. The tests and the STE linter examine them. The writing standard gives the reason for this note.