Long Tasks
One task that runs long blocks input, rendering and accessibility-tree updates at the same time — which is why "the page froze" is one symptom with one cause.
The intent, the obvious build, and why it breaks
Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.
What is actually happening to the user while one of my tasks is running, and how do I find the task responsible?
A person taps a control and expects the page to acknowledge it. What they get instead is a page that ignores taps, does not scroll, and then catches up all at once.
The function is slow, so optimise it. Profile it, shave the hot path, and the freeze goes away because the work takes less time.
Halving the duration of a task that blocks the thread halves the freeze; it does not remove it. A user notices being ignored long before the work is anywhere near instant (Interaction Responsiveness).
- Halving the duration of a task that blocks the thread halves the freeze; it does not remove it. A user notices being ignored long before the work is anywhere near instant (Interaction Responsiveness).
- The freeze is not one symptom but three at once — input is not dispatched, no frame is produced, and the accessibility tree is not updated — because all three are the same thread's responsibility (What the Main Thread Owns).
- Input that arrives during the task is not lost; it is queued and applied afterwards, which produces the characteristic "nothing, nothing, everything at once" behaviour that reads as a bug in the application logic.
- Optimisation has a floor. Some work — parsing a large response, hydrating a large tree, sorting a large list — is irreducibly long on a slow device, and the only remaining move is to change *when* and *where* it runs (When a Worker Is Actually the Answer).
- The task that blocks is frequently not one you wrote. Script evaluation, hydration and third-party code all produce long tasks under your page's name (The Real Cost of JavaScript).
What is actually happening
In the browser, not in the framework.
- A task runs to completion. While it does, the loop cannot reach the point where it selects another task, drains microtasks or considers a rendering opportunity — so every other pending thing waits by construction (The Event Loop, Precisely).
- Input events arriving during a long task are queued by the browser and often coalesced. The delay between the user's action and the handler starting is the part of interaction latency that scheduling controls, and it is caused by whatever was already running (Interaction Responsiveness).
- The accessibility tree is derived from the DOM by the same thread. A long task therefore freezes what assistive technology can observe just as completely as it freezes the screen (The Accessibility Tree).
- The Long Tasks API reports tasks that exceed a threshold defined by the specification, with attribution limited to a container — an iframe or the top-level document — rather than to a function. It tells you that a freeze happened and roughly whose script it was, not which line.
- Newer instrumentation reports the whole animation frame rather than an isolated task, which better matches the user's experience: several medium tasks with no frame between them feel identical to one long one, and only frame-level reporting shows that (Vitals in the Field).
- Long tasks compound with the other thing on the thread: a rendering phase that follows a long task is itself delayed, so the frame the user finally gets is late by the task plus the render.
What this makes the browser do
And which of it is avoidable.
- Queueing and coalescing input while it cannot dispatch it — real bookkeeping, and the reason a five-second freeze does not fire five seconds of
mousemovehandlers on recovery. - Skipping rendering opportunities it cannot take, then doing a larger-than-usual amount of style and layout work when it finally can, because more was invalidated (The Cost of a Change).
- Holding accessibility-tree updates and platform accessibility notifications until the thread is free.
- Running garbage collection in the gaps it can find — and when there are no gaps, inside your task, which lengthens it further (Memory Leaks).
One cause, three symptoms
The reason "the page froze" is such a useful bug report is that it is precise: input, rendering and accessibility all stop together because they are all served by the same thread at the same point in the loop. Any fix that addresses one of them without shortening or moving the task is addressing a symptom.
The rows below are the same underlying event seen by four different users. Note that the last two have no visual cue at all, which is why relying on a spinner as the whole mitigation quietly excludes people.
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| Pointer user clicks during the task | Nothing happens, then everything happens at once | Input events are queued and coalesced; they dispatch when the loop turns | Shorten or move the task; render a pending state in an earlier task (The Rendering Opportunity). |
| Any user tries to scroll | Content sticks, then jumps | No rendering opportunity is taken while the task runs; frames are lost, not delayed | Keep scroll-adjacent handlers passive and off the critical path (Passive Listeners). |
| Screen-reader user navigates | Stale content is announced as current | The accessibility tree is derived on the same thread and is not updated | Mark the region busy before the work and announce completion once (Live Regions and Announcement). |
| Keyboard user tabs through a form | Focus ends up several fields away from where they expected | Queued key events all apply after the task, in a burst | Avoid long tasks in input handlers entirely; chunk or defer (Keyboard Operability). |
| Any user, during page load | Content is visible but does not respond | Script evaluation and hydration are long main-thread tasks | Ship less script; split it; hydrate less of the page (Islands and Partial Hydration). |
What the gap costs
Interaction latency is not the duration of your handler. It is the wait for the thread, plus the handler, plus the wait for the next frame — and a long task already in flight contributes to the first and the last of those.
The schematic below shows a tap that arrives one unit into a long task. The handler itself is short. The user's experience is dominated entirely by the two waits around it.
- Long task (already running) — Nothing else can be selected, drained or rendered until this returns.
- Tap arrives — Queued. The browser is not ignoring it; it cannot reach the dispatch step.
- Input delay (waiting for the thread) — Caused entirely by the task that was already running.
- Handler runs — The only part most developers profile — and often the smallest.
- Paint + composite (user sees it) — The presentation delay. The interaction is only over when this lands.
Optimising the handler attacks the smallest bar. Shortening or moving the first bar is the fix.
What to actually do about it
The options are ordered by how much they remove rather than move. Doing less genuinely removes cost; a worker moves it somewhere the user does not feel; chunking keeps the cost and redistributes it; deferring moves it in time. Each is right in a different situation, and reaching for chunking first is the common mistake because it is the easiest to implement.
Whichever you pick, decide what the user sees while it happens. A mitigation that keeps the page responsive but shows nothing meaningful has fixed the metric and not the experience (Loading, Error, Empty — The States You Did Not Render).
Can the work be removed, moved off the thread, split, or deferred?
when The work is proportional to something you control — rows rendered, bytes parsed, script shipped.
cost Product surface, or an architecture change such as virtualising a list or paginating a response (List Virtualization).
when It is CPU-bound and does not need the DOM: parsing, transforming, diffing, compressing (When a Worker Is Actually the Answer).
cost Serialisation across the boundary, a second copy of the data, and asynchronous plumbing through code that was synchronous (Structured Clone and Transferables).
when The work must touch the DOM, or is too entangled to move, but can be split into independent pieces (Yielding and Scheduling).
cost Longer total time, intermediate states the user can see and interact with, and a re-entrancy problem if state changes mid-run.
when It is not needed for the current interaction — prefetching, analytics, warming a cache (Analytics Events That Answer a Question).
cost It has to happen eventually; deferred work often lands during the next interaction unless it is explicitly scheduled for idle.
when The result is the same for many users and the client is doing work that could have arrived done (Server-Side Rendering).
cost Server cost, cache invalidation, and a payload that may be larger than the input it replaces (How API Shape Drives UI Complexity).
How to build it
Most important first.
- Do less work first. The cheapest long task is the one that never had a reason to exist: render fewer nodes, parse less data, ship less script (List Virtualization).
- Then move the work. CPU-bound work that does not need the DOM belongs on a worker, where its duration stops mattering to the thread that paints (When a Worker Is Actually the Answer).
- Then break it up. Chunk across tasks so the loop turns between pieces, accepting a longer total for a responsive page (Yielding and Scheduling).
- Then move it in time. Work that is not needed for the current interaction can be deferred to idle or to a later navigation, which removes it from the critical window without removing it from the product (Code Splitting).
- Measure at the frame level, not the function level. The user experiences "no frame for a while", and several medium tasks with no rendering opportunity between them are the same experience as one long one.
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
- This is the sharpest accessibility argument in the module: a blocked main thread stops accessibility-tree updates, so a screen reader continues to announce content that is no longer true, with nothing to indicate that it is stale (The Accessibility Tree).
- A sighted user infers "busy" from the absence of motion. A screen-reader user has no equivalent cue, so the failure mode is not a delay but a confidently wrong answer.
- Keyboard input queues exactly like pointer input. A user tabbing through a form during a long task finds focus somewhere unexpected when it resolves, because several queued key events apply at once (Keyboard Operability).
- If work is unavoidable, say so in the accessibility layer as well as visually: mark the region busy before starting, announce completion once, and never rely on a spinner alone (Live Regions and Announcement).
- Users of switch access, voice control and other slower input methods are affected disproportionately, because a freeze that costs a mouse user one click costs them a whole re-entry of an interaction (Accessible Component Patterns).
What can go wrong
- A freeze during page load that everyone attributes to the network. Script evaluation and hydration are main-thread tasks, and a page whose bytes have all arrived can still be unresponsive for a long time (Hydration).
- A chunking mitigation that yields with a nested zero-delay timer and is clamped, so the work now takes far longer and the page is still not usable.
- Chunking that leaves the DOM in an intermediate state between chunks. The user now sees a half-rendered list and can interact with it (Loading, Error, Empty — The States You Did Not Render).
- Moving work to a worker and paying more in serialisation than the work cost. Structured cloning a large object graph is itself main-thread work (Structured Clone and Transferables).
- Optimising the task you can see. Long-task attribution points at a container, and the honest answer is often a third-party script that your instrumentation attributes to you (Third-Party Scripts and the Supply Chain).
- Input queued during a long task applies afterwards against a DOM that may have been replaced by the task itself, so a click lands on an element that no longer means what the user thought (Node Identity Across Updates).
- Several interactions can queue and then dispatch in a burst, producing state transitions in an order the user did not intend to create.
- A network response that arrives during a long task is delivered afterwards, so a request started later can appear to complete first from the application's point of view (Out-of-Order Responses).
- Timers due during the task all become due at once and run back to back when the thread frees.
- Any script on the page can hold the thread. There is no per-script budget and no isolation within a page, so responsiveness is a shared resource with no enforcement (Third-Party Scripts and the Supply Chain).
- A long task is a soft denial of service against your own users, and an attacker who can inject content that provokes one — a pathological input to an unbounded client-side loop — does not need any further capability (Cross-Site Scripting).
- Client-side rate limiting implemented by disabling controls fails during a freeze in the user's favour and against yours: the queued events dispatch after the task, and the server sees the duplicates. Enforce it server-side (How API Shape Drives UI Complexity).
- "The page froze, so something is blocking on I/O." Nothing in the browser blocks on I/O on the main thread. A freeze is the thread being occupied, not waiting (Async I/O: What `await readFile()` Actually Does).
- "Async code cannot cause a long task." An
asyncfunction with a heavy synchronous body produces exactly the same long task as a plain one (Async Is Not Parallelism). - "Faster is enough." Below the threshold that instrumentation reports, the user can still feel a delay; above it, halving the duration halves an experience that was already unacceptable.
- "Long tasks are a loading problem." They are equally an interaction problem, and the interaction case is the one users complain about, because they were actively waiting when it happened (Interaction Responsiveness).
- "The profile shows one long task, so there is one culprit." Attribution is at container granularity, and one task can contain work from several scripts including code you did not write.
Measuring it, and what changes in the field
- The Performance panel flags long tasks on the main-thread track, and the frame track above shows the gap they created. Read them together: the gap is what the user felt (A Mental Model of the Devtools).
- A
PerformanceObserveron long-task entries gives field coverage; newer long-animation-frame reporting attributes at frame granularity and identifies scripts more usefully, where it is supported (Real User Monitoring). - Interaction latency in the field is the outcome metric — it decomposes into input delay, handler duration and time to the next frame, and long tasks show up in the first and third (Interaction Responsiveness).
- Always profile with CPU throttling on. A long task on a mid-range phone is often not a long task on a development machine, so the local profile will not contain the bug (Measure Before Optimising).
- On a slow device the same code produces longer tasks, and work that never crossed the threshold locally crosses it constantly in the field — commonly by a large multiple (The Real Cost of JavaScript).
- During page load the thread is already contended by parsing, script evaluation and hydration, so a task that is fine later is a freeze then (The Critical Rendering Path).
- On a large dataset, per-item costs that are individually trivial aggregate into one task; the failure appears at a data size nobody tested.
- In a long-lived tab, accumulated listeners, retained nodes and growing caches make the same operation slower over the session, so the freeze appears only after prolonged use (Long-Lived Clients and Version Skew).
- Every mitigation costs something. Chunking costs total time and adds intermediate states; a worker costs serialisation and architectural complexity; deferring costs a later wait; doing less costs product surface.
- Instrumenting long tasks in the field adds an observer and reporting traffic, and the data is attributed coarsely enough that acting on it takes judgement rather than a lookup.
- Optimising the wrong thing is the expensive failure here: a week spent shaving a task that is a small share of the user's wait, while the real cost was a third-party script, is a common outcome of measuring functions instead of frames.
Where this applies
Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.
- GENERALThat a task cannot be interrupted, and that input, rendering and accessibility updates therefore all wait on it, follows from the processing model and holds in every engine.
- SPEC-EVOLVINGHow long tasks are reported is actively changing: the original long-task entry type carries coarse container-level attribution, while long-animation-frame reporting attributes at frame granularity with script-level detail and is not yet available everywhere, so instrumentation written today should feature-detect rather than assume a single entry type.
- DEVICE-SPECIFICWhether a given piece of work becomes a long task is a property of the device, not the code: mid-range phones routinely run main-thread JavaScript several times slower than a development laptop, so the same function crosses the threshold in the field and never crosses it locally.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Programming Languages & Runtime Internals — parse, compile and optimisation work performed by the JavaScript engine happens inside these tasks, which is why the first execution of a large script is so much more expensive than the second.