DEV Community

Alex Chen
Alex Chen

Posted on

JavaScript Performance: 8 Fixes That Actually Matter (2026)

JavaScript Performance: 8 Fixes That Actually Matter (2026)

Not all performance optimizations are equal. These 8 fixes give you the biggest bang for your buck.

Fix #1: Debounce & Throttle Expensive Operations

// Problem: Search fires on every keystroke → API calls on every key
// Solution: Debounce — wait until user stops typing

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

const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce(async (e) => {
  const results = await fetch(`/api/search?q=${e.target.value}`);
  displayResults(results);
}, 300)); // Wait 300ms after last keystroke

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

window.addEventListener('scroll', throttle(() => {
  updateScrollProgress();
}, 100)); // At most once every 100ms

// When to use which:
// Debounce: search input, resize end, form auto-save
// Throttle: scroll handlers, button click spam, mouse move tracking
Enter fullscreen mode Exit fullscreen mode

Fix #2: Lazy Load Everything Possible

<!-- Native lazy loading for images -->
<img src="placeholder.jpg" data-src="photo.jpg" loading="lazy" alt="Photo" />

<!-- Intersection Observer for any element -->
<script>
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      // Load the heavy content
      entry.target.src = entry.target.dataset.src;
      observer.unobserve(entry.target); // Don't observe again
    }
  });
}, { rootMargin: '200px' }); // Start loading 200px before visible

document.querySelectorAll('[data-src]').forEach(el => observer.observe(el));
</script>

<!-- Dynamic imports for code splitting -->
// Instead of importing everything upfront:
import { HeavyChart } from './chart';        // ❌ Blocks initial load
import { DataGrid } from './data-grid';       // ❌ Blocks initial load

// Use dynamic imports:
const chartModule = import('./chart');        // ✅ Loaded when needed
button.addEventListener('click', async () => {
  const { HeavyChart } = await chartModule;   // ✅ Fetched on demand
  new HeavyChart(container);
});

// React.lazy + Suspense (if using React)
const Chart = React.lazy(() => import('./Chart'));
<Suspense fallback={<Spinner />}>
  <Chart data={data} />
</Suspense>
Enter fullscreen mode Exit fullscreen mode

Fix #3: Virtualize Long Lists

// Problem: Rendering 10,000 rows → DOM is huge, scrolling is slow
// Solution: Only render what's visible + small buffer

class VirtualList {
  constructor(container, items, rowHeight = 50) {
    this.container = container;
    this.items = items;
    this.rowHeight = rowHeight;
    this.visibleCount = Math.ceil(container.clientHeight / rowHeight) + 5; // +5 buffer

    container.addEventListener('scroll', () => this.render());
    window.addEventListener('resize', () => this.render());
    this.render();
  }

  render() {
    const scrollTop = this.container.scrollTop;
    const startIndex = Math.max(0, Math.floor(scrollTop / this.rowHeight) - 2); // -2 buffer above
    const endIndex = Math.min(this.items.length, startIndex + this.visibleCount);

    const fragment = document.createDocumentFragment();
    for (let i = startIndex; i < endIndex; i++) {
      const row = document.createElement('div');
      row.style.height = `${this.rowHeight}px`;
      row.style.position = 'absolute';
      row.style.top = `${i * this.rowHeight}px`;
      row.textContent = this.items[i];
      fragment.appendChild(row);
    }

    this.container.innerHTML = '';
    this.container.appendChild(fragment);
    this.container.style.height = `${this.items.length * this.rowHeight}px`;
  }
}

// Or use a library:
// react-window, react-virtualized, tanstack-virtual
Enter fullscreen mode Exit fullscreen mode

Fix #4: Optimize Images Properly

// The image optimization checklist:

// 1. Right format
// Photos → WebP or AVIF (30-70% smaller than JPEG)
// Icons/logos → SVG (infinitely scalable)
// Simple graphics → PNG with palette
// Animated → WebP animation (replaces GIF)

// 2. Right size (don't serve 4000px image for 300px display!)
<img srcset="
  photo-320w.webp 320w,
  photo-640w.webp 640w,
  photo-1280w.webp 1280w
" sizes="(max-width: 600px) 100vw, 50vw"
src="photo-640w.webp"
alt="Description"
loading="lazy"

// 3. Blur placeholder (perceived performance!)
<img
  style="background-size: cover; background-position: center;"
  src="photo.jpg"
  alt="Photo"
  onload="this.style.background='none'"
/>

// 4. CDN + caching
// Set far-future Cache-Control for hashed assets
Cache-Control: public, max-age=31536000, immutable

// 5. Compression quality
// JPEG: quality 75-85 (sweet spot)
// WebP: quality 80-90
// Don't go higher — human eye can't tell the difference but file size doubles
Enter fullscreen mode Exit fullscreen mode

Fix #5: Reduce JavaScript Bundle Size

# Analyze your bundle
npx webpack-bundle-analyzer build/stats.json
# See exactly what's in your bundle and how big each module is

