DEV Community

Alex Chen
Alex Chen

Posted on

JavaScript Performance: Making Your Apps Fast (2026)

JavaScript Performance: Making Your Apps Fast (2026)

Performance isn't about premature optimization — it's about understanding what makes JavaScript slow and knowing how to fix it when it matters.

Measuring Performance Correctly

// ❌ Wrong ways to measure:
const start = Date.now();        // Low resolution (ms only)
doSomething();
console.log(Date.now() - start); // Inaccurate

// ✅ Right way: Performance API (microsecond precision!)
const { performance, PerformanceObserver } = require('perf_hooks');

// Measure a specific operation:
function measure(name, fn) {
  const start = performance.now();
  const result = fn();
  const duration = performance.now() - start;
  console.log(`${name}: ${duration.toFixed(2)}ms`);
  return result;
}

// Mark + measure for complex flows:
performance.mark('fetch-start');
await fetchData();
performance.mark('parse-start');
parseData();
performance.mark('render-start');
renderUI();
performance.mark('done');

performance.measure('fetch', 'fetch-start', 'parse-start');
performance.measure('parse', 'parse-start', 'render-start');
performance.measure('render', 'render-start', 'done');

// Get all measurements:
const entries = performance.getEntriesByType('measure');
entries.forEach(e => console.log(`${e.name}: ${e.duration.toFixed(2)}ms`));

// Observer pattern (non-blocking monitoring):
const obs = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 100) {
      console.warn(`Slow operation: ${entry.name} took ${entry.duration.toFixed(0)}ms`);
    }
  }
});
obs.observe({ entryTypes: ['measure'] });
Enter fullscreen mode Exit fullscreen mode

The V8 Engine & Hidden Classes

// V8 uses "hidden classes" to optimize property access.
// When objects have the same shape → same hidden class → optimized!

// ✅ Same shape (FAST):
class Point {
  constructor(x, y) { this.x = x; this.y = y; }
}
const points = Array.from({ length: 100000 }, () => new Point(
  Math.random(), Math.random()
));
// All Point instances share the same hidden class → blazing fast!

// ❌ Different shapes (SLOW):
const mixed = [
  { x: 1, y: 2 },
  { x: 1, y: 2, z: 3 },     // Different shape!
  { x: 1 },                   // Different shape!
  { x: 1, y: 2, color: 'red' } // Different shape!
];
// Each object gets its own hidden class → deoptimized access!

// Rule: Initialize ALL properties in the constructor,
// even if they're null/undefined initially.
// Don't add properties after construction in hot loops.

// Arrays: Use typed arrays for numeric data:
const regularArray = [1, 2, 3, 4, 5];           // Generic array
const typedArray = new Float64Array([1, 2, 3, 4, 5]); // Typed, contiguous memory
// Typed arrays are 3-10x faster for numeric operations and use less memory!
Enter fullscreen mode Exit fullscreen mode

Async Performance Patterns

// ❌ Sequential awaits (slow — each waits for the previous):
async function getUserData(userId) {
  const user = await db.users.findById(userId);      // Wait...
  const posts = await db.posts.findByUser(userId);   // Then wait...
  const comments = await db.comments.findFor(posts);  // Then wait...
  return { user, posts, comments };
}
// Total time: sum of all three operations

// ✅ Parallel awaits (fast — all run simultaneously):
async function getUserDataFast(userId) {
  const [user, posts, comments] = await Promise.all([
    db.users.findById(userId),
    db.posts.findByUser(userId),
    db.comments.findForPosts(userId),
  ]);
  return { user, posts, comments };
}
// Total time: max of the three operations (usually 3x faster!)

// ✅ Concurrent with limit (don't overwhelm external APIs):
async function fetchAll(urls, concurrency = 5) {
  const results = [];
  const executing = [];

  for (const url of urls) {
    const promise = fetch(url).then(r => r.json()).then(data => {
      results.push({ url, data });
      executing.splice(executing.indexOf(promise), 1);
    });

    executing.push(promise);
    if (executing.length >= concurrency) {
      await Promise.race(executing); // Wait for one to finish before starting next
    }
  }

  await Promise.all(executing); // Wait for remaining
  return results;
}

// Worker threads for CPU-intensive tasks:
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');

if (isMainThread) {
  function runInWorker(path, data) {
    return new Promise((resolve, reject) => {
      const worker = new Worker(__filename, { workerData: data });
      worker.on('message', resolve);
      worker.on('error', reject);
      worker.on('exit', (code) => {
        if (code !== 0) reject(new Error(`Worker stopped with code ${code}`));
      });
    });
  }

  async function processLargeDataset(dataset) {
    const result = await runInWorker('./heavy-computation.js', dataset);
    return result;
  }
} else {
  // This runs in the worker thread (doesn't block main event loop!)
  const result = heavyComputation(workerData);
  parentPort.postMessage(result);
}
Enter fullscreen mode Exit fullscreen mode

Memory Management & Leaks

// Common memory leak patterns:

