NC Logo UseToolSuite
Web Performance

JavaScript Performance Tips Every Developer Should Know

Practical JavaScript performance tips for real-world applications. Covers DOM manipulation, event handling, memory leaks, lazy loading, and profiling techniques.

Necmeddin Cunedioglu Necmeddin Cunedioglu

Practice what you learn

JavaScript Beautifier

Try it free →

A few months ago, I profiled a React dashboard that took 8 seconds to render a table with 2,000 rows. The fix took 15 minutes — it was a re-render issue caused by creating new object references on every render cycle. The actual computation was fast; the framework was just doing 50x more work than necessary because of a subtle JavaScript pattern.

Performance issues in JavaScript are rarely about the language being slow. They’re about developers unknowingly triggering expensive operations — unnecessary DOM updates, memory leaks from forgotten event listeners, layout thrashing from interleaved reads and writes. Here are the patterns I look for when a JavaScript application feels sluggish.

DOM Manipulation: The Biggest Performance Trap

The DOM is the single most expensive thing you interact with in browser JavaScript. Every DOM read or write can trigger style recalculation, layout, and paint operations. The key is to minimize interactions and batch them when possible.

Batch DOM Writes

// Slow: triggers layout recalculation on every iteration
for (let i = 0; i < 1000; i++) {
  const div = document.createElement('div');
  div.textContent = `Item ${i}`;
  container.appendChild(div);  // triggers reflow each time
}

// Fast: build everything, then append once
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const div = document.createElement('div');
  div.textContent = `Item ${i}`;
  fragment.appendChild(div);
}
container.appendChild(fragment);  // single reflow

Using DocumentFragment reduces 1,000 reflows to 1. On a mid-range mobile device, this difference can be 200ms vs 5ms.

Avoid Layout Thrashing

Layout thrashing happens when you alternate between reading and writing DOM properties:

// Slow: forces layout recalculation on every read
items.forEach(item => {
  const height = item.offsetHeight;  // read → forces layout
  item.style.height = height * 2 + 'px';  // write → invalidates layout
});

// Fast: batch reads, then batch writes
const heights = items.map(item => item.offsetHeight);  // all reads
items.forEach((item, i) => {
  item.style.height = heights[i] * 2 + 'px';  // all writes
});

Every time you read a layout property (offsetHeight, getBoundingClientRect(), scrollTop) after modifying the DOM, the browser must recalculate layout synchronously. Batching reads and writes separately avoids this forced synchronous layout.

Event Handling: Debounce and Throttle

The Problem with Scroll and Resize Handlers

// Dangerous: fires 60+ times per second during scroll
window.addEventListener('scroll', () => {
  updateHeader();
  calculateVisibility();
  trackScrollDepth();
});

Scroll and resize events fire at the screen’s refresh rate — typically 60 times per second. If your handler takes more than 16ms, you’ll drop frames and the page will feel janky.

Debounce: Wait Until They Stop

Use debounce when you only care about the final value — like search input:

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

const handleSearch = debounce((query) => {
  fetchResults(query);
}, 300);

searchInput.addEventListener('input', (e) => handleSearch(e.target.value));

Throttle: Limit Frequency

Use throttle when you need periodic updates during continuous action — like scroll position:

