Skip to main content

Bottlenecks in JS code

A bottleneck is the piece of code that caps the performance of the whole application, and in JavaScript there are eight typical ones: the main thread, the DOM, memory, loops, the network, rendering, the bundle and algorithms. The ninth and most common is the absence of profiling, when people optimise at random.

Theory

TL;DR

  • JavaScript is single threaded: any heavy synchronous operation blocks the UI, rendering and event handling.
  • DOM operations are the most expensive; reading geometry inside a loop forces a reflow.
  • Memory leaks come from timers, event listeners, closures and global variables.
  • On large arrays the algorithm and the data structure decide everything: Set and Map instead of a linear search.
  • The network and bundle size often matter more than the speed of the code itself.
  • Animations belong on the GPU (transform, opacity, requestAnimationFrame).
  • Profile first, optimise second.

Quick example

javascript
// Bad: reading geometry inside a loop makes the browser recalculate layout every time items.forEach((el) => { el.style.height = el.offsetHeight + 10 + 'px'; }); // Good: read everything first, then write everything const heights = items.map((el) => el.offsetHeight); items.forEach((el, i) => { el.style.height = heights[i] + 10 + 'px'; });

Main thread blocking

JavaScript is single threaded. Any heavy operation blocks the UI, rendering and event handling.

Examples:

  • large loops (for, while, forEach) without setTimeout or requestIdleCallback;
  • parsing or processing large JSON payloads (JSON.parse(hugeData));
  • computationally heavy algorithms (sorting, recursion, encryption);
  • synchronous requests (XMLHttpRequest without async);
  • large DOM changes inside a single frame.

How to fix it:

  • split the work into chunks (setTimeout, requestAnimationFrame);
  • use Web Workers for background computation;
  • apply debounce and throttle to events.

Excessive renders, DOM work and painting

DOM operations are the most expensive thing you can do for performance.

Problems:

  • inserting and removing elements too often;
  • style and layout recalculation on every change;
  • touching offsetHeight, getComputedStyle or scrollTop triggers a reflow;
  • loops that mutate the DOM inside the body.

How to fix it:

  • use DocumentFragment, a virtual DOM, batch updates;
  • cache element references;
  • minimise reflow by switching a class for a whole group of elements at once;
  • in React: memoisation, PureComponent, React.memo, useMemo, useCallback.

Weak rendering optimisation

The UI stutters, animations lag, FPS drops.

Causes:

  • changing style and transform too frequently;
  • heavy shadows, filters and border-radius;
  • not using the GPU (CSS animations without transform: translateZ(0));
  • layout recalculation on every animation frame.

Solutions:

  • move animations to the GPU;
  • combine DOM changes into a single frame;
  • use will-change, transform, opacity;
  • use requestAnimationFrame instead of timers for animation.

Excessive memory usage

Memory leaks make the tab heavier and heavier until the interface freezes.

Causes:

  • uncleared timers (setInterval, setTimeout);
  • dangling event listeners that are never removed when the element goes away;
  • closures holding references to large objects;
  • global variables that are never cleaned up;
  • caches that are never evicted.

Solutions:

  • clear timers (clearInterval, clearTimeout);
  • remove handlers (removeEventListener);
  • use WeakMap and WeakSet;
  • analyse through Chrome DevTools, the Memory tab.

Inefficient loops, collections and algorithms

This shows up especially on large arrays.

Typical mistakes:

  • nested O(n^2) loops where they are not needed;
  • using .map(), .filter() and .reduce() on large arrays without optimisation;
  • creating new arrays or objects on every iteration;
  • frequent Array.splice() and Array.shift() calls, which are expensive operations.

What to do:

  • use more suitable structures: Set, Map, WeakMap;
  • use iterators, generators and for...of instead of forEach on large volumes;
  • profile the hot spots with performance.now().

Suboptimal data structures

An example: sorting an array of 100 000 elements with sort() and no comparator, or searching with filter() instead of a Set.

What to do:

  • pick the structure for the task: Set for uniqueness, Map for fast lookups;
  • avoid a linear search where a hash lookup will do;
  • use binary search and cache computed results.

Network, bundle and profiling

Even fast JS code will not save you if the network is saturated.

Problems:

  • repeated requests with no cache;
  • duplicate API calls on every render;
  • downloading enormous JS and CSS bundles;
  • no gzip or brotli compression;
  • no lazy loading.

How to fix it:

  • caching (HTTP cache, IndexedDB, Service Worker, memoisation);
  • request batching;
  • React Query, SWR, the Cache-Control header;
  • code splitting and dynamic imports (import()).

A heavy bundle

The more JavaScript there is, the longer loading and parsing take.

Causes:

  • pulling in unnecessary libraries;
  • duplicated dependencies;
  • not using tree-shaking;
  • inline JSON, large icons and images embedded in code.

Solutions:

  • analyse the bundle (webpack-bundle-analyzer, next build --analyze);
  • tree-shaking, code splitting, dynamic import();
  • use a CDN, HTTP/2 and ESM bundles;
  • minification (Terser, SWC).

No profiling and no metrics

The most common mistake of all is optimising blindly.

Solutions:

  • Chrome DevTools (Performance, Memory, Coverage);
  • console.time(), performance.mark() and performance.measure();
  • Lighthouse, Web Vitals;
  • Sentry Performance, New Relic, Datadog.

A short summary:

CategoryExample problemHow to fix it
Thread blockingA long loopSplit it up, use a Worker
DOMToo many reflowsBatch updates
MemoryLeaksWeakMap, cleanup
LoopsO(n^2)Optimise
NetworkRepeated requestsCache
RenderingFPS < 60GPU animations
Bundle2 MB of JSTree-shaking
AlgorithmsWrong structureMatch it to the task

Common mistakes

  • Optimising without measuring. Without a profile in DevTools, effort goes into code that does not affect the result.
  • Treating async as a cure for blocking. async/await does not move computation to another thread: a heavy loop inside an async function still freezes the UI. A Web Worker is what you need.
  • Interleaving DOM reads and writes. The pattern "read geometry, write a style, read again" causes layout thrashing. Do all the reads first, then all the writes.
  • Memoising everything. useMemo and useCallback cost memory and comparisons themselves; on cheap computations they only add overhead.
  • Forgetting to clean up. A listener or setInterval created on mount and never removed on unmount is a leak by construction.
  • Confusing bundle size with parse time. Even a cached file has to be parsed and compiled, so the amount of code matters on repeat visits too.
  • Micro-optimising loops instead of the algorithm. Swapping forEach for for will not rescue an O(n^2) solution, moving to a Map will.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.