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:
SetandMapinstead 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
// 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) withoutsetTimeoutorrequestIdleCallback; - parsing or processing large JSON payloads (
JSON.parse(hugeData)); - computationally heavy algorithms (sorting, recursion, encryption);
- synchronous requests (
XMLHttpRequestwithout 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,getComputedStyleorscrollToptriggers 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
reflowby 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
styleandtransformtoo 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
requestAnimationFrameinstead 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
WeakMapandWeakSet; - 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()andArray.shift()calls, which are expensive operations.
What to do:
- use more suitable structures:
Set,Map,WeakMap; - use iterators, generators and
for...ofinstead offorEachon 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:
Setfor uniqueness,Mapfor 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-Controlheader; - 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()andperformance.measure();- Lighthouse, Web Vitals;
- Sentry Performance, New Relic, Datadog.
A short summary:
| Category | Example problem | How to fix it |
|---|---|---|
| Thread blocking | A long loop | Split it up, use a Worker |
| DOM | Too many reflows | Batch updates |
| Memory | Leaks | WeakMap, cleanup |
| Loops | O(n^2) | Optimise |
| Network | Repeated requests | Cache |
| Rendering | FPS < 60 | GPU animations |
| Bundle | 2 MB of JS | Tree-shaking |
| Algorithms | Wrong structure | Match 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
asyncas a cure for blocking.async/awaitdoes not move computation to another thread: a heavy loop inside anasyncfunction 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.
useMemoanduseCallbackcost memory and comparisons themselves; on cheap computations they only add overhead. - Forgetting to clean up. A listener or
setIntervalcreated 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
forEachforforwill not rescue anO(n^2)solution, moving to aMapwill.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.