JavaScript: Performance and Best Practices
Learn how to write fast, memory-efficient JavaScript and apply the patterns professionals use to keep applications smooth and maintainable.
Code Optimisation Techniques
Small code habits compound into large performance gains. The most impactful optimisations are usually the simplest: cache repeated lookups, avoid unnecessary work inside loops, and let the engine do what it is good at.
General Optimisation Habits
Profile first, optimise second: fix the bottleneck you measured, not the one you guessed.
- Cache expensive values outside loops:
const len = arr.length - Prefer built-in methods (
map,filter), they are implemented natively - Avoid creating objects and arrays inside hot loops, reuse when possible
- Use
console.time()/console.timeEnd()to measure before and after
Caching and Measuring
Cache DOM references outside loops and use console.time() to measure real impact.
Press Run to execute the code and see output here.
Avoiding Global Variables
Every variable declared at the top level of a classic script becomes a property ofwindow. Too many globals create name collisions, make debugging harder, and prevent the garbage collector from freeing memory.
Global Variable Problems
Globals live for the entire page lifetime: wrap code in modules or IIFEs to contain scope.
- Any script on the page can read or overwrite your global, including third-party libraries
- Globals are never garbage-collected while the page is open
- Solution: use ES modules (automatic strict mode, file scope) or wrap in an IIFE
- If a global is truly needed, namespace it:
window.MyApp = { ... }
Containing Scope with an IIFE
Wrap module-like code in an IIFE to keep variables private and off the global object.
Press Run to execute the code and see output here.
Memory Leaks and Prevention
A memory leak happens when objects that are no longer needed still have references pointing to them, preventing the garbage collector from reclaiming the memory.
Common Memory Leak Sources
Most leaks come from forgotten event listeners, closures holding large data, or detached DOM nodes.
- Forgotten event listeners: add a listener but never call
removeEventListener - Detached DOM nodes: a removed element still referenced by a variable
- Closures over large objects: a long-lived closure keeps a large object in scope
- setInterval never cleared: the callback closure keeps its scope alive indefinitely
- Use the Chrome DevTools Memory panel to take heap snapshots and find growing objects
Preventing Memory Leaks
Remove listeners when no longer needed and always clear intervals.
Press Run to execute the code and see output here.
Debouncing and Throttling
High-frequency events like scroll, resize, and keyup can fire hundreds of times per second. Debouncing and throttling limit how often your handler actually executes.
Debounce vs Throttle
Debounce waits for silence; throttle fires on a schedule regardless of how many events fired.
- Debounce: delays execution until the event stops firing for N ms, ideal for search input
- Throttle: fires at most once every N ms, ideal for scroll and resize handlers
- Both reduce unnecessary work without losing the final user intent
- Production code often uses Lodash
_.debounce/_.throttlefor reliability
Debounce and Throttle Implementations
Debounce for search, throttle for scroll and resize.
Press Run to execute the code and see output here.
Efficient DOM Manipulation
Every DOM write can trigger a reflow or repaint. Batch your reads and writes, useDocumentFragment for bulk inserts, and prefer class toggling over inline style changes.
DOM Manipulation Tips
Minimise the number of times you touch the DOM: batch changes and separate reads from writes.
- Build complex DOM structures with
DocumentFragmentthen insert once - Prefer
classList.add/remove/toggleoverelement.style.propertyfor multiple changes - Read layout properties (offsetHeight, getBoundingClientRect) in one batch before writing
- Use
innerHTMLcarefully for bulk inserts, it destroys and recreates all children
DocumentFragment for Bulk Inserts
Build the entire list in a fragment then attach it in one operation.
Press Run to execute the code and see output here.
Event Delegation
Instead of attaching a listener to every child element, attach one listener to a common parent and use event.target to identify which child was clicked.
Event Delegation
One parent listener handles all current and future children: fewer listeners means less memory.
- Works because events bubble up from child to parent by default
- Handles dynamically added elements automatically, no need to re-attach listeners
- Use
event.target.closest("selector")to safely match the intended element - Reduces memory: 1 listener vs 1000 listeners for a long list
Event Delegation
One parent listener handles all buttons, including ones added to the DOM later.
Press Run to execute the code and see output here.
Minimising Reflows and Repaints
A reflow recalculates the layout of all or part of the page, it is expensive. A repaint redraws pixels without changing layout, cheaper but still has a cost.
Reflow vs Repaint
The worst pattern is interleaving layout reads and writes, called layout thrashing.
- Reflow triggers: changing width/height/margin/padding, adding/removing elements, reading layout properties
- Repaint triggers: changing color, background, visibility, outline, no geometry change
- Layout thrashing: read → write → read → write in a loop forces a reflow on every read
- Fix: batch all reads first, then all writes, or use
requestAnimationFrame
Avoiding Layout Thrashing
Batch all DOM reads before any writes to prevent forced reflows on every iteration.
Press Run to execute the code and see output here.
requestAnimationFrame()
requestAnimationFrame() schedules a callback to run just before the browser's next repaint, the natural slot for animation updates that keeps animations smooth at 60fps.
requestAnimationFrame()
rAF syncs your animation code with the display refresh rate: smoother than setInterval and paused when the tab is hidden.
- The callback receives a high-resolution timestamp in milliseconds
- Automatically paused when the tab is not visible, saves CPU and battery
- Call
requestAnimationFrame(loop)recursively inside the callback to animate continuously - Store the returned ID and call
cancelAnimationFrame(id)to stop the loop
requestAnimationFrame Loop
Recurse inside the callback to drive a smooth 60fps animation loop.
Press Run to execute the code and see output here.
Web Workers Basics
Web Workers run JavaScript on a background thread, keeping the main thread free for UI updates. Use them for CPU-intensive tasks like image processing, sorting large datasets, or encryption.
Web Workers
Workers communicate via message passing: they cannot access the DOM or window directly.
- Create with
new Worker("worker.js") - Send data with
worker.postMessage(data), data is copied, not shared - Receive results with
worker.onmessage = (e) => { e.data } - Terminate with
worker.terminate() - Inline workers can be created with a Blob URL, no separate file needed
Inline Web Worker
Sort 100,000 numbers on a background thread, the main thread stays free.
Press Run to execute the code and see output here.
Code Splitting and Lazy Loading
Code splitting breaks your JavaScript bundle into smaller chunks that load on demand. Lazy loading delays loading a resource until it is actually needed, reducing the initial page load time.
Code Splitting and Lazy Loading
Ship only the code the user needs right now: load the rest when they navigate to it.
- Dynamic
import()is the native code splitting mechanism - Bundlers (Vite, Webpack) automatically create separate chunk files for dynamic imports
- Lazy load images with
loading="lazy"on<img>tags - Use
IntersectionObserverto load content when it enters the viewport
Code Splitting and Lazy Images
Load routes on demand with dynamic import and images on scroll with IntersectionObserver.
Press Run to execute the code and see output here.
Minification and Bundling
Minification removes whitespace, comments, and shortens identifiers to reduce file size. Bundling combines many module files into fewer files to reduce network round-trips.
Minification and Bundling
A bundler + minifier can reduce JavaScript payload by 60–80%, dramatically improving load time.
- Minification: removes whitespace, comments, renames variables to single letters
- Bundling: merges hundreds of module files into one or a few chunks
- Tree shaking: removes unused exports from the final bundle
- Popular tools: Vite (esbuild + Rollup), Webpack, Parcel, all produce minified production builds
- Enable gzip or Brotli compression on the server for an additional 70–80% size reduction
Minification Concept
The same logic: far fewer bytes after a minifier processes the output.
Press Run to execute the code and see output here.
| Technique | What it does | When to apply |
|---|---|---|
| Debounce | Delays execution until events stop | Search input, form validation |
| Throttle | Fires at most once per interval | Scroll, resize, mouse move |
| Event delegation | One parent listener for many children | Long lists, dynamic elements |
| DocumentFragment | Batch DOM inserts in memory first | Rendering large lists |
| requestAnimationFrame | Sync animation to display refresh | CSS-in-JS animations, canvas |
| Web Worker | Offload heavy work to background thread | Sorting, parsing, encryption |
| Code splitting | Load routes/features on demand | Large SPAs, heavy libraries |
| Minification | Shrink JS bundle size for production | Every production build |
Knowledge Check
1. Why should you avoid global variables?
2. What is a memory leak in JavaScript?
3. What is the difference between debouncing and throttling?
4. What is event delegation?
5. What triggers a browser reflow?
6. What is requestAnimationFrame() used for?
7. What problem do Web Workers solve?