// Leak #1: Event listeners never removed
function setupLeak() {
  const element = document.getElementById('button');
  element.addEventListener('click', () => doSomething());
  // If this function is called repeatedly without cleanup → leak!
}
// Fix: Store reference and remove later, or use AbortController:
function setupSafe() {
  const controller = new AbortController();
  const element = document.getElementById('button');
  element.addEventListener('click', () => doSomething(), { signal: controller.signal });
  // Cleanup: controller.abort() removes all listeners at once
  return controller; // Call .abort() when done
}

// Leak #2: Closures holding references
function createLeakyFunction() {
  const hugeData = new Array(1000000).fill('data'); // Large array
  return function() {
    console.log(hugeData[0]); // Closure keeps hugeData alive forever!
  };
}
// Fix: Null out large references after use:
function createSafeFunction() {
  let hugeData = new Array(1000000).fill('data');
  return function() {
    const result = hugeData[0];
    hugeData = null; // Release reference!
    return result;
  };
}

// Leak #3: Map/Set as caches without bounds
const cache = new Map();
function getData(id) {
  if (!cache.has(id)) cache.set(id, fetchHugeData(id)); // Grows forever!
  return cache.get(id);
}
// Fix: Use LRU cache or set max size:
const MAX_CACHE_SIZE = 1000;
function getCachedData(id) {
  if (!cache.has(id)) {
    if (cache.size >= MAX_CACHE_SIZE) {
      // Remove oldest entry (Map preserves insertion order)
      const oldestKey = cache.keys().next().value;
      cache.delete(oldestKey);
    }
    cache.set(id, fetchHugeData(id));
  }
  return cache.get(id);
}

// Monitoring memory usage:
setInterval(() => {
  const mem = process.memoryUsage();
  console.log({
    rss: `${(mem.rss / 1024 / 1024).toFixed(0)}MB`,   // Resident Set Size
    heapUsed: `${(mem.heapUsed / 1024 / 1024).toFixed(0)}MB`,
    heapTotal: `${(mem.heapTotal / 1024 / 1024).toFixed(0)}MB`,
    external: `${(mem.external / 1024 / 1024).toFixed(0)}MB`,
  });
}, 30000);

// Force garbage collection (only with --expose-gc flag):
if (global.gc) global.gc();
Enter fullscreen mode Exit fullscreen mode

DOM Performance

// ❌ Slow: Multiple DOM operations (reflow + repaint each time!)
for (let i = 0; i < 1000; i++) {
  const div = document.createElement('div');
  div.textContent = `Item ${i}`;
  document.body.appendChild(div); // Reflow + repaint every iteration!
}

// ✅ Fast: Batch DOM operations (single reflow + repaint)
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const div = document.createElement('div');
  div.textContent = `Item ${i}`;
  fragment.appendChild(div); // No reflow! Fragment is not in DOM yet.
}
document.body.appendChild(fragment); // Single reflow + repaint

// ✅ Even faster: innerHTML for bulk content (one parse operation):
document.body.innerHTML += `<div>${items.map(i => `Item ${i}`).join('</div><div>')}</div>`;

// Layout thrashing: Reading/writing alternately causes multiple reflows
// ❌ Bad (layout thrashing):
elements.forEach(el => {
  el.style.height = el.scrollHeight + 'px'; // Read → Write (reflow!)
});

// ✅ Good (batch reads, then batch writes):
const heights = elements.map(el => el.scrollHeight); // Read all first
elements.forEach((el, i) => {
  el.style.height = heights[i] + 'px';            // Write all after
});

// requestAnimationFrame for visual updates:
function animateScroll(targetY) {
  function step() {
    const currentY = window.scrollY;
    const diff = targetY - currentY;
    if (Math.abs(diff) < 1) return;
    window.scrollTo(0, currentY + diff * 0.1);
    requestAnimationFrame(step); // Browser decides optimal timing
  }
  requestAnimationFrame(step);
}
Enter fullscreen mode Exit fullscreen mode

Quick Wins Checklist

Before optimizing anything:

1. Profile first! Never guess where the bottleneck is
2. Optimize the critical path (what user sees first)
3. Common quick wins:
   □ Defer non-critical JS (<script defer>, dynamic import())
   □ Minify + compress assets (gzip/brotli)
   □ Add caching headers (Cache-Control, ETag)
   □ Lazy-load images (loading="lazy", IntersectionObserver)
   □ Use CSS containment (contain: layout paint)
   □ Virtualize long lists (react-virtualized, etc.)
   □ Debounce/throttle scroll/resize/input handlers
   □ Use web workers for heavy computation
   □ Enable HTTP/2 or HTTP/3 on your server
   □ Preload critical resources (<link rel="preload">)

4. What NOT to optimize:
   - Code that runs once at startup
   - Error handling paths (rarely executed)
   - Developer experience over micro-optimizations
   - Anything you haven't measured
Enter fullscreen mode Exit fullscreen mode

What's the biggest performance win you've ever achieved? What performance myth drives you crazy?

Follow @armorbreak for more practical developer guides.

Top comments (0)