Skip to the content

DriftLag

Measures main-thread lag: the time that other tasks block the main thread in each window of approximately 100 ms, with a timer baseline that the monitor calibrates.

DriftLag sets a chain of short timers and measures how late each window of approximately 100 ms ends. It subtracts the idle duration of each timer step, which it calibrates from the recent steps. The result is the time in the window in which other tasks blocked the main thread.

What it measures

The signal is the lag of one window: the duration of the window minus the idle duration of its timer steps. Each block of the main thread in the window adds its length to the lag.

DriftLag answers this question: how much of the last 100 ms was the main thread busy? It gives approximately 10 samples each second, thus it is the high-frequency lag signal of the library. It works in all browsers, also where Long Animation Frames are not available.

How it works

  1. The monitor sets a timeout of 5 ms (driftStepMs). When the callback starts, the monitor records the duration of the step and sets the next timeout.
  2. A window has at most 20 steps (100 ms divided by 5 ms), or up to 10 more steps during a possible change of the timer granularity. After the last step of the window, the monitor calculates the lag and starts the next window.
  3. The lag of the window is its duration minus the number of its steps multiplied by the baseline.
  4. The monitor calculates the number of steps for the next window again: 100 ms divided by the baseline, rounded, from 1 to 20. Thus a window stays near 100 ms.

The baseline is the idle duration of one step. A timer step takes longer than its requested delay, also on an idle thread. The cause is the timer granularity of the browser and the operating system. The monitor keeps the durations of the last 100 steps. The baseline is the mean of the steps that are not longer than the median plus max(4 ms, half of the median). Before the first step, the baseline is the requested delay (5 ms).

The monitor uses the mean, not the median. Timer jitter is skewed to the right. A first version used the median, and it gave an idle median lag of 3.3 ms in Chromium (experiment E3).

A long step is a block, not jitter. It is longer than the limit, thus it does not change the baseline.

A change of the timer granularity

The timer granularity can change while the page is open. For example, Windows can change the timer resolution of the browser on battery power, or when a window is hidden and shown again. Then all steps change by the same quantity, also on an idle thread. A block makes one step long, and the next steps are normal again. The monitor uses these rules:

  • It compares each step with the baseline at the end of the last window. A step is outside the baseline when it differs from it by more than max(4 ms, half of the baseline).
  • A row of 10 steps that are outside the baseline, but agree with each other, is a new granularity. The steps agree when the longest minus the shortest is not more than max(2 ms, 20% of the mean of the two).
  • At a new granularity, the baseline comes only from the steps of the row.
  • A window that ends during such a row continues until the row ends or the monitor accepts the new granularity. It continues for at most 10 more steps.
  • In the window of the change, the steps before the row keep the old baseline. The other steps get the new baseline. Thus the change gives no lag.
  • A step before the row that is outside the old baseline can have the new granularity. It can also be the step in which the granularity changed, or a block. Its idle duration is its duration, but not less than the shorter baseline and not more than the longer baseline. Thus no step before the row gives false lag. A block immediately before the row can lose up to the difference of the two baselines.
  • A step of more than 40 ms is not a granularity, thus it ends the row. Two ticks of the Windows timer (31.25 ms) and the 30 ms grid of WebKit in Low Power Mode are shorter.

The CDP tests found this case. In Chromium on Windows, the steps changed from 5.5 ms to 15.6 ms after the page was hidden and shown again. Before this rule, DriftLag reported windows of 160 ms of lag on an idle page (testing strategy).

A code review found a second error. The window of the change used the new baseline also for its steps before the row. A unit test changed the steps from 15.6 ms to 5.7 ms at 240 times in a window. On an idle thread, 115 of these times gave more than 20 ms of lag, and up to 49.5 ms. A change to a longer granularity gave a negative lag, thus it hid a block.

The warm-up after a start