function throttle(fn, limit) {
  let inThrottle;
  return (...args) => {
    if (!inThrottle) {
      fn(...args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
}

window.addEventListener('scroll', throttle(updateHeader, 100));

With throttle at 100ms, the handler fires at most 10 times per second instead of 60+.

Memory Leaks: The Silent Performance Killer

Memory leaks don’t crash your app immediately — they slowly degrade performance until the browser tab uses 2GB of RAM and starts freezing.

The Three Most Common Leaks

1. Forgotten Event Listeners

// Leak: listener is never removed
function setupWidget(element) {
  const handler = () => updateWidget(element);
  window.addEventListener('resize', handler);
  // element gets removed from DOM, but handler still references it
}

// Fix: clean up when done
function setupWidget(element) {
  const handler = () => updateWidget(element);
  window.addEventListener('resize', handler);
  return () => window.removeEventListener('resize', handler);
}

2. Closures Holding References

// Leak: the closure holds a reference to the entire dataset
function processData(hugeDataset) {
  return function getFirstItem() {
    return hugeDataset[0];  // keeps hugeDataset in memory forever
  };
}

// Fix: extract what you need
function processData(hugeDataset) {
  const firstItem = hugeDataset[0];
  return function getFirstItem() {
    return firstItem;  // only keeps the single item
  };
}

3. Detached DOM Nodes

// Leak: removed elements still referenced in JavaScript
const cache = new Map();
function removeElement(id) {
  const el = document.getElementById(id);
  cache.set(id, el);  // prevents garbage collection
  el.remove();
}

// Fix: use WeakMap or clear references
const cache = new WeakMap();

String Operations at Scale

String manipulation is fast for small inputs but can become a bottleneck with large datasets:

// Slow: creates a new string on every iteration (O(n²))
let result = '';
for (let i = 0; i < 100000; i++) {
  result += `item ${i}\n`;
}

// Fast: join an array (O(n))
const parts = [];
for (let i = 0; i < 100000; i++) {
  parts.push(`item ${i}`);
}
const result = parts.join('\n');

The array approach is 10–100x faster for large iterations because strings are immutable — every += allocates a new string and copies all previous data.

Testing regex patterns? Complex regex can cause catastrophic backtracking that freezes your browser. Test patterns in our Regex Tester before putting them in production code.

Lazy Loading and Code Splitting

Dynamic Imports

Don’t load code until it’s needed:

// Bad: all tools loaded upfront
import { JSONFormatter } from './tools/json';
import { RegexTester } from './tools/regex';
import { ImageConverter } from './tools/image';

// Good: load on demand
const tools = {
  json: () => import('./tools/json'),
  regex: () => import('./tools/regex'),
  image: () => import('./tools/image'),
};

async function loadTool(name) {
  const module = await tools[name]();
  return module.default;
}

Intersection Observer for Scroll-Based Loading

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      loadContent(entry.target);
      observer.unobserve(entry.target);
    }
  });
}, { rootMargin: '200px' });

document.querySelectorAll('.lazy-section').forEach(el => observer.observe(el));

The rootMargin: '200px' starts loading content 200px before it enters the viewport, so users never see a loading state.

Profiling: Finding the Real Bottleneck

Before optimizing anything, measure. Premature optimization based on intuition wastes time.

Chrome DevTools Performance Tab

  1. Open DevTools → Performance tab
  2. Click Record, interact with your page, then Stop
  3. Look for:
    • Long Tasks (yellow bars over 50ms) — anything blocking the main thread
    • Layout Shift indicators — unexpected reflows
    • JavaScript execution time — which functions take the most time

console.time for Quick Measurements

console.time('render');
renderDashboard(data);
console.timeEnd('render');
// Output: render: 142.3ms

Performance.now() for Precise Benchmarks

const start = performance.now();
processLargeDataset(data);
const duration = performance.now() - start;
console.log(`Processing took ${duration.toFixed(2)}ms`);

Quick Wins Checklist

These optimizations have the highest impact-to-effort ratio:

  • Use DocumentFragment for batch DOM insertions
  • Debounce search inputs (300ms delay)
  • Throttle scroll/resize handlers (100ms limit)
  • Remove event listeners when components unmount
  • Use loading="lazy" on below-fold images
  • Dynamic import heavy modules
  • Avoid innerHTML for repeated updates (use textContent)
  • Use requestAnimationFrame for visual updates
  • Cache DOM queries outside loops
  • Profile before optimizing — measure, don’t guess

Further Reading


Optimize your JavaScript with our browser-based tools: JS Beautifier for formatting, Regex Tester for pattern validation, and CSS Minifier for production-ready stylesheets.

Necmeddin Cunedioglu
Necmeddin Cunedioglu Author

Software developer and the creator of UseToolSuite. I write about the tools and techniques I use daily as a developer — practical guides based on real experience, not theory.