Performance work that users can actually feel

A timer can stop while the interface still says Loading. Reproduce that boundary error, preserve application failures, and choose what completion means before comparing latency.
Data arrival is not the end of a search
For a search screen, I recommend naming the moment the user is waiting for: results committed to the interface, with a usable next action. A successful response is an earlier event. If a timer stops there, moving expensive work into rendering can make the metric look better without making the search feel faster.
The downloadable browser fixture makes that distinction observable. Both paths receive two synthetic results. The data-only path records while the result element still says Loading. The full path waits for an explicit commit gate, updates the element, and records when its text is 2 results. Neither observation is a screen-presentation timestamp or evidence of a production speedup.
Keep the timer out of the application result
For a duration-only metric, I prefer subtracting two performance.now() timestamps. That API uses a monotonic clock within its time origin. The helper below creates no named User Timing entries, so it has no shared timeline entries to clean up. The tradeoff is that it does not add a named measure to a browser performance trace.
The operation is awaited inside try/finally. A returned value sets the outcome to ok. A thrown error or rejected promise leaves it as error. The finally block records either outcome without returning a replacement value. Timer and collector exceptions are contained, and a rejected collector promise is handled without waiting for delivery.
export async function measureFlow(operation, record, clock = performance) {
let start;
try {
start = clock.now();
} catch {
// A missing timer must not prevent the operation.
}
let outcome = "error";
try {
const result = await operation();
outcome = "ok";
return result;
} finally {
try {
const durationMs = clock.now() - start;
if (Number.isFinite(durationMs) && durationMs >= 0) {
// Do not wait for telemetry delivery; handle rejected collectors.
Promise.resolve(record({ durationMs, outcome })).catch(() => {});
}
} catch {
// Timer and collector failures must not replace the result or error.
}
}
}
Test the instrumentation before trusting its numbers
The pinned Node run passes seventeen checks, including returning the same object, preserving the original synchronous error and rejected error, and surviving throwing or rejected collectors. A collector that never settles does not hold the operation open. Three negative controls independently break outcome labeling, duration subtraction and detached collection. The harness detects each defect.
The overlap test starts one operation at a synthetic time of 10 and another at 20. The second finishes at 35 and records 15. The first finishes at 50 and records 40. These are chosen test-clock values, not measured application latencies. They verify that each call keeps its own start value even when completion order reverses.
The original timer used the shared name product-flow and cleared that name after recording. The reproduction creates an unrelated entry with that name and confirms that the old helper removes it. A separate injected cleanup exception replaces its successful result. That exception is a fault-injection test, not an observed native browser defect. The replacement avoids timeline cleanup entirely.
The guarantee is limited to returned values and rejection reasons under these checks. The wrapper still adds execution overhead and returns its own promise. A slow synchronous collector still blocks the event loop. I recommend making the collector a short enqueue operation and handling persistence separately. Detached telemetry can be lost when the page closes.
Choose a completion signal you can explain
The browser harness imports the same module and runs the shared checks. Its synthetic data and commit gates let the test pause between receiving rows and changing the DOM. Before the commit gate opens, the full-flow collector has no event. After the update, its event observes 2 results. The data-only variant records Loading, exposing the early measurement boundary without relying on arbitrary sleep durations.
This fixture changes DOM text directly. It does not exercise a framework scheduler, real fetch, image decoding or paint. In a framework application, I recommend using the relevant post-commit signal and label the metric commit latency. A browser trace needs separate inspection before calling this visible presentation time. An export needs a different endpoint: an authorized download ready for use, not merely an accepted job.
Use the result to choose an investigation
Core Web Vitals answer related questions: LCP measures loading, INP responsiveness and CLS visual stability. Google recommends evaluating them at the 75th percentile, with mobile and desktop separated. A custom search or export duration adds evidence about a particular workflow. Core Web Vitals do not describe every asynchronous business workflow.
I recommend comparing the same flow across matched device classes and representative data sizes, keeping deployment version, failure rate and sample count beside the duration distribution. A release that rejects difficult requests quickly can improve successful-request latency while making the product worse. A timer record without that context is insufficient evidence for a performance claim.
I recommend locating work within the interval before proposing a fix. When data arrives promptly but completion stays late, inspect main-thread work and rendering. When data arrival itself is late, inspect the request and its server dependencies. Change one suspected cause, then compare the same completion boundary. Renaming a boundary or excluding failures does not demonstrate an improvement.
Reproduce the boundary, then collect real evidence
From the extracted download root, run python3 frontend/scripts/verify-performance-flow.py with Docker running. For the browser checks, follow the included README to install locked Playwright dependencies and run verify-performance-browser.mjs. Both commands produce fresh evidence. The JSON already in the archive describes an earlier run.
The fixture is a local demonstration with synthetic results. Its deterministic checks establish timer behavior, and its browser checks establish when the DOM changed relative to recording. It does not measure a customer workload, reproduce production network conditions, establish a performance budget or prove a user-experience improvement.
I recommend stable metric names chosen by the application and a reviewed telemetry pipeline. Do not put search text or account identifiers into a timing label. This helper records only duration and outcome. Aggregation, sampling, consent, storage, delivery guarantees and deployment comparison are outside its scope.
Reproduce the examples
Duration-only timer, deterministic Node checks, original-defect reproduction and browser DOM-boundary tests. Includes the pinned runtime and standalone reproduction instructions.
Download the source and tests (.zip)