Safari aligns only the timers of the nesting level 10 or more (refer to browser support). start() starts a new chain at the level 0, for example in the task of a visibilitychange event. Thus the first 10 steps after a start are not aligned, and they take 5 ms. The 11th step goes from a time that is not aligned to the next boundary. On the grid of 30 ms, it takes between 5 ms and 35 ms.

Thus the first 11 steps after each start are a warm-up. A warm-up step that is shorter than the baseline is not a step of the window. It is not a recent step, it is not in a row, and it gives no lag.

A longer warm-up step is a usual step, because the granularity can change while the monitor is stopped. For example, the steps of Chromium on Windows changed from 5.5 ms to 15.6 ms after the page was hidden and shown again. At the first start, the baseline is the requested delay, and no step is shorter.

A code review found this error with a test on the simulated thread. After a restart on the grid of 4 ms, the steps of 5 ms decreased the baseline. Then five windows had approximately 4 ms of lag each on an idle thread. On the grid of 30 ms, each step of 5 ms subtracted 30 ms. Thus only 25 ms of a 150 ms block showed, and the next 22 windows had approximately 9 ms of lag each.

A first version of the warm-up left out all of the 11 steps, also at the first start. The browser test lag-monitors.test.ts then failed in WebKit on Windows, with 4 browsers in parallel. The first window compared the steps of 15.6 ms after the warm-up with the requested delay of 5 ms, thus they were a row. A block ended the row, and the first window waited for 10 more steps. The test read the histogram before the window ended.

With that version, a restart could also give false lag. When the steps changed from 5.7 ms to 15.6 ms during the stop, a window could end before the row. Then each of the 11 steps got 9.9 ms of false lag.

A sustained load

A sustained load can also make all steps longer. On a thread that is busy with tasks of the same length, the steps agree with each other, thus they look like a new granularity. If the thread is busy during more than half of the steps, the median is a busy step, and the baseline increases. In the two cases, the monitor reports less lag. Experiment E5 measured this with a load of 100% for 4 s. The lag decreased to less than 15% in each engine, and to approximately 0 in three of six cases (local experiments).

Thus the monitor accepts a longer baseline only after a probe shows an idle thread. The probe (BusyTimeProbe) is a chain of message tasks during one timer step. Each message posts the next message. On an idle thread, a message starts at once. On a busy thread, it waits until the current task ends.

The busy time is the sum of the waits of more than 2 ms. A probed step is idle if the busy time is less than max(2 ms, half of the extra duration of the step).

A probe keeps the thread awake, thus it changes the timing. In Chromium, the probed step took 0.7 ms less than an idle step, and the next step took up to 3 ms more. Thus the probed step and the next step are not recent steps, and they are not in a row. The rules are in BaselineConfirmation and drift-baseline.ts:

  • A check. A check is an idle probe, 3 steps, and a second idle probe. The 3 steps must agree with each other. The step after the first probe is not one of the 3 steps.
  • The first confirmed value. After the first window, a check confirms the baseline. The first confirmed value is the recent baseline.
  • An increase. At the end of a window, a recent baseline that is more than max(1 ms, 10%) above the confirmed value starts a check. Until a check confirms the increase, the window uses the confirmed value. The new value is the lower of the recent baseline and the mean of the 3 steps.
  • The end of a load. If the 3 steps agree better with the confirmed value than with the recent baseline, the longer recent steps were load. The monitor removes them from the recent steps.
  • A decrease. A busy thread does not make a step shorter, thus a decrease needs no check. The confirmed value decreases to the highest recent baseline of the last 3 windows. Thus a short decrease does not change it.
  • A row of longer steps. After the first confirmed value, a row of 5 longer steps starts a probe of the next step. A row of longer steps is a new granularity only if the last probe during the row showed an idle thread. A window does not wait for a row that a probe showed as busy.
  • The wait for a row. The probed step and the step after it are not in the row, thus a row of longer steps needs 12 steps. A window that waits for a row does not count these two steps. Before this rule, a row that started at the last step of a window did not complete in the window. Then the window used the old baseline for 11 steps of 15.6 ms, and it had 109 ms of lag.

