Browser · beginner

How to verify a browser long-task performance fix

This guide explains how to verify that a browser long-task performance fix has actually removed an interaction from waiting behind a long main-thread task. It focuses on measurement before and after the change, using browser Performance APIs and the Long Tasks API, so engineers can confirm an observable improvement rather than rely on assumed gains.

The symptoms

  • After deploying a change intended to shorten a main-thread task, user-reported jank or input delay appears unchanged on the same interaction.
  • Interaction-to-next-paint (INP) values stay above the same threshold even though the targeted function now uses yield, requestIdleCallback, or a worker.
  • The Long Tasks API continues to report entries longer than 50 ms on the same interaction path the fix was supposed to address.
  • Field metrics for the page improve in aggregate but the specific interaction the fix targeted still blocks on the main thread.
  • Performance traces show the interaction handler finishing before the long task ends, suggesting the fix moved work but did not remove the blocking window.

Likely causes

  • The fix addressed a different long task than the one the interaction actually waits on, so the blocking window persists.
  • Yielding was added inside the targeted function, but a synchronous dependency above or below it still spans the 50 ms long-task boundary.
  • Work was moved to a worker or idle callback, but the main thread still has to await a synchronous reply before the next paint.
  • The measurement changed (different machine, profile, throttling setting) so before/after numbers are not directly comparable.
  • The fix was applied behind a feature flag or in a code path that the verification trace does not exercise.

First ten minutes

  1. 01Restate the failure mode: the interaction still waits behind a targeted main-thread task, so any fix that does not shrink or relocate that task cannot be declared successful yet.
  2. 02Reproduce the exact interaction in a controlled browser profile, recording a Performance trace and a Long Tasks snapshot at the same throttling setting used for the original report.
  3. 03Identify the specific long task entry the interaction waits on: its start time, duration, container type (script, layout, paint, system), and which frame it runs in.
  4. 04Capture baseline numbers (entry count, p95 duration, INP for the interaction) before toggling the fix, so the comparison is apples to apples.
  5. 05Decide the verification target: a reduction in the targeted entry's duration below the long-task threshold, or its absence from the trace on the same interaction path.

Evidence to collect

  • PerformanceObserver entries of type "longtask" filtered to the interaction's time window, with startTime, duration, and entryType recorded.
  • A Performance trace (Event Timing, Performance Timeline, or devtools timeline) showing the interaction event, its presentation timestamp, and intervening main-thread tasks.
  • Interaction-to-next-paint values for the specific interaction, both before and after the fix, under identical CPU and network throttling.
  • The source location (file and function name) of the top script task on the main thread during the blocking window.
  • Confirmation of which build or flag was active when each trace was recorded, so the comparison is not across different code paths.

Where to look

  • The Performance interface and its observer types: PerformanceObserver, PerformanceEntry, and the "longtask" entry type exposed by the Long Tasks API.
  • Event Timing entries (PerformanceEventTiming) on the same window, especially interactionId and processingStart-to-processingEnd timing.
  • DevTools Performance panel's "Main" thread track, filtering for Scripting, Rendering, and Painting rectangles that span the interaction's processingStart.
  • The frame the interaction occurs in (top frame versus iframe), since cross-frame waits change which main-thread task the interaction actually blocks on.
  • The function or module the fix was applied to, to confirm the deployed bundle includes the change and the verification trace exercises it.

Diagnostic steps

  1. 01Compare longtask entry counts and p95 duration between the baseline and post-fix traces using the same throttling profile; the targeted entry should be absent or shortened.
  2. 02Inspect the main-thread track between processingStart of the interaction and its presentation: the fix is real only if no Scripting rectangle longer than 50 ms remains in that window.
  3. 03Cross-reference the top scripting task with the fix's source file: if the top function is not in the touched module, the fix did not remove the blocking work.
  4. 04Re-measure INP for the targeted interaction only, not the aggregate page INP, since a page-level improvement can hide an interaction-level regression.
  5. 05Test the interaction under both throttled and unthrottled CPU to ensure the fix holds at the slower profile where long tasks are most visible.
  6. 06Confirm the build under test contains the fix by checking the bundle's source map or the flag's effective state for the test session.

