Browser · beginner

Browser performance checklist

Engineers frequently chase browser performance problems as if they were a single defect, when in practice input handling, navigation timing, and rendering each consume a separate share of the interaction budget. This guide frames browser performance as a budget-allocation problem, gives an ordered triage that measures where the budget is actually being spent, and pairs each diagnostic branch with a conditional fix and a regression check that can be re-run. The central argument: stabilize the budget you can measure before touching the code you suspect.

The symptoms

  • Interaction to Next Paint (INP) exceeds the 200ms target at the 75th percentile while the page is otherwise responsive, indicating that input, navigation, or rendering work is crossing the interaction budget.
  • First Contentful Paint remains fast but Largest Contentful Paint regresses, suggesting navigation cost is concentrated in resource loading rather than in script execution.
  • Long Tasks entries appear in the performance timeline during scroll or pointer events, confirming that main-thread work is being scheduled inside the input-to-render window.
  • Layout shifts accumulate in the absence of new content, pointing to rendering (style, layout, paint, composite) work that is being triggered after input handlers complete.
  • Frame timing shows dropped or delayed frames on a previously stable workload after a code, markup, or third-party change, narrowing the failure to a recent delta.

Likely causes

  • Input handlers that perform synchronous layout reads (offsetWidth, getBoundingClientRect) followed by writes, triggering forced reflow inside the input-to-paint window.
  • JavaScript bundles or third-party scripts whose main-thread execution is scheduled between pointerup and the next paint, inflating the input handling portion of INP.
  • Render-blocking resources (synchronous scripts in head, unoptimized CSS, large web fonts) that push LCP later even when FCP is healthy, consuming the navigation budget.
  • Unbounded list virtualization or non-virtualized rendering of large DOM trees, where scroll and pointer events trigger style and layout over a large element count.
  • Animation work promoted to the compositor but driven from main-thread rAF callbacks, leaving rendering work that competes with input handling on the same thread.

First ten minutes

  1. 01Confirm the failure mode: record a Performance trace covering a representative user interaction (click, key press, scroll end) and locate the Long Tasks entries that overlap the input-to-paint window; the presence of any task above 50ms inside that window is the first budget signal.
  2. 02Decompose the budget: using the same trace, separate timing into input delay, input handling duration, and presentation delay; identify which segment exceeds its share before attributing the cause to any specific script.
  3. 03Capture navigation timing: read the PerformanceNavigationTiming entries and note the gap between responseEnd, domInteractive, and the LCP candidate timestamp; a wide gap between responseEnd and LCP is a navigation-budget symptom, not a rendering one.
  4. 04Inventory recent change boundaries: identify the script, stylesheet, font, or third-party tag that was added or modified in the window where the regression appeared, so subsequent fixes can be scoped to a delta rather than the whole page.
  5. 05Establish a measurement baseline: export the trace summary (Long Tasks count, INP, LCP, CLS) so that any subsequent fix can be compared against the starting budget, not against intuition.

Evidence to collect

  • Performance trace (PerformanceObserver entries for longtask, element, layout-shift, and resource timing) with the input-to-paint window annotated.
  • PerformanceNavigationTiming: fetchStart, responseEnd, domInteractive, domContentLoadedEventEnd, and the LCP candidate element timestamp.
  • Script attribution: per-script execution time derived from the trace's script evaluation breakdown, scoped to the input handling segment.
  • Layout Shift entries with their attributed elements and the input events that immediately preceded them.
  • Resource waterfall for the navigation period: sizes, transfer sizes, and render-blocking status of CSS, fonts, and synchronous scripts.

Where to look

  • At the input boundary: the listeners attached to the element that received the interaction, including delegated listeners on ancestors, and the microtasks scheduled inside them.
  • At the navigation boundary: the document head (render-blocking resources), the critical CSS path, and any preload/preconnect directives that are absent or mis-scoped.
  • At the rendering boundary: the elements flagged by layout-shift entries, the style recalculation scopes in the trace, and the composite layers listed in the paint records.
  • At the third-party boundary: the script tags that are not first-party, their load timing relative to first paint, and whether they register listeners that overlap the input window.

Diagnostic steps

  1. 01Run a Performance trace on the slowest interaction reported by INP; if no INP value is available, use the longest Long Tasks entry that overlaps a pointer or keyboard event.
  2. 02In the trace, mark the input event's dispatch timestamp, the start and end of any Long Task that overlaps it, and the timestamp of the next paint; classify the gap as input delay, input handling, or presentation delay.
  3. 03Cross-reference the Long Task with the Script Evaluation and Function Call entries to identify which script is responsible; do not assume the largest bundle is the cause until the attribution confirms it.
  4. 04Read the PerformanceNavigationTiming for the same page load; if LCP lag originates before responseEnd, the issue is network-bound and belongs to the navigation branch, not the rendering branch.
  5. 05Correlate Layout Shift entries with the input events that preceded them within a short window; if shifts cluster around an input handler, the cause is rendering triggered by that handler, not an unrelated resource.
  6. 06Disable or defer the candidate script/stylesheet/font (via a scoped build flag or a tag removal in a non-production environment) and re-record the trace; only accept the attribution if the relevant budget segment shrinks proportionally.