Before the first confirmed value, the monitor accepts a longer baseline without a probe, as before the change. The probe needs MessageChannel. Without it, the monitor always operates as before the change.

In the WebKit build of Playwright for Windows, a message waits for the next timer tick of the system after an idle period. Thus the probe finds no idle step there, and no check confirms a value. There, a probe on an idle page posted 278 messages or fewer in 100 ms. Chromium and Firefox posted more than 9,000.

The browser test drift-load.test.ts measured the result with a load of 100% for 3 s. The table gives the lag in the second half of the load, as a part of the time of the windows (8 October 2026). The rows of GitHub runners come from the logs of one CI run:

BrowserPlatformMessage tasks of 20 msTimer tasks of 30 ms, 4 ms apart
Chromium 145Windows 11, three runs80% to 86%78% to 85%
Chrome 153Windows 11, three runs80% to 86%79% to 85%
Firefox 146Windows 11, three runs24% to 29%53% to 72%
WebKit (Playwright)Windows 11The test skips itselfThe test skips itself
Chromium 145Linux, GitHub runner87%85%
Firefox 146Linux, GitHub runner75%84%
WebKit (Playwright)Linux, GitHub runner60%79%
Safari 26.6.2macOS, GitHub runner62%77%

Before the change, the lag of each case decreased to less than 15% within 4 s. In WebKit on Linux and in Safari on macOS, the probe found an idle page (0 ms of busy time). Only the WebKit build for Windows makes a message wait. In Firefox on Windows, an idle step already takes 15.6 ms. A step that waits for a task of 20 ms takes 20 ms, thus it gives only 4.4 ms of lag. This is the resolution of one step (refer to the limits).

Windows and records

A window has a whole number of steps. Thus its length is 100 ms only when the baseline divides 100 ms. In Firefox and WebKit on Windows, a window has 6 steps of 15.6 ms, thus approximately 93 ms. The first window has 20 steps, thus approximately 310 ms there. getLastWindowMs() gives the measured length of the last window, with its lag.

For each window, the factory gives the lag and the measured window length to the sample validator. For a valid sample, the factory records the lag in lag_drift_histogram and the current baseline in lag_drift_baseline_histogram. A negative lag is jitter around the baseline, thus the factory records it as 0.

LagLogger gets each lag that the validator records, with its interval: the window length minus the lag. The logger examines the most recent 2 s and 5 s of measurements, by time. The lag of a period is the sum of the lags divided by the sum of the intervals. A period goes above its threshold at more than 100% (2 s) or more than 50% (5 s). After each 30 s of measurement intervals, it logs a warning for each period that went above its threshold.

Browser support

DriftLag uses only setTimeout, thus it starts in all browsers. The timer granularity is different in each engine. We measured it on an idle page on Windows 11, with the engines of Playwright (experiments E2 and E3):

MeasurementChromium 145Firefox 146WebKit (Safari 26.0 build)
Median step of setTimeout(5)5.7 ms16 ms15 ms
Baseline step after calibration5.7 ms15.5 ms15.6 ms
Steps in a window after calibration1866
Old probe (20 steps, lag = elapsed − 100 ms), idle median lag15.8 ms211 ms211 ms
Calibrated DriftLag, idle median lag (approximately 30 windows)0.1 ms−0.3 ms−0.4 ms
Calibrated DriftLag, idle p90 lag3.1 ms1.3 ms1.6 ms
Calibrated DriftLag, lag after a 300 ms block297.4 ms282.7 ms294.6 ms

The number of steps in a window comes from the rule of step 4 and the measured baselines. Firefox and WebKit on Windows use the 15.6 ms timer tick of the system for a 5 ms timeout. Chromium asks for a 1 ms tick (experiment E2).

