DEV Community

Alex Chen
Alex Chen

Posted on

JavaScript Performance Tips That Actually Make a Difference

JavaScript Performance Tips That Actually Make a Difference

Not micro-optimizations. Real performance wins for real apps.

1. DOM Updates Are Expensive (Batch Them!)

// ❌ Slow: Multiple DOM writes
const list = document.getElementById('list');
items.forEach(item => {
  const li = document.createElement('li');
  li.textContent = item.name;
  list.appendChild(li); // DOM update each iteration!
});

// ✅ Fast: Document fragment (single DOM update)
const fragment = document.createDocumentFragment();
items.forEach(item => {
  const li = document.createElement('li');
  li.textContent = item.name;
  fragment.appendChild(li); // No DOM update yet!
});
list.appendChild(fragment); // Single DOM update!

// Or even faster: innerHTML with one write
list.innerHTML = items.map(item => `<li>${escapeHtml(item.name)}</li>`).join('');
Enter fullscreen mode Exit fullscreen mode

2. Debounce and Throttle User Input

// Debounce: Wait until user stops typing
function debounce(fn, delay) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

// Throttle: Run at most once per time period
function throttle(fn, interval) {
  let lastTime = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastTime >= interval) {
      lastTime = now;
      fn.apply(this, args);
    }
  };
}

// Usage:
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce((e) => {
  fetchSearchResults(e.target.value); // Only fires after user stops typing for 300ms
}, 300));

window.addEventListener('resize', throttle(() => {
  recalculateLayout(); // Runs at most once per 200ms
}, 200));
Enter fullscreen mode Exit fullscreen mode

3. Lazy Load Everything

// Images: Native lazy loading
<img src="photo.jpg" alt="..." loading="lazy" decoding="async">

// Components: Dynamic imports
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));

// Routes: Code splitting per route
const Dashboard = () => import('./pages/Dashboard');
const Settings = () => import('./pages/Settings');

// Data: Infinite scroll / pagination
async function loadMore() {
  const spinner = showSpinner();
  const newItems = await fetchItems(currentPage + 1);
  appendItems(newItems);
  currentPage++;
  hideSpinner(spinner);
}

// Intersection Observer for visibility-based loading
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      loadContent(entry.target.dataset.src);
      observer.unobserve(entry.target);
    }
  });
});

document.querySelectorAll('[data-lazy]').forEach(el => observer.observe(el));
Enter fullscreen mode Exit fullscreen mode

4. Use requestAnimationFrame for Animations

// ❌ Bad: setInterval/setTimeout for animations
setInterval(() => {
  element.style.left = parseInt(element.style.left) + 5 + 'px';
}, 16); // Not synced with display refresh, wastes battery

// ✅ Good: requestAnimationFrame
let position = 0;

function animate() {
  position += 5;
  element.style.left = position + 'px';

  if (position < targetPosition) {
    requestAnimationFrame(animate); // Runs at optimal time (~60fps)
  }
}
requestAnimationFrame(animate);

// Benefits:
// - Synced with browser's repaint cycle
// - Pauses when tab is not visible (saves CPU/battery!)
// - Automatically adjusts to display refresh rate
Enter fullscreen mode Exit fullscreen mode

5. Web Workers for Heavy Computation

// main.js
const worker = new Worker('worker.js');

worker.postMessage({ type: 'PROCESS', data: largeDataset });

worker.onmessage = (e) => {
  console.log('Result:', e.data.result);
};

// worker.js — runs on separate thread (doesn't block UI!)
self.onmessage = function(e) {
  if (e.data.type === 'PROCESS') {
    const result = heavyComputation(e.data.data);
    self.postMessage({ result });
  }
};

function heavyComputation(data) {
  // This can take seconds without freezing the UI!
  let sum = 0;
  for (let i = 0; i < data.length; i++) {
    sum += Math.sqrt(data[i]) * Math.log(i + 1);
  }
  return sum;
}
Enter fullscreen mode Exit fullscreen mode

6. Memoize Expensive Computations

// Simple memoization
function memoize(fn) {
  const cache = new Map();

  return function(...args) {
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      return cache.get(key); // Return cached result
    }

    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

// Usage:
const expensiveFibonacci = memoize(function(n) {
  if (n <= 1) return n;
  return expensiveFibonacci(n - 1) + expensiveFibonacci(n - 2);
});

expensiveFibonacci(50); // First call: slow (computes)
expensiveFibonacci(50); // Second call: instant (cached!)

// In React: useMemo / useCallback
const sortedItems = useMemo(
  () => [...items].sort((a, b) => b.date - a.date),
  [items] // Only recompute when items changes
);

const handleClick = useCallback(
  () => doSomething(userId),
  [userId] // Only recreate when userId changes
);
Enter fullscreen mode Exit fullscreen mode

7. Virtual Scrolling for Long Lists

// Instead of rendering 10,000 DOM elements:
// Only render the ~20 that are visible in the viewport

function VirtualList({ items, itemHeight = 50, containerHeight }) {
  const [scrollTop, setScrollTop] = useState(0);

  const startIndex = Math.floor(scrollTop / itemHeight);
  const endIndex = Math.min(
    startIndex + Math.ceil(containerHeight / itemHeight) + 2,
    items.length
  );

  const visibleItems = items.slice(startIndex, endIndex).map((item, i) => ({
    item,
    index: startIndex + i,
  }));

  const totalHeight = items.length * itemHeight;
  const offsetY = startIndex * itemHeight;

  return (
    <div
      style={{ height: containerHeight, overflow: 'auto' }}
      onScroll={(e) => setScrollTop(e.target.scrollTop)}
    >
      <div style={{ height: totalHeight, position: 'relative' }}>
        <div style={{ transform: `translateY(${offsetY}px)` }}>
          {visibleItems.map(({ item, index }) => (
            <ListItem key={index} item={item} height={itemHeight} />
          ))}
        </div>
      </div>
    </div>
  );
}

// Libraries: react-window, react-virtualized, tanstack-virtual
Enter fullscreen mode Exit fullscreen mode

8. Optimize Loops

const arr = /* huge array */;

// ❌ Slower patterns:
for (let i = 0; i < arr.length; i++) { }     // Checks .length every iteration
arr.forEach(item => { });                       // Function call overhead
arr.map(item => { });                           // Creates new array even if not needed

// ✅ Faster patterns:
for (let i = 0, len = arr.length; i < len; i++) { } // Cache length
for (let i = arr.length; i--; ) { }                // Reverse loop (faster comparison)
for (const item of arr) { }                        // Clean and fast (modern engines optimize well)

// For nested loops: break out early when possible
function findMatch(items, target) {
  outer: for (let i = 0; i < items.length; i++) {
    for (let j = 0; j < items[i].length; j++) {
      if (items[i][j] === target) return { i, j }; // Found! Exit immediately
    }
  }
  return null;
}
Enter fullscreen mode Exit fullscreen mode

Performance Measurement Tools

// High-resolution timing
console.time('operation');
doSomething();
console.timeEnd('operation'); // "operation: 123.456ms"

// Performance API (more precise)
const start = performance.now();
heavyOperation();
const duration = performance.now() - start;
console.log(`Took ${duration.toFixed(2)}ms`);

// Performance marks (in DevTools Performance tab)
performance.mark('fetch-start');
await fetchData();
performance.mark('fetch-end');
performance.measure('fetch', 'fetch-start', 'fetch-end');

// Memory monitoring
const used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(`Memory: ${Math.round(used)}MB`);
Enter fullscreen mode Exit fullscreen mode

What's your biggest performance win? Share it below!

Follow @armorbreak for more JS content.

Top comments (0)