Common mistakes

  • Treating a slow INP as a single problem and optimizing the largest script first, instead of measuring which budget segment (input delay, input handling, presentation delay) actually owns the regression.
  • Adding more listeners or throttling handlers in response to a rendering regression, when the trace shows the work is in style or layout rather than in the handler itself.
  • Removing a render-blocking resource and concluding the fix worked, without re-measuring LCP, INP, and Long Tasks against the baseline captured before the change.
  • Attributing layout shifts to images or fonts when the shifts occur inside an input handler's microtask queue; the root cause is then mis-located and the fix targets the wrong boundary.
  • Relying on averages from aggregate performance dashboards instead of the trace that reproduces the slowest interaction, which can hide the single cause responsible for the budget exceedance.

Safe fixes

  • Conditional on the trace showing input handling above 50ms in a specific listener: move the synchronous work into a scheduler.postTask or requestIdleCallback, or split the handler so that only the user-visible response runs synchronously and the rest is yielded.
  • Conditional on layout reads inside an input handler followed by writes: batch the reads first, perform the writes in the next frame, or use transform and opacity to avoid triggering layout.
  • Conditional on a render-blocking CSS or font delaying LCP past the navigation budget: inline critical CSS for above-the-fold content, preload the LCP font with the correct crossorigin attribute, and add media queries that prevent unused stylesheets from blocking.
  • Conditional on a third-party script overlapping the input window: defer the script, load it on idle, or scope it to a route so it does not register listeners that compete with first-party input handling.
  • Conditional on a large DOM causing layout work per scroll event: virtualize the list so that the rendered element count is bounded, and confirm via the trace that style recalculation scope shrinks proportionally.
  • Conditional on rAF-driven animations competing with input handling: move the animation to CSS or to a Web Worker, leaving only the input listeners on the main thread.

Prove the fix

  1. 01Re-record the Performance trace on the same interaction used for the baseline and confirm that no Long Task above 50ms overlaps the input-to-paint window for that interaction.
  2. 02Re-read PerformanceNavigationTiming and confirm that the LCP candidate timestamp now falls inside the navigation budget expected for the page's network profile; if it does not, the fix targeted the wrong boundary.
  3. 03Re-read the Layout Shift entries and confirm that the shifts that previously clustered around the input event are absent, or are attributed to elements that are no longer affected by the handler.
  4. 04Re-measure INP at the 75th percentile across a representative sample of interactions and confirm the value has moved back inside the target band; do not declare success on a single trace.
  5. 05Keep the baseline trace and the post-fix trace under the same PerformanceObserver configuration so the comparison is reproducible by another engineer.

Prevention and next steps

  • Treat input handling, navigation, and rendering as three budgets in a Performance budget document, with explicit per-page thresholds so regressions are caught before they ship.
  • Add a PerformanceObserver-based check in CI that fails the build when INP, LCP, or CLS regress against a stored baseline, scoped to a representative interaction on a representative route.
  • Restrict third-party scripts from registering listeners on first-party input paths unless their cost is measured and accepted as part of the budget.
  • Audit render-blocking resources and font loading on each route change, since a new route can quietly introduce a CSS or font that consumes the navigation budget.

Safe commands and checks

// Capture a Performance trace for a specific interaction; the interaction marker is required so the trace can be aligned to input handling. (DevTools, Performance panel)
// Read PerformanceNavigationTiming for the current page; the gap between responseEnd and the LCP candidate timestamp is the navigation-budget symptom. (DevTools, Performance panel)
// Observe long tasks from the page itself using a PerformanceObserver; filter to entries whose duration exceeds 50ms.
const lt = new PerformanceObserver((list) => { for (const e of list.getEntries()) if (e.duration > 50) console.log(e); }); lt.observe({ type: 'longtask', buffered: true });
// Observe layout shifts and attribute them to the most recent input; entries with a recent.input value identify input-triggered shifts.
const ls = new PerformanceObserver((list) => { for (const e of list.getEntries()) if (e.recentInput) console.log(e); }); ls.observe({ type: 'layout-shift', buffered: true });
// Defer a candidate script in a non-production environment and re-record the trace; only keep the change if the relevant budget segment shrinks.