Other platforms have other granularities (clocks and timers):

  • Safari aligns each one-shot timer of the nesting level 10 or more to the next boundary of a grid with a random offset (DOMTimer.cpp). The grid is 4 ms on a visible page, and 30 ms in Low Power Mode or with thermal mitigation. The chain of DriftLag is such a chain. A step of 5 ms ends at the next boundary after 5 ms, thus a step takes 8 ms, or 30 ms in Low Power Mode. The first 11 steps after a start are a warm-up (the warm-up after a start).
  • Chromium on Windows uses a timer resolution of 1 ms on AC power and 8 ms on battery power.

The calibration measures these granularities too. A CI job operates drift-accuracy.test.ts in Safari 26.6.2 on a GitHub macOS runner, and the test passes there with a baseline of 8.4 ms. Safari 26.5 in the iOS Simulator also gave 8.4 ms. The WebKit build for Windows is not Safari on macOS.

A GitHub macOS runner cannot turn on Low Power Mode: sudo pmset -a lowpowermode 1 gives "LowPowerMode not supported on AC Power". Thus the unit test DriftLag.power.test.ts uses the rule of the source on a simulated thread. With the grid of 4 ms, the baseline is 8 ms, as in the CI job. With the grid of 30 ms, DriftLag accepts steps of 30 ms as a granularity, and the idle lag stays near 0. A block of 300 ms then gives more than 260 ms of lag. Nobody tested DriftLag on a device in Low Power Mode.

Measurement validity

With measurement conditions, DriftLag pauses while the page is hidden or frozen. Browsers throttle the timers of hidden pages, thus a window there measures the throttling. When the page is visible again, the first window starts at the restart, and the monitor keeps it. The first 11 steps after the restart are a warm-up.

Each window goes to a SampleValidator with its measured length (getLastWindowMs()). The validator discards a window that overlaps a hidden, frozen or suspend interval. A discarded window with a lag of 5000 ms or more that overlaps a suspend interval is a stall of the type suspend. A lag of 5000 ms or more waits 2000 ms for late evidence:

  • If the worker or the clock-drift monitor reports a suspend in that time, the validator discards the window. It is a stall of the type suspend.
  • If no evidence comes, the validator records the window. It is a stall of the type hang.

The conditions of setupAllMonitors() count the discarded windows in lag_samples_discarded. They join the stall samples of all monitors whose windows overlap into one stall episode, and they count the episodes in lag_stalls.

The validator reads document.visibilityState again for each window. Thus it also discards a window that ends after the page became hidden, but before the visibilitychange event. Refer to measurement validity.

Metrics

MetricKindUnitAttributesDescription
lag_drift_histogramHistogrammsNoneThe lag of one window (approximately 100 ms) of chained timeouts: its duration minus the idle duration of its steps. Each block of the main thread in the window adds to the lag.
lag_drift_baseline_histogramHistogrammsNoneThe idle duration of one timer step: the mean of the recent steps that are not blocks. An increase needs a probe that shows an idle thread. It is the timer granularity of the browser and the operating system. DriftLag subtracts it.

DriftLag sends no events of its own. The measurement conditions send a lag.stall event for each stall episode, with the attributes kind (hang or suspend) and duration_ms. LagLogger writes its warning ("Average event loop lag exceeded threshold") to the logger, not to the event sink.

Configuration

function createInstrumentedDriftLag(
    deps : CoreDeps & TimerDeps & Partial<Pick<SchedulingDeps, "MessageChannel">>,
    conditions? : MeasurementConditions,
) : MonitorHandle<DriftLag>;

The factory uses these dependency groups:

  • CoreDeps: logger, clock (performance.now()) and meter.
  • TimerDeps: setTimeoutFn, clearTimeoutFn, setIntervalFn and clearIntervalFn.
  • MessageChannel (optional): the message tasks of the probe. The factory makes one queue on it, and it closes the queue when it stops. Without it, the monitor has no probe.