Common mistakes

  • Declaring success because aggregate INP improved, without isolating the specific interaction that was supposed to stop waiting on the long task.
  • Comparing traces taken on different machines, browser versions, or throttling settings, which shifts the 50 ms threshold and invalidates the before/after delta.
  • Reading the Long Tasks API as "tasks longer than 50 ms" but ignoring that some browsers exclude tasks from cross-origin frames, so the fix appears effective while the iframe still blocks.
  • Assuming that moving work off the main thread (worker, idle callback, scheduler.postTask) removes the wait, when the main thread still synchronously awaits a result before the next paint.
  • Verifying only the dev environment, where bundle minification, third-party scripts, and feature flags differ from production, and the long task comes from a different source.

Safe fixes

  • If the targeted entry is gone from the post-fix trace and the interaction's processing-to-presentation window contains no Scripting rectangle longer than 50 ms, the fix is verified for this interaction.
  • If the entry is still present but shorter than the long-task threshold and INP for the interaction drops, the fix is verified as a partial mitigation; record the residual duration.
  • If the entry is gone but a different entry now spans the same window at the same duration, the fix relocated blocking work; do not declare success until that new entry is identified and addressed.
  • If verification is inconclusive, freeze the build and the throttling profile, re-run the same scenario, and re-collect longtask and event-timing entries before drawing conclusions.
  • If the deployed bundle does not contain the fix, roll forward the correct build and repeat the trace rather than attributing any change to the wrong code path.

Prove the fix

  1. 01A post-fix PerformanceObserver longtask snapshot for the same interaction window shows zero entries whose duration exceeds 50 ms on the main thread, or shows entries strictly shorter than the pre-fix baseline by an amount greater than measurement noise.
  2. 02The post-fix Performance trace for the interaction shows processingEnd-to-presentation with no main-thread Scripting rectangle longer than 50 ms between them.
  3. 03The interaction's INP value, measured under the same CPU throttling profile used in the baseline, is below the original value and below the long-task threshold for that profile.
  4. 04The top scripting task on the main thread during the interaction's processing window is no longer attributed to the function the fix targeted, and the function's new location (worker, idle callback, or removed path) is confirmed.
  5. 05Two independent traces taken on separate runs reproduce the same result, ruling out a single-sample anomaly.

Prevention and next steps

  • Record the exact CPU and network throttling profile used for verification, and reuse it for every before/after comparison so thresholds stay comparable.
  • Keep longtask and event-timing collection in the verification harness, not only in ad-hoc traces, so regressions are caught by automated checks.
  • Pin the interaction scenario (steps, data, frame) used for verification, so the same code path is exercised on every run.
  • Track the source location of the top main-thread task alongside the fix, so future changes can be verified against the same function rather than a generic metric.
  • Document the residual blocking work after each fix, so a later optimization knows which entry to target next instead of re-discovering the same hot spot.

Safe commands and checks

performance.getEntriesByType('longtask') // returns long-task entries recorded in the current page; inspect startTime and duration
performance.getEntriesByType('event') // returns Event Timing entries including interactionId, processingStart, processingEnd, and startTime
new PerformanceObserver((list) => { for (const e of list.getEntries()) console.log(e.entryType, e.duration, e.startTime); }).observe({ type: 'longtask', buffered: true })
new PerformanceObserver((list) => { for (const e of list.getEntries()) if (e.interactionId) console.log(e.name, e.duration, e.processingStart - e.startTime); }).observe({ type: 'event', buffered: true })
document.querySelectorAll('script[src]') // list deployed scripts to confirm the fix's source file is present in the running bundle
performance.now() // capture a high-resolution timestamp before and after the interaction to bracket the verification window