Web Performance Intermediate

Triaging Main-Thread Browser Freezes: A Playbook

A repeatable workflow to diagnose main-thread browser freezes, choose workload-specific mitigations, and validate fixes with performance traces.

45 min Octacer Engineering April 8, 2026
A dim after-hours workstation where an admin import panel has locked up mid-conversion, with one small green alert marking the stalled job

Triaging Main-Thread Browser Freezes: A Playbook

Objective

The target end state: a repeatable triage workflow where any reported freeze can be traced to a specific cause, matched to a workload-appropriate mitigation, and verified as resolved with quantitative evidence. The business reason: unresponsive interfaces cause user frustration, lost sessions, and support complaints; freezes that originate from different workload types (long synchronous scripts, layout thrashing, excessive garbage collection, or third-party work) require different mitigations, and applying the wrong one wastes engineering effort without resolving the problem.

Systems affected: any browser-based frontend — the code running on the main thread, the rendering pipeline, and the performance instrumentation used to observe it.

Success criteria

  • A reported freeze can be reproduced or observed in a performance trace.
  • The cause of the freeze is categorized by workload type, not just symptom.
  • A mitigation is chosen based on that workload category.
  • Traces before and after the change show a measurable reduction in main-thread blocking.
  • The freeze no longer reproduces under the conditions that triggered it.

These criteria are measured directly through the long-task and frame-timing metrics gathered in Step 2 and compared in Step 5; a criterion passes only when its trace data shows the expected change.

Prerequisites

Access Requirements

Access to the application's codebase and the ability to deploy changes to a staging or production environment.
Ability to run the application locally or in an environment that reproduces the freeze condition.
If the freeze requires authentication or specific data, an account and dataset that triggers it.

Data Requirements

A reproducible trigger: a user flow, page, or action that reliably causes the freeze.
If the freeze is intermittent, any available user reports, session replays, or prior traces that narrow down when it occurs.

Technical Conditions

A Chromium-based browser (Chrome or Edge) for tracing, because the performance tooling and long-task APIs are most complete there. Firefox and Safari can verify the fix afterward.
A local HTTP server or the ability to run the app's dev server, so traces capture realistic loading behavior rather than file://.
No other performance instrumentation conflicting with the trace capture (profiler extensions, heavy devtools plugins).

Decisions to Make

Whether the fix will be validated in staging or production. Staging is preferred for the first pass; production validation may be necessary for conditions that only appear with real user data or load.
Who owns the follow-up if the freeze is caused by a third-party script that the team does not control.
Whether fixing the freeze is a point fix or part of a broader performance budget the team wants to introduce.

Tools and systems

Tool Role in this implementation
Chrome DevTools Performance panel Captures main-thread activity, long tasks, and frame timing; the primary diagnostic instrument
Performance panel's Summary and Bottom-Up views Identify what work dominates the main thread and attribute it to specific script locations or functions
Long Tasks API (PerformanceObserver) Detects main-thread blocking programmatically, useful for regression monitoring after the fix
RAIL model The mental framework for judging whether a task is too long; the 100–200 ms threshold for input responsiveness is often cited from Google's RAIL guidance, not an absolute standard — treat it as a starting point for what users perceive as frozen
Browser task manager or chrome://tracing (optional) Measures the impact of third-party frames and extensions when the freeze may originate outside the page's own scripts

No additions to the stack are required. This playbook uses the browser's built-in tooling, so the main prerequisite is access to a Chromium browser and the application.

Step 1 — Reproduce the freeze and capture a baseline trace

What this step does

Before any change, you need a trace that shows the freeze happening. A trace is the evidence base for the entire diagnosis; without one, every later conclusion is speculation. The goal here is a clean, repeatable capture that shows the main thread blocking.

Actions

  1. Open the application in Chrome with DevTools closed (or with the Performance panel not yet recording, to avoid contaminating the measurement).
  2. Open DevTools (F12 or Ctrl+Shift+I / Cmd+Option+I) and navigate to the Performance panel.
  3. Set a low CPU throttle if the machine is fast and the freeze is hard to reproduce. In the Performance panel's capture settings, choose 4× slowdown or 6× slowdown to amplify main-thread work. Note the throttle setting in your notes — the before and after traces must use the same setting to be comparable.
  4. Click the record button (or press Ctrl+E / Cmd+E).
  5. Perform the exact user flow that triggers the freeze. If the freeze happens on load, reload the page during recording. If it happens on interaction, perform that interaction.
  6. Stop recording after the freeze resolves or a few seconds after the flow completes.
  7. Save the trace using the save icon (down arrow) in the Performance panel. Name it clearly, e.g., baseline-freeze-YYYY-MM-DD.cpuprofile or .json.
  8. Repeat the capture two more times to confirm the freeze is consistent and not a one-off. Freezes caused by garbage collection or network may vary between runs; note the variance.