Give conditions to pause the monitor while the page is hidden and to validate each window. The factory uses a window of 100 ms (highFrequencyLagIntervalMs) and a step of 5 ms. The class has three options:

OptionDefaultWhat it does
stepMs5The requested delay of each timer step.
baselineSteps100The number of recent steps that give the baseline.
postTaskNoneA function that starts a callback in a new task, for example createMessageTaskQueue(...).post. With it, a probe must show an idle thread before the baseline increases.
import { metrics } from "@opentelemetry/api";
import {
    createBrowserDeps,
    createInstrumentedDriftLag,
    createInstrumentedLifecycle,
    createMeasurementConditions,
} from "@mark1russell7/lag";

const deps = createBrowserDeps(window, { logger : console, meter : metrics.getMeter("lag") });
const lifecycle = createInstrumentedLifecycle(deps);
const conditions = createMeasurementConditions({
    clock : deps.clock,
    setTimeoutFn : deps.setTimeoutFn,
    clearTimeoutFn : deps.clearTimeoutFn,
    ...(lifecycle.monitor ? { lifecycle : lifecycle.monitor } : {}),
});
const driftLag = createInstrumentedDriftLag(deps, conditions);

// Stop in the reverse sequence of the start
driftLag.stop();
conditions.dispose();
lifecycle.stop();

setupAllMonitors() does these steps for you. In a cross-origin-isolated page with a worker, it also gives DriftLag a setTimeoutFn that beats the counter of the shared-memory liveness watcher.

Cost

  • Timers: one chain of timeouts. At the measured idle steps, the chain has approximately 175 callbacks each second in Chromium (5.7 ms). In Firefox and WebKit on Windows (15.6 ms), it has approximately 64. These are estimates, calculated from experiment E3.
  • Records: two histogram values for each valid window, thus approximately 20 values each second.
  • CPU: the overhead benchmark measured approximately 11 ms of main-thread time each second for the chain alone. It used Chromium in the new headless mode on a desktop computer (the comment of overhead/overhead.test.ts).
  • Probes: a check is two probes. A probe is a chain of message tasks during one step: approximately 6 ms in Chromium and 16 ms in Firefox on Windows. On an idle page, the monitor does one check after the first window, and then only when the recent baseline increases. In the overhead benchmark, the monitors posted 0.6 messages each second during the measurement, thus no probe operated then. During a load, each window starts a check. A probe on a busy thread posts few messages, because each message waits for a task.
  • Memory: the durations of 100 steps. Each calculation of the baseline sorts them, three times for each window.
  • Power: short timers keep a fine timer resolution of the operating system. While short timers wait, Chromium on Windows asks for a 1 ms tick on AC power, and 8 ms on battery power. A published measurement gives approximately 0.3 W for a 1 ms tick, approximately 10% of the idle power of the processor package (clocks and timers). We did not measure this for DriftLag.

The callbacks of the monitor are also tasks on the main thread. Thus the monitor measures a small part of its own cost.

