JavaScript Performance

Optimizing JavaScript performance improves user experience. Follow these practices for faster code.

Performance Tips

Minimize DOM manipulation because accessing and modifying the DOM is expensive—it's much slower than regular JavaScript operations. Each time you modify the DOM, the browser may need to recalculate styles, layout, and repaint the page. If you're adding 100 items to a list, don't call appendChild() 100 times. Instead, build up an HTML string or use a DocumentFragment, then insert everything at once. Batch DOM changes together and minimize the number of times you touch the DOM. Reading from the DOM (like offsetHeight) can also trigger expensive reflows.

Cache DOM queries instead of repeatedly querying for the same element. document.getElementById(), querySelector(), and similar methods search through the DOM tree, which takes time. If you use the same element multiple times, query it once and store it in a variable: const button = document.getElementById('submit'). This is especially important in loops—querying inside a loop that runs 1000 times means 1000 DOM searches. Cache elements outside the loop and reuse the reference.

Use event delegation instead of attaching event listeners to many individual elements. If you have 100 buttons and attach a click listener to each one, you create 100 listener registrations, which uses memory and slows down the page. Instead, attach a single listener to a parent element and use event.target to determine which child was clicked. This pattern uses less memory, improves performance, and automatically handles dynamically added elements without needing to re-attach listeners.

Avoid global variables because they clutter the global namespace, increase the chance of naming conflicts, and make JavaScript slower. Every time you access a variable, JavaScript looks through the scope chain starting from the current scope and working outward to the global scope. Local variables are faster to access than globals. Additionally, global variables aren't garbage collected until the page unloads, potentially wasting memory. Wrap code in functions or modules to keep variables in local scope.

Use const and let instead of var for better performance and code clarity. While the performance difference is minimal, const and let have block scope, which makes it easier for JavaScript engines to optimize your code. More importantly, const prevents reassignment, which helps engines optimize better because they know the reference won't change. Block scope also keeps variables in a smaller scope, making them eligible for garbage collection sooner. Modern JavaScript should use const by default and let when reassignment is needed.

Minimize unnecessary loops and iterations by using appropriate data structures and algorithms. Nested loops can create performance problems quickly—a loop inside another loop makes complexity O(n²), which gets slow with large datasets. Use array methods like map, filter, and find that express intent clearly and can be optimized by engines. Consider whether you need to process all items or can exit early. Use efficient algorithms—for example, use a Set for lookups (O(1)) instead of array.includes() (O(n)).

// Bad: repeated DOM queries
for (let i = 0; i < 100; i++) {
  document.getElementById("output").innerHTML += i;
}

// Good: cache DOM reference
const output = document.getElementById("output");
let html = "";
for (let i = 0; i < 100; i++) {
  html += i;
}
output.innerHTML = html;

// Bad: attach many event listeners
const buttons = document.querySelectorAll("button");
buttons.forEach(btn => {
  btn.addEventListener("click", handleClick);
});

// Good: event delegation
document.addEventListener("click", (e) => {
  if (e.target.matches("button")) {
    handleClick(e);
  }
});

Optimization Techniques

Debounce expensive operations that happen frequently, like search-as-you-type or window resize handlers. Debouncing delays execution until after a pause in events—for example, waiting 300ms after the user stops typing before making an API request. This prevents making hundreds of requests while the user is actively typing. Without debouncing, rapid events can overwhelm your application and the server. A debounce function wraps your handler and only executes it after the specified delay has passed without new events. This dramatically reduces unnecessary work.

Use Web Workers for heavy computational tasks that would otherwise block the main thread and freeze the UI. JavaScript is single-threaded, so intensive calculations (like image processing, large dataset parsing, or complex algorithms) make the page unresponsive. Web Workers run JavaScript in background threads, allowing the main thread to handle UI interactions smoothly. You send data to workers, they perform the computation, and send results back via messages. This keeps your UI responsive even during heavy processing.

Lazy load resources that aren't immediately needed when the page first loads. Don't load images below the fold, scripts for features users might not use, or data for tabs that aren't visible. Use the Intersection Observer API to load images as they come into view, dynamically import() JavaScript modules only when needed, and fetch data on-demand rather than all upfront. Lazy loading dramatically improves initial page load time, which is crucial for user experience and SEO. Users on slow connections especially benefit.

Minimize reflows and repaints by batching DOM changes and being careful about what properties you modify. Reflows (layout recalculation) are triggered by changing dimensions, positions, or visibility, and are expensive. Repaints (visual updates) are triggered by color or background changes and are cheaper but still costly. Reading layout properties like offsetHeight or getComputedStyle() can force a synchronous reflow. Batch DOM reads together and DOM writes together, and use CSS transforms/opacity for animations (which use the compositor thread) instead of changing position/size properties.

Use efficient data structures for your use case. Objects are great for key-value lookups but slow for checking if they contain a value. Arrays work for ordered lists but array.includes() is O(n). Sets provide fast lookups, addition, and deletion (all O(1)), and are perfect for unique value collections. Maps are better than objects for frequent additions/deletions and maintain insertion order. For large datasets, choosing the right data structure can make the difference between instant and slow operations. WeakMaps and WeakSets allow garbage collection of keys.

Profile code to find actual bottlenecks instead of guessing what's slow. Use browser DevTools Performance tab to record and analyze your application. The profiler shows exactly where time is spent—which functions are called most, which take longest, and where the browser is busy with layout/paint. Focus optimization efforts on the actual slow parts, not on micro-optimizations that don't matter. Premature optimization wastes time—measure first, then optimize the bottlenecks. Tools like console.time(), Chrome DevTools, and Lighthouse help identify real performance issues.

// Debounce function
function debounce(func, delay) {
  let timeout;
  return function(...args) {
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(this, args), delay);
  };
}

// Use debounce for search
const searchInput = document.getElementById("search");
searchInput.addEventListener("input", debounce(performSearch, 300));

// Use Set for unique values (faster than array)
const uniqueIds = new Set();
uniqueIds.add(1);
uniqueIds.add(2);
uniqueIds.add(1); // Ignored

// Use Map for key-value pairs (faster lookup)
const userMap = new Map();
userMap.set("user1", { name: "John" });
const user = userMap.get("user1"); // O(1) lookup