Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

A browser page can freeze when JavaScript occupies the main thread for too long. A synchronous job runs to completion, so while it is running the browser cannot use that thread to handle input or reach a rendering opportunity. The event loop coordinates scripts, events, and rendering; understanding its queues explains why some fixes help and why others—especially wrapping a calculation in a Promise—do not.

Why can JavaScript freeze a browser page?

In a browser page, JavaScript and much of the page’s user-interface work share the main thread. A job that runs for a long time keeps that thread occupied. Clicks and scrolls may not be handled, and the browser cannot reach its next opportunity to update the display until the job finishes. MDN summarizes the effect: “Because your code runs in the same thread, using the same event loop, as the browser’s user interface, if your code blocks or enters an infinite loop, the browser itself will stall.” MDN’s in-depth event-loop guide explains the relationship between page code and the browser UI.

This is run-to-completion: one job is processed completely before another begins. The rule makes execution order predictable, but it also means the browser cannot interrupt ordinary synchronous JavaScript halfway through to respond to a user. An infinite loop blocks the thread indefinitely; a finite but expensive calculation blocks it until completion. MDN’s JavaScript execution model describes the trade-off and recommends keeping jobs short.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How does the browser event loop work?

The WHATWG HTML Standard describes event loops as coordinating work that includes events, user interaction, scripts, rendering, and networking. An event loop is a browser-platform concept, not a guarantee that each loop corresponds to a separate operating-system thread.

A useful simplified model for a page iteration is:

  1. The browser runs at most one pending task, such as a script or event callback.
  2. It drains pending microtasks, including any new microtasks queued while draining.
  3. If appropriate, it reaches a rendering opportunity and may update the display before continuing.

This is a mental model, not a promise that the browser paints after every callback or statement. Rendering is scheduled by the browser; JavaScript cannot assume that each completed callback produces a visible frame. See MDN’s in-depth guide to the event loop for the practical browser-page sequence.

Tasks and microtasks: why Promises do not automatically unblock the UI

Tasks are separate turns of work

Tasks include starting a script, dispatching an event, and running timer callbacks. After a task finishes, the browser drains the microtask queue before moving to a later task. That queue ordering is why the distinction between a task and a microtask matters when trying to let the page respond.

Promise callbacks are microtasks

Promise callbacks and MutationObserver callbacks run as microtasks. The browser drains microtasks until the queue is empty, including microtasks added by other microtasks. A chain that continually queues another microtask can therefore prevent the event loop from reaching later tasks and a rendering opportunity. MDN’s microtask guide describes the queue and cautions against using microtasks indiscriminately.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

For example, this does not move the calculation off the main thread or guarantee a paint before it runs:

Promise.resolve().then(() => {
  doExpensiveCalculation();
});

The callback is deferred to the microtask queue, but the calculation inside it is still synchronous. Likewise, queueMicrotask() is useful for specialized ordering and cleanup, not for yielding to rendering. To let the browser handle later event-loop work, split computation into separate tasks or move suitable computation to a worker.

Async I/O is not the same as CPU-heavy work

When code starts asynchronous I/O—such as a fetch() request or an IndexedDB operation—the page can do other work while it waits. The eventual callback runs when the result is available. That is different from a long calculation that runs synchronously on the main thread.

Adding async to a function or using Promises does not make CPU-heavy synchronous code run elsewhere. An async function executes synchronously until it reaches an await; after resumption, any lengthy calculation still occupies the thread on which that code runs. MDN’s execution-model documentation explains how asynchronous operations fit with JavaScript jobs.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How to keep a page responsive

Split long work into separate jobs

Break a large operation into smaller units and schedule later units as separate tasks. Between jobs, the browser can return to the event loop and handle other work. The right chunk size depends on the work and application; the cited documentation does not establish a universal millisecond cutoff. Avoid replacing one long synchronous task with a continuously replenished microtask chain, which can still keep later tasks waiting.

Move independent computation to a web worker

Use a worker when lengthy computation can run independently of direct DOM updates. A worker executes outside the page’s main code, allowing the main thread to stay available for interface work. The worker and page communicate by messages, so this approach fits calculations that can be isolated from DOM access; it adds coordination and data-transfer considerations. Keep DOM updates on the main thread. MDN’s in-depth guide discusses workers as a way to run work outside the page’s main thread.

Choose the right animation mechanism

  • CSS animation: Prefer it for effects that can be expressed as CSS, rather than repeatedly calculating and updating them in JavaScript.
  • requestAnimationFrame(): Use it when animation needs JavaScript-driven drawing, such as canvas rendering. It schedules work in coordination with browser rendering rather than running an old-style interval loop.

The choice depends on whether the effect can be represented with CSS or requires per-frame JavaScript logic. MDN’s JavaScript performance guide covers these animation approaches.

Reduce avoidable interface work

Limit unnecessary DOM changes and batch updates that must happen. Remove event listeners when they are no longer needed, especially listeners for events that fire continuously. These steps reduce avoidable work, though they do not make a genuinely long synchronous calculation non-blocking. See MDN’s performance guidance for JavaScript.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Should you split work or use a worker?

Approach Best fit Main consideration
Split work into separate main-thread jobs Work that can be broken into short steps and needs to interact with page state or the DOM. Design the steps and scheduling so the browser can return to other event-loop work between them.
Use a web worker Complex or lengthy computation that can run independently of direct DOM access. Isolate the computation and communicate with the page by messages; consider coordination and data transfer.

There is no universal numeric threshold in the cited documentation for choosing between these options. Decide based on whether the computation can be divided cleanly, whether it needs direct DOM access, and how much message-based coordination its data requires.

A practical way to reason about a freeze

  • If input stops responding during a loop or calculation, identify the synchronous work occupying the main thread.
  • If a Promise-based change did not help, check whether its callback still performs a long calculation; Promise callbacks are microtasks, not a separate execution thread.
  • If the calculation does not need the DOM, consider a worker. If it can be performed in smaller stages, schedule those stages as separate jobs.
  • If the issue is animation, use CSS for suitable effects or requestAnimationFrame() when JavaScript must control drawing.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.