Limits

  • Before the first check, and in some browsers. Before a check confirms the first value, a sustained load increases the baseline, and the monitor reports less lag. This is also true without MessageChannel. It is also true in a browser in which a message waits on an idle thread, for example the WebKit build of Playwright for Windows. The worker monitor measures that case correctly.

    The cause of the wait is in the Windows port of WebKit. Its event loop schedules each task with a timer of 0 ms, and on Windows that timer can wait for the system timer tick (local experiments). No major browser uses that port, thus the library does not work around it.

  • A load at the start. The first confirmed value is the recent baseline. After a load at the start of the page, it can be too long. It decreases with the recent baseline in 100 steps or less.

  • A small increase of the granularity. An increase of less than max(4 ms, half of the baseline) starts no row. The recent baseline follows it after approximately 50 steps, and then a check confirms it. Until then, each window has a small lag.

  • A light load. A probed step is idle if the thread was busy for less than max(2 ms, half of the extra duration of the step). Thus the steps of a light load can be in a confirmed baseline.

  • A probe delays other work. During one step, a message task starts each few microseconds. An idle callback cannot start in that step.

  • Firefox after a load. In Firefox on Windows, the steps changed between 8 ms and 16 ms for some seconds after a load (experiment E5). The lag of each window was between −9 ms and 8 ms in that time.

  • The resolution is one step. A block can start immediately after a step. Thus the lag of a block can be less than its duration by up to one step: 16 ms in Firefox on Windows (experiment E3). In Safari without focus, some steps are late, thus the baseline is longer than the usual step. Then the other steps of a window decrease its lag a little. One time on a CI runner, the decrease was 12.4 ms in a window with a block of 300 ms.

  • One sample for each block. The chain is closed-loop: a long block gives one long window, not many samples. In a histogram of windows, a 30 s block is one value among hundreds of normal windows (clocks and timers, coordinated omission). The sum of the histogram contains the full length of the block. Thus use the sum for the blocked time, and the quantiles only for the typical window.

  • Idle noise. The factory records a negative lag as 0. Thus an idle page gives small positive values, for example a p90 of 3.1 ms in Chromium (experiment E3).

  • A change of the timer granularity. A row of 10 steps that agree with each other starts a new baseline. Thus the baseline follows a change, for example to battery power, after 10 steps of the new granularity: approximately 160 ms with steps of 16 ms. The step in which the granularity changes can be in the jitter limit of the old baseline. Then it gives up to max(4 ms, half of the old baseline) of lag. A block immediately before the row can lose up to the difference of the two baselines.

  • The first start in Safari. At the first start, the baseline is the requested delay. Thus the 10 steps that Safari does not align are recent steps. On the simulated grid of 4 ms, the first windows had up to 18 ms of lag for approximately 0.8 s.

  • No samples from hidden pages. The monitor pauses there, and so does the worker monitor. The timer throttling detector and the clock drift monitor continue in hidden pages.

Tests

Unit tests (Vitest, fake timers):

  • DriftLag.test.ts, the calibration: no lag with exact timers. No lag with 16 ms steps, and a baseline of 16 ms. A window of 20 steps, then of 6 steps of 16 ms. A 300 ms block gives a lag of 300 ms. A long step does not change the baseline.
  • DriftLag.test.ts, the timer chain: one chain after stop() and start(), also from inside the report function. A window of 103 ms has 20 steps of 5 ms.
  • DriftLag.granularity.test.ts, a change of the granularity: steps that change from 6 ms to 16 ms on an idle thread give a lag of less than 20 ms. The baseline follows the change, also when the change comes at the end of a window. In the window of a change to a finer granularity, the steps before the row keep the old baseline. After that change, a 300 ms block gives a lag of 300 ms. A row of steps of more than 40 ms, and a row of steps that do not agree, stay lag.
  • DriftLag.granularity.test.ts, the baseline: the mean of the steps that are not longer than the median plus max(4 ms, half of the median). A block does not count, also when it comes before shorter steps.
  • drift-baseline.test.ts: the rules of the baseline at their limits. An increase of max(1 ms, 10%) needs no check. A probed step is idle below max(2 ms, half of its extra duration). The findings of a check, and the steps that it removes. The value of a step before an accepted row that is outside the old baseline stays between the two baselines.
  • BaselineConfirmation.test.ts: a check of an idle probe, 3 steps and a second idle probe. The first value, the decrease to the highest recent baseline of 3 windows, a busy closing probe and a probe of a row.
  • BusyTimeProbe.test.ts: no busy time on an idle thread. Each task delays one message. A wait of 2 ms or less does not count, and a message of an earlier measurement does nothing.
  • DriftLag.load.test.ts uses a simulated thread with a timer queue and a message queue. Equal message tasks of 20 ms and timer tasks of 30 ms stay lag for 5 s, also with steps of 15.6 ms. Without a probe, the same load gives less than 5% lag. After the load, there is no lag.
  • DriftLag.load.test.ts also simulates three behaviors of browsers. In Chromium, a probed step is shorter and the next step is longer. In WebKit for Windows, a message waits also on an idle thread.
  • DriftLag.load.test.ts, the timer tick of Windows (steps of 15.6 ms): a block of 300 ms in the first window gives its duration minus up to one step. The first window has 20 steps. A restart can change the steps from 5.7 ms to 15.6 ms. Then a block of 150 ms after the restart gives its duration minus up to one step.
  • DriftLag.load.test.ts, a change of the granularity at 240 times in a window. The steps change from 15.6 ms to 5.7 ms and from 5.7 ms to 15.6 ms, with and without a probe. Each change gives less than 1 ms of lag on an idle thread. A block of 100 ms before a change to 15.6 ms gives more than 83 ms of lag.
  • DriftLag.power.test.ts: the timer alignment of WebKit on the simulated thread, with three offsets of the grid. A grid of 4 ms gives a baseline of 8 ms. A grid of 30 ms (Low Power Mode) gives a baseline of 30 ms, an idle lag near 0, and the lag of a 300 ms block. A change into Low Power Mode and out of it. A change out of Low Power Mode at 120 times in a window gives less than 2 ms of lag. A change into it gives not more than the jitter limit of 4 ms.
  • DriftLag.power.test.ts, the warm-up: after a restart, no window has more lag than the extra duration of the 11th step. A block of 150 ms during the warm-up after a restart gives its duration minus the baseline.
  • LagLogger.test.ts: the thresholds of the 2 s and 5 s periods, the reset after each report, and no log for a hidden tab. The logger uses the interval of each measurement, for example windows of 93 ms.
  • setup-all-monitors.test.ts: a 250 ms lag goes into lag_drift_histogram. The validator discards a window that overlaps a system suspend. The monitor pauses while the page is hidden, and it keeps the first window after the page is visible again.

