← Back to Articles

JavaScript Performance Optimization

JavaScript performance directly impacts user experience. A slow JavaScript bundle means slow initial page loads. Inefficient runtime code means janky interactions. Unoptimized memory usage causes battery drain on mobile devices. Every millisecond matters.

Measuring Performance

You can't optimize what you don't measure. Chrome DevTools provides excellent profiling tools.

Open DevTools, go to the Performance tab, record a user interaction, and analyze the timeline. Look for long tasks (JavaScript execution taking more than 50ms). These block user interactions and cause jank.

Use the Lighthouse tool (also in DevTools) to measure Core Web Vitals:

Target: LCP under 2.5 seconds, FID under 100ms, CLS under 0.1.

Code Splitting

Shipping all your JavaScript upfront is wasteful. Users on the homepage don't need the checkout logic.

Modern bundlers like Vite and Webpack support automatic code splitting:

// Load the checkout module only when needed
import("./checkout.js").then(module => {
  module.initCheckout();
});

React offers suspense boundaries for automatic splitting:

const CheckoutModule = React.lazy(() => import("./Checkout"));

export default function App() {
  return (
    }>
      
    
  );
}

Every route should load its own bundle. Use dynamic imports to load features only when users access them.

Bundle Analysis

Understand what's in your bundle. Use bundle analyzers to visualize your JavaScript:

npm install -D webpack-bundle-analyzer

Check for duplicate dependencies. A package installed twice wastes kilobytes. Look for large dependencies. Some npm packages are bigger than you'd expect.

Consider alternatives. Is lodash really necessary? Native JavaScript provides map, filter, reduce. Is moment.js needed, or could you use date-fns (8x smaller)?

Tree Shaking

Modern bundlers remove unused code automatically, but only if you use ES modules:

// Good: bundler can tree-shake
import { debounce } from "lodash-es";

// Bad: entire lodash library is included
import _ from "lodash";

Always prefer named imports from packages that support tree shaking.

Minification and Compression

Ensure your build process minifies JavaScript. Minification removes whitespace, shortens variable names, and reduces file size by 30-40%.

Enable gzip or brotli compression on your server. Brotli achieves 15-20% better compression than gzip.

Most bundlers handle this automatically. Verify that production builds are minified:

// Production: 12kb minified + gzipped
// Development: 45kb unminified

Runtime Performance

Optimization doesn't end at bundle size. Runtime performance matters equally.

Avoid Layout Thrashing

Interleaving DOM reads and writes causes forced reflows:

// Bad: causes layout thrashing
elements.forEach(el => {
  el.style.width = el.offsetWidth * 2 + "px"; // Read, then write
});

Read all values first, then write:

// Good: batches reads and writes
const widths = elements.map(el => el.offsetWidth);
elements.forEach((el, i) => {
  el.style.width = widths[i] * 2 + "px";
});

Debounce and Throttle

Resize and scroll events fire hundreds of times per second. Don't run expensive logic on every event:

function debounce(fn, wait) {
  let timeoutId;
  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn(...args), wait);
  };
}

window.addEventListener(
  "resize",
  debounce(() => {
    recalculateLayout();
  }, 200)
);

Use requestAnimationFrame

For animations and visual updates, use requestAnimationFrame instead of setTimeout:

// Good: syncs with browser refresh rate
function animate() {
  updatePosition();
  requestAnimationFrame(animate);
}
animate();

// Bad: arbitrary timing
setInterval(() => {
  updatePosition();
}, 16);

Memory Leaks

Unreleased references keep objects in memory indefinitely. Common culprits:

Always clean up:

class Component {
  constructor(element) {
    this.element = element;
    this.handleClick = this.onClick.bind(this);
    this.element.addEventListener("click", this.handleClick);
  }

  destroy() {
    // Remove listener to prevent memory leak
    this.element.removeEventListener("click", this.handleClick);
  }
}

Virtual Scrolling

Rendering 10,000 list items kills performance. Virtual scrolling renders only visible items:

// Use a library like react-window or vue-virtual-scroller
import { FixedSizeList } from "react-window";

function App() {
  return (
    
      {({ index, style }) => (
        
Item {index}
)}
); }

Web Workers

Offload CPU-intensive work to background threads:

// main.js
const worker = new Worker("worker.js");
worker.postMessage({ data: largeArray });
worker.onmessage = (event) => {
  console.log("Result:", event.data);
};

// worker.js
self.onmessage = (event) => {
  const result = expensiveCalculation(event.data);
  self.postMessage(result);
};

Profiling Tools

Beyond DevTools, use these tools:

Real-World Example

Here's a checklist for optimizing a typical React application:

  1. Measure baseline performance with Lighthouse
  2. Analyze bundle size, remove unused packages
  3. Implement code splitting on routes
  4. Profile runtime with DevTools Performance tab
  5. Fix layout thrashing and forced reflows
  6. Debounce resize/scroll handlers
  7. Implement virtual scrolling for long lists
  8. Optimize images (next/image or similar)
  9. Re-measure and celebrate the wins

Performance is not a one-time optimization. It's an ongoing practice. Build measurement into your workflow, prioritize user experience, and optimize incrementally.

🛠 Essential Reading

Books on JavaScript performance. Affiliate links support this site.

You Don't Know JS Yet (Book Series)

Kyle Simpson's deep dive into JS internals. Understanding the engine helps you write faster code.

High Performance Browser Networking

Essential for understanding why network performance matters as much as code optimization.