Catch the user interaction that blocks the main thread — before it ships.
When a click, tap, or form submit runs synchronous JavaScript that holds the main thread, the browser cannot respond — the interaction feels frozen and your INP regresses. loopwatch measures that blocking work during a real user action and turns it into a failing test, so the regression breaks the build instead of reaching users.
Zero runtime dependencies. ESM-only. Built for Playwright CI first.
A Playwright test that fails when an interaction blocks the main thread:
import { test as base } from "@playwright/test";
import { loopwatchFixture, assertHealthy } from "@irisfield/loopwatch-playwright";
const test = base.extend(loopwatchFixture);
test("checkout submit stays responsive", async ({ page, loop }) => {
await page.goto("https://your-app.com/checkout");
const m = await loop.measure(page, async () => {
await page.click("#submit-order");
});
assertHealthy(m, {
maxP99: 30, // tail input-handling lag must stay under 30ms
maxBlockedMs: 0, // no blocking spike >= 50ms
maxLongTasks: 0, // no task may hold the thread for >= 50ms
});
});When the interaction blocks the thread, the build fails by naming what to fix — the metric, the measured value, the limit it broke, and the worst blocking window with its culprit:
Loop health assertion failed:
- lag.p99 142.3ms exceeds limit 30ms
- longTasks.count 3 exceeds limit 0
- lag.blockedTimeMs 142.3ms exceeds limit 0ms
Worst blocking window: 142ms blocked at t=218ms (encryptPayload in checkout.js)
| Package | What it's for |
|---|---|
loopwatch-playwright |
Start here. Fail CI when a user interaction blocks the main thread. The flagship. |
loopwatch |
The measurement engine — measureLoopLag, assertHealthy, LoopMonitor. Use directly for in-browser monitoring and one-off measurements. |
loopwatch-react |
React hooks for ambient loop-health state and scoped measurement. A convenience layer, not the enforcement story. |
loopwatch owns input delay: the gap between a user interaction and the moment the browser can begin running your event handlers, caused by synchronous JavaScript holding the main thread. This is one of the three components of INP.
It does not measure event-handler execution time (processing) or rendering (presentation delay), so it does not claim to fix all of INP. It is not a production monitor.
| If you need... | Use |
|---|---|
| To fail a PR when an interaction blocks the main thread | loopwatch |
| Post-deploy monitoring and Core Web Vitals dashboards | Sentry / Datadog |
| To find unnecessary React re-renders | React Scan |
bunx jsr add @irisfield/loopwatch-playwright @irisfield/loopwatch
See each package's README for full API reference, threshold guidance, and setup.
MIT