Important considerations

  • Do not record while other heavy applications are running on the same machine. Browser traces are sensitive to system load, and the goal is to measure the page, not the laptop.
  • Same throttle, same machine, same flow for all baseline captures. Comparable traces are the core of this playbook.
  • If the freeze cannot be reproduced locally, capture it in production using the same Performance panel flow, or use a session replay that includes performance traces if one is available. Document that the baseline came from production and note the environment.

Done when:

You have at least one trace (ideally two or three) that clearly shows the reported freeze — a visible gap in responsiveness, a long task, or a period of dropped frames — and the trace is saved with a name that records the date and conditions.

Step 2 — Identify the workload type causing the freeze

What this step does

This is the diagnostic core. The freeze is a symptom; the workload is the cause. You categorize the blocking work into one of four workload types, each with a different mitigation in Step 3. The categorization comes from reading the trace, not from guessing about the code.

Actions

  1. Open the baseline trace in the Performance panel.
  2. In the Summary view, look at the color breakdown of the captured time. The dominant colors indicate the workload category:
  • Yellow/JavaScript — script execution dominates. This points to long synchronous JavaScript.
  • Purple/Rendering — layout and paint work dominates. This points to layout thrashing or excessive DOM work.
  • Gray/Other, long gaps with little activity — the main thread is idle but frames are still dropped. This points to excessive garbage collection or third-party work outside the page's scripts.
  1. In the Main track, locate the longest tasks. A task is any contiguous block of main-thread work. Click the longest one.
  2. In the Bottom-Up or Call Tree view, identify the function or script that accounts for the largest self-time within that task.
  3. Classify the freeze:
  • Long synchronous script — one task dominated by a single function or loop that runs for hundreds of milliseconds. Look for heavy computation, parsing, or processing of large arrays.
  • Layout thrashing — the trace alternates rapidly between JavaScript reads (e.g., offsetTop, getBoundingClientRect) and forced layout recalculations, with many small layout events rather than one long task.
  • Excessive garbage collection — the trace shows frequent GC pauses or "Minor GC" / "Major GC" events scattered through the timeline, often after the page allocates many short-lived objects.
  • Third-party work — the long tasks or frame drops correlate with network requests to third-party domains, or the trace shows work inside an iframe. Check the browser's task manager (Shift+Esc) to see per-tab CPU and whether an embedded frame or extension is consuming resources.
  1. Record the classification, the offending function or script, and the long-task duration in your notes. Example note:
Fast: baseline-freeze-2025-06-01.json
Category: long synchronous script
Offender: app.bundle.js -> renderReportTable(), 620 ms self-time
Long task: 720 ms

Important considerations

  • The dominant color in Summary and the long-task duration are the hard evidence for the classification. Do not classify on code reading alone.
  • A freeze may be a combination (e.g., a long script that also triggers layout thrashing). Classify by the dominant contributor; the mitigation targets that first, and Step 5 will reveal whether the secondary contributor still matters.
  • The 100–200 ms threshold for "long task" comes from Google's RAIL model, which is often cited as a guideline (a task over 100 ms feels unresponsive to users; 200 ms is the outer bound for perceived instant response) — treat it as a strong heuristic, not a hard law, and use your own baseline as the reference point for whether a task is problematic in your app.

Done when:

The freeze is classified into one of the four workload types, the offending script or function is named, and the classification is recorded with the trace it came from.

Step 3 — Apply the workload-appropriate mitigation

What this step does

The mitigation must match the workload type. Applying the wrong mitigation — for example, splitting a layout-thrash problem into chunks — will not fix the freeze. This step implements the minimal change appropriate to the classification from Step 2.

Actions

If long synchronous script

  1. Confirm the offending function is doing computation that can be deferred or split.
  2. Split the work into chunks and yield to the browser between chunks using either:
  • setTimeout(..., 0) / requestIdleCallback to defer non-urgent work, or
  • an async/await pattern that yields periodically, for example:
