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.

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.

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.

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 / _.throttle for reliability

Debounce and Throttle Implementations

Debounce for search, throttle for scroll and resize.

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 DocumentFragment then insert once
  • Prefer classList.add/remove/toggle over element.style.property for multiple changes
  • Read layout properties (offsetHeight, getBoundingClientRect) in one batch before writing
  • Use innerHTML carefully 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.

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.

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.

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.

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.

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 IntersectionObserver to 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.

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.

TechniqueWhat it doesWhen to apply
DebounceDelays execution until events stopSearch input, form validation
ThrottleFires at most once per intervalScroll, resize, mouse move
Event delegationOne parent listener for many childrenLong lists, dynamic elements
DocumentFragmentBatch DOM inserts in memory firstRendering large lists
requestAnimationFrameSync animation to display refreshCSS-in-JS animations, canvas
Web WorkerOffload heavy work to background threadSorting, parsing, encryption
Code splittingLoad routes/features on demandLarge SPAs, heavy libraries
MinificationShrink JS bundle size for productionEvery 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?