# Tree shaking (remove unused code)
// ✅ ES modules enable tree shaking
export { usedFn } from './utils';
// ❌ CommonJS does NOT tree shake
module.exports = { usedFn, unusedFn }; // Both included!

# Code splitting strategies
// Route-based: Each page loads its own chunk
const Home = () => import('./pages/Home');
const Dashboard = () => import('./pages/Dashboard');

// Vendor chunks: Separate third-party code (changes rarely, caches long)
optimization: {
  splitChunks: {
    cacheGroups: {
      vendor: {
        test: /[\\/]node_modules[\\/]/,
        name: 'vendors',
        chunks: 'all',
      },
    },
  },
}

# Remove unused dependencies
npx depcheck          # Find deps in package.json not used in code
npm outdated           # Find outdated packages that might be smaller
Enter fullscreen mode Exit fullscreen mode

Fix #6: Efficient DOM Manipulation

// ❌ Slow: Multiple DOM operations (each triggers reflow/repaint)
for (let i = 0; i < 1000; i++) {
  const el = document.createElement('div');
  el.textContent = `Item ${i}`;
  document.getElementById('list').appendChild(el);
}
// Result: 1000 reflows!

// ✅ Fast: Document fragment (single reflow)
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const el = document.createElement('div');
  el.textContent = `Item ${i}`;
  fragment.appendChild(el);
}
document.getElementById('list').appendChild(fragment);
// Result: 1 reflow!

// ✅ Faster: innerHTML (browser parses HTML in one pass)
const items = Array.from({length: 1000}, (_, i) => `<div>Item ${i}</div>`).join('');
document.getElementById('list').innerHTML = items;

// Batch reads/writes (avoid layout thrashing)
// ❌ Bad: Read/write interleaved (forces synchronous layout)
el.style.left = el.offsetLeft + 10 + 'px'; // Read offsetLeft → reflow → write
el.style.top = el.offsetTop + 10 + 'px';    // Read offsetTop → reflow → write

// ✅ Good: All reads first, then all writes
const left = el.offsetLeft;  // Read once
const top = el.offsetTop;    // Read once
el.style.left = left + 10 + 'px';  // Write
el.style.top = top + 10 + 'px';    // Write
// Result: 1 reflow instead of 2+

// Use CSS transforms for animations (GPU accelerated, no reflow)
el.style.transform = 'translateX(100px)';  // ✅ Composited layer
el.style.left = '100px';                   // ❌ Triggers layout
Enter fullscreen mode Exit fullscreen mode

Fix #7: Caching Strategy

// Browser caching (HTTP headers)
// Static assets (with content hash in filename):
Cache-Control: public, max-age=31536000, immutable  // Cache forever

// API responses:
Cache-Control: no-cache                              // Always revalidate
ETag: "v1-abc123"                                    // For conditional requests

// Service Worker cache (offline support + speed)
const CACHE_NAME = 'app-v1';
const ASSETS = ['/', '/styles.css', '/app.js'];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache => cache.addAll(ASSETS))
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then(cached => {
      // Return cached version, update cache in background (stale-while-revalidate)
      const fetched = fetch(event.request).then(response => {
        caches.open(CACHE_NAME).then(cache => cache.put(event.request, response));
        return response.clone();
      });
      return cached || fetched;
    })
  );
});

// Application-level cache (avoid redundant API calls)
const apiCache = new Map();
const CACHE_TTL = 60 * 1000; // 1 minute

async function cachedFetch(url) {
  const cached = apiCache.get(url);
  if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
    return cached.data; // Return from cache
  }

  const data = await fetch(url).then(r => r.json());
  apiCache.set(url, { data, timestamp: Date.now() });
  return data;
}
Enter fullscreen mode Exit fullscreen mode

Fix #8: Measure Before Optimizing

// DON'T GUESS. MEASURE.

// Chrome DevTools Performance tab:
// 1. Open DevTools → Performance
// 2. Click Record → interact with page → Stop
// 3. Look for: Long tasks (>50ms), Layout shifts, Paint time

// Performance API (in your code):
const start = performance.now();

// ... do something expensive ...

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

// User Timing API (shows up in DevTools!):
performance.mark('fetch-start');
await fetchData();
performance.mark('fetch-end');
performance.measure('fetch-duration', 'fetch-start', 'fetch-end');

// Long Task API (detect jank):
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.warn(`Long task detected: ${entry.duration}ms`);
    // Report to analytics to track real-user performance!
  }
});
observer.observe({ entryTypes: ['longtask'] });

// Core Web Vitals (what Google measures):
// LCP (Largest Contentful Paint): < 2.5s
// INP (Interaction to Next Paint): < 200ms
// CLS (Cumulative Layout Shift): < 0.1

// Measure them:
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(`${entry.name}: ${entry.value}`);
  }
});
observer.observe({ entryTypes: ['largest-contentful-paint', 'first-input'] });
Enter fullscreen mode Exit fullscreen mode

Which fix would help YOUR app the most? What's your #1 performance tip?

Follow @armorbreak for more practical developer guides.

Top comments (0)