// Simplified: process rows in chunks, yielding to the event loop between chunks
async function renderReportTable(rows) {
  const CHUNK = 100;
  for (let i = 0; i < rows.length; i += CHUNK) {
    const slice = rows.slice(i, i + CHUNK);
    renderChunk(slice);
    await new Promise(resolve => setTimeout(resolve, 0));
  }
}

This is simplified to show the pattern; adapt renderChunk to your rendering logic.

  1. If the computation is genuinely CPU-bound (data transformation, aggregation), move it to a Web Worker so it runs off the main thread entirely:
// Simplified: hand off heavy computation to a worker
const worker = new Worker('/workers/report-worker.js');
worker.postMessage({ rows });
worker.onmessage = (e) => renderChunk(e.data);

This is the stronger fix when the work does not touch the DOM.

If layout thrashing

  1. Locate the read-write alternation in the trace — the sequence of forced layout reads followed by writes.
  2. Batch the reads: read all layout values, then perform all writes. Use requestAnimationFrame to group writes into the next frame:
// Simplified: batch layout reads first, then writes
const heights = elements.map(el => el.offsetHeight);
requestAnimationFrame(() => {
  elements.forEach((el, i) => el.style.height = heights[i] + 'px');
});
  1. If the layout work is unavoidable, consider reducing the number of elements being laid out (e.g., virtualize long lists so only visible rows are rendered).

If excessive garbage collection

  1. Identify what allocates the short-lived objects — look in the trace for the allocation site or use the Memory panel's allocation instrumentation.
  2. Reduce allocations in hot paths: avoid creating new objects/arrays inside loops, reuse buffers, or use object pooling where appropriate.
  3. If the allocations come from a library or third-party script, consider replacing it or using a cached variant.

If third-party work

  1. Identify the third-party domain in the trace or task manager.
  2. Load the third-party script defer or async so it does not block initial render, or lazy-load it only when the feature it powers is used.
  3. If the third-party work happens in an iframe, the freeze impact may be hard to eliminate entirely; consider whether the iframe can be loaded on-demand or replaced with a lighter integration.

Important considerations

  • Make the smallest change that addresses the classified workload. Do not refactor unrelated code in the same change; it makes the before/after comparison in Step 5 uninterpretable.
  • Web Workers cannot access the DOM. If the heavy work touches the DOM, chunking and yielding is the correct path, not a worker.
  • For third-party work, the fix may require coordination with the vendor or an internal decision about whether that script is worth its cost. That decision should surface early.

Done when:

A code change is implemented that targets the classified workload type, and the app still builds and runs without regressions in the flow being tested.

Step 4 — Deploy the change

What this step does

The fix must be deployed to the environment where the freeze was reproduced, so the before/after comparison happens under the same conditions. This step moves the change from the working tree to the running application.

Actions

  1. Commit the change with a message that references the workload classification, e.g., perf: chunk renderReportTable to fix long-task freeze on report view.
  2. Deploy to the environment used for the baseline capture. If the baseline was captured in staging, deploy to staging. If the baseline was captured in production, deploy to production (or confirm the freeze was also reproducible in staging, and deploy there).
  3. Confirm the app is running the deployed version — check the build identifier or a version string if the app exposes one.

Important considerations

  • The environment must match the baseline environment so the comparison in Step 5 is valid. The machine, browser, throttle, and flow must be the same.
  • If the freeze condition depends on production data or load that staging cannot replicate, the valid comparison has to happen in production, and the release window and rollout should account for that.
  • If the change is not safe to deploy alone (e.g., it touches a critical path), use a feature flag to roll it out incrementally, but keep the flag enabled for the validation capture.

Done when:

The change is deployed to the environment where the baseline was captured, and the app is confirmed to be running that version.

Step 5 — Validate the fix with a new trace

What this step does

The validation determines whether the freeze is actually gone, measured the same way it was observed. This step repeats the capture under identical conditions and compares the long-task metrics.

Actions

  1. Repeat the exact capture procedure from Step 1: same throttle, same flow, same machine, same environment.
  2. Capture at least one trace, preferably two, of the same flow that previously froze.
  3. Open the new trace and check the same metrics from Step 2:
  • Long-task duration: the longest task should be materially shorter, ideally under the 100–200 ms RAIL guideline range.
  • Dominant workload color: the JavaScript/rendering/GC time should be reduced in the area that was offending.
  • The offending function from Step 2 should no longer appear as a dominant self-time contributor.
  1. Check that the flow no longer visibly freezes: input responsiveness, scrolling, and animation should remain smooth during the flow.
  2. Confirm no new freeze or severe jank appeared elsewhere in the flow as a side effect of the change (the fix should not have moved the problem).