Browser tests (Vitest browser mode with Playwright, in Chromium, Firefox, WebKit and Chrome, unless an item names other browsers):

  • drift-accuracy.test.ts: on an idle page, the median lag is between −5 ms and 5 ms. In a window without focus, the limit is 10 ms. On macOS, the timers of an app that is not in front can be late. After a 300 ms block, the largest lag is more than 300 ms minus the baseline minus 10 ms (20 ms without focus), and less than 340 ms.
  • drift-load.test.ts: equal message tasks of 20 ms and timer tasks of 30 ms for 3 s give more than 10% lag in the second half of the load. After the load, the median lag of a window is less than the idle median lag before the load plus 10% of a window. A negative idle median counts as 0. The test skips itself if a probe on an idle page posts fewer than 400 messages in 100 ms (the WebKit build for Windows: 122 to 278). The other WebKit builds post 580 to 920 messages in CI, and the test operates there.
  • lag-monitors.test.ts: a 300 ms block gives a lag of more than 200 ms. The test waits up to 2 s for the window of the block, because the first window of a page can be long.
  • stress.test.ts: with the light profile of @lag/load, the largest lag is less than 150 ms. With the heavy profile, it is more than 100 ms.
  • cdp/visibility.test.ts and cdp/freeze.test.ts (Chromium in the new headless mode): the monitor records no window while the page is hidden or frozen. It records more than 5 windows after the page is visible again. After a freeze of 2 s, each lag after the resume is less than half of the freeze, and no stall occurs.
  • cdp/cpu-throttling.test.ts (Chromium in the new headless mode): with 4× CPU throttling, the same work gives a median lag of more than 2.5 times the lag without throttling.
  • overhead/overhead.test.ts (Chromium in the new headless mode): all monitors together make more than 40 and not more than 210 timer callbacks each second on an idle page. The lower limit is also true with the 15.6 ms timer tick of Windows. The comment of the test gives approximately 175 callbacks each second for DriftLag with a 1 ms tick.

Source

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.