Important considerations

  • If the long task is shorter but the flow still visibly janks, the fix reduced but did not eliminate the problem — repeat Step 2 through Step 4 for the next largest contributor.
  • If the trace shows the same long task at the same duration, the mitigation did not address the cause. Reopen the baseline, re-examine the classification, and reconsider whether a different workload type is dominant.
  • A change that halves a 700 ms task but leaves a 350 ms task still fails the goal; the target is the RAIL range, not "better than before."

Done when:

The longest main-thread task in the fixed flow is inside or near the 100–200 ms guideline range, the previously offending function no longer dominates the trace, and the user flow completes without a perceptible freeze.

Validation

This section verifies the complete workflow end-to-end as a system, not just each step in isolation.

  1. Reproduce the original condition: run the app at the pre-fix commit in the baseline environment and confirm the freeze still reproduces there. This confirms the fix, not the environment, resolved the issue.
  2. Run the fixed flow: run the same flow at the post-fix commit in the same environment. Confirm no freeze.
  3. Compare long-task metrics: for three runs of each version, record the longest task. The post-fix distribution should be materially lower than the pre-fix distribution, and the post-fix values should be near or inside the 100–200 ms guideline. Use the median, not the best or worst run, to compare.
  4. Check for regressions: capture a trace of nearby flows that were not the target (e.g., other pages in the app) to confirm the fix did not introduce new long tasks or dropped frames.
  5. Verify failure behavior: introduce a deliberate error in the fix (e.g., a large dataset that exceeds the chunking assumption) and confirm the app fails gracefully — the async/await yielding path should handle a large payload without freezing, and an error should be visible in the console rather than silent corruption.
  6. Observability: if the app has performance monitoring (e.g., the Long Tasks API wired to an analytics endpoint), confirm a long-task event is no longer being reported for the fixed flow, or that reported durations dropped.
  7. Repeatability: run the fixed flow twice more and confirm results are consistent — the fix should not resolve the freeze only intermittently.

A pass means: the freeze is gone, long-task metrics are inside the guideline range, no new regressions appeared, and the failure path behaves gracefully.

Rollback & edge cases

Rollback

  1. 1

    Revert the commit

    Because the change is a code change, rollback is the standard revert path:

  2. 2

    Redeploy the prior version

    Revert the commit that introduced the fix (git revert <commit-hash>).
    Redeploy the reverted version to the same environment.
    Confirm the app returns to the pre-fix state and still builds and runs.

Use this path only if the fix introduces a regression, breaks the flow it was meant to fix, or fails validation. A revert is safe because the change is isolated to a single commit; if the fix was deployed behind a feature flag, disable the flag instead of reverting.

Edge cases

  • Freeze not reproducible: if you cannot trigger the freeze locally, capture from production or use session replay with traces. The diagnosis then runs on that trace. If the freeze is environment-specific (e.g., depends on load), note that the validation must also occur under that load.
  • Intermittent freeze: garbage-collection-driven freezes and network-dependent freezes vary between runs. Take more baseline and post-fix traces (five runs each) and compare distributions rather than single values.
  • Multiple contributors: a freeze may have a primary and a secondary cause. The first pass targets the primary; re-run Step 2 on the post-fix trace to check if the secondary still exceeds the guideline.
  • Third-party script that cannot be changed: if the vendor owns the script, document the decision and either accept the freeze, negotiate a lighter integration, or replace the dependency. This is a product decision, not a code decision.
  • The fix moves the problem: chunking a script can push work into later frames, shifting jank rather than removing it. The Step 5 and validation traces check the whole flow, not just the offending function, to catch this.
  • Very large datasets: chunked processing assumes the chunk loop terminates reasonably. A pathological dataset (millions of rows) may still freeze; cap the chunked work and consider a worker for that case.
  • Local vs. production discrepancy: a fix validated on a fast machine may not hold on low-end devices. If the freeze was reported on specific hardware, validate on that hardware or with CPU throttling that approximates it.

Next step

If validation passes, wire the Long Tasks API (or existing performance monitoring) into your CI or runtime observability so regressions are caught automatically, and set a performance budget that fails builds when a long task exceeds the RAIL guideline in your critical flows.

Ready to Implement This Playbook?

Our team can implement these strategies for you, tailored to your specific business needs.

Schedule Consultation