Javascript · Performance

Performance Interview Questions
With Answers & Code Examples

9 carefully curated Performance interview questions with working code examples and real interview gotchas.

Practice Interactively →← All Categories
9 questions0 beginner5 core4 advanced
Q1Core

What are debounce and throttle? When do you use each?

💡 Hint: Debounce=wait for pause; throttle=limit rate
  • Debounce — fires AFTER user stops triggering. Best for: search input, resize, form validation
  • Throttle — fires at most once per interval. Best for: scroll, mouse move
function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}
const onSearch = debounce((q) => fetchResults(q), 300);
💡 Debounce = waits for storm to pass. Throttle = steady drip.
Practice this question →
Q2Core

What is React.memo and when does it actually prevent re-renders?

💡 Hint: Shallow prop comparison — skips re-render if props are shallowly equal. Useless without stable prop references.

React.memo wraps a component and shallowly compares old and new props. Skips the re-render if props haven't changed.

const ExpensiveList = React.memo(function ExpensiveList({ items, onSelect }) {
  // Only re-renders if items or onSelect reference changes
  return items.map(item => );
});

// ─── For memo to work, props must be stable references ────────────────────
function Parent() {
  const [count, setCount] = useState(0);

  // ❌ New array reference every render → memo useless
  const items = [{ id: 1 }, { id: 2 }];
  const onSelect = (id) => console.log(id);

  // ✅ Stable references
  const items = useMemo(() => [{ id: 1 }, { id: 2 }], []);
  const onSelect = useCallback((id) => console.log(id), []);

  return ;
}

Custom comparison:

const List = React.memo(Component, (prev, next) => {
  return prev.items.length === next.items.length; // custom equality
});
💡 React.memo is not free — it adds a comparison on every render. Only add it to components that are measurably slow or render very frequently. Profile with React DevTools Profiler first.
Practice this question →
Q3Core

What is code splitting and lazy loading in React?

💡 Hint: React.lazy + Suspense — load component code only when needed, reduces initial bundle
import { lazy, Suspense } from 'react';

// ❌ Eager import — bundled in the main chunk even if rarely used
import HeavyChart from './HeavyChart';

// ✅ Lazy — loaded only when rendered for the first time
const HeavyChart = lazy(() => import('./HeavyChart'));
const AdminPanel = lazy(() => import('./AdminPanel'));

// Suspense shows fallback while the chunk loads
function Dashboard() {
  return (
    }>
      
    
  );
}

// Route-based code splitting — most impactful
const Home    = lazy(() => import('./pages/Home'));
const Profile = lazy(() => import('./pages/Profile'));
const Admin   = lazy(() => import('./pages/Admin'));

function App() {
  return (
    }>
      
        } />
        } />
        } />
      
    
  );
}
💡 Route-level splitting is the highest-impact optimization. Admin panels, dashboards, and settings pages are perfect candidates — users on the landing page never need that code.
Practice this question →
Q4Advanced

What is virtualization (windowing) and when do you need it?

💡 Hint: Render only visible items in a long list — react-window / react-virtual renders a DOM window into a large dataset

Virtualization renders only the items visible in the viewport instead of the entire list. For 10,000 items, you might only render 20 DOM nodes at a time.

import { FixedSizeList } from 'react-window';

// ❌ Renders all 10,000 DOM nodes immediately
function NaiveList({ items }) {
  return items.map(item => );
}

// ✅ Renders ~20 DOM nodes, recycles them as you scroll
function VirtualList({ items }) {
  const Row = ({ index, style }) => (
    
{/* style positions the row */} {items[index].name}
); return ( {Row} ); }

When to virtualize:

  • Lists with 100+ items where each item has significant DOM
  • Visible performance issues (jank, slow initial render)
  • Tables, feeds, chat history, autocomplete dropdowns

Libraries: react-window (smaller), react-virtual (TanStack, headless), react-virtuoso (variable heights)

💡 Pagination or infinite scroll at ~50 items is often enough and simpler. Reach for virtualization when you genuinely have 500+ items that must all be available without pagination.
Practice this question →
Q5Advanced

What causes unnecessary re-renders and how do you diagnose them?

💡 Hint: Parent renders, new object/function props, context value changes — diagnose with React DevTools Profiler

Common causes:

  • Parent re-renders → all children re-render (even with same props, unless memoized)
  • New object/array/function created inline → reference changes → memo fails
  • Context value is a new object reference → all consumers re-render
  • State updates that produce the same value (React bails out after first redundant render)
// ❌ New object every render — breaks memo
  // new {} each time
 doWork()} />  // new fn each time
         // new [] each time

// ❌ Context providing unstable reference
function Provider({ children }) {
  const [user, setUser] = useState(null);
  return (
      {/* new object every render! */}
      {children}
    
  );
}

// ✅ Stable context value
const value = useMemo(() => ({ user, setUser }), [user]);

Diagnosis tools:

  • React DevTools → Profiler → record interaction → see what rendered and why
  • React DevTools → Components → highlight updates checkbox
  • why-did-you-render library — logs unnecessary renders with reasons
💡 Re-renders are not always a problem — React is fast. Optimize only when you measure a real performance issue. Premature memoization adds complexity and can introduce bugs.
Practice this question →
Q6Advanced

What is the React Profiler and how do you use it to diagnose performance issues?

💡 Hint: React DevTools Profiler records renders — shows which component rendered, why, and how long it took

The React DevTools Profiler helps identify slow renders and unnecessary re-renders.

Using React DevTools Profiler:

  1. Open DevTools → Profiler tab
  2. Click Record, interact with the app, stop recording
  3. See flame chart showing each render by component and duration
  4. Click a bar to see why it rendered: props changed, hook changed, parent rendered

The Profiler API for automated performance testing:

import { Profiler } from 'react';

function onRenderCallback(
  id,           // tree id (prop)
  phase,        // "mount" | "update" | "nested-update"
  actualDuration, // time spent rendering committed update
  baseDuration,   // estimated time without memoization
  startTime,
  commitTime
) {
  if (actualDuration > 16) { // over one 60fps frame
    console.warn(`Slow render in ${id}: ${actualDuration}ms`);
  }
}

function App() {
  return (
    
      
    
  );
}

What to look for:

  • Components that render when they shouldn't (add React.memo)
  • baseDuration >> actualDuration → memoization is working
  • actualDuration consistently high → expensive render, consider useMemo
💡 Profile before you optimize. Adding useMemo/useCallback everywhere without profiling first adds complexity and can actually slow things down by running extra comparisons.
Practice this question →
Q7Core

What causes memory leaks in JavaScript and how do you detect them?

💡 Hint: Unintentional references prevent GC: forgotten timers, closures, detached DOM nodes

Memory leaks occur when objects are no longer needed but are still referenced, preventing garbage collection.

Common causes:

// 1. Forgotten intervals
const el = document.getElementById('status');
const id = setInterval(() => { el.innerHTML = Date.now(); }, 1000);
// If el is removed from DOM but interval isn't cleared → leak
// Fix: clearInterval(id) when done

// 2. Detached DOM nodes
let detached;
function create() {
  detached = document.createElement('div'); // global reference
  document.body.appendChild(detached);
  document.body.removeChild(detached); // removed from DOM...
  // but 'detached' variable still holds it → leak
}

// 3. Closures capturing large objects
function leaky() {
  const bigData = new Array(1_000_000).fill('*');
  return () => bigData[0]; // ALL of bigData kept alive!
}

// 4. Event listeners not removed
window.addEventListener('resize', heavyHandler);
// Fix: window.removeEventListener('resize', heavyHandler) on cleanup

// 5. Growing caches without eviction
const cache = {};
function store(key, val) { cache[key] = val; } // never cleared!

Detection: Chrome DevTools → Memory → Heap Snapshot → look for "Detached" DOM nodes, or compare snapshots over time for growing retained size.

💡 Use WeakMap for object-keyed caches — entries are automatically released when the key object is GC'd. Perfect for per-element data storage.
Practice this question →
Q8Core

What is the difference between reflow and repaint, and how do you avoid layout thrashing?

💡 Hint: Reflow=recalculate layout (expensive cascade); repaint=visual update only; batch DOM reads/writes

Repaint — visual property change (color, background, visibility) without affecting layout. Less expensive.

Reflow (Layout) — geometry change (width, height, position, padding, margin). Expensive: cascades through the document.

// Layout thrashing — alternating reads and writes force reflow each iteration
for (let i = 0; i < 100; i++) {
  const h = el.offsetHeight;       // READ — forces layout (reflow)
  el.style.height = h + 1 + 'px'; // WRITE — invalidates layout
}
// Browser must reflow 100 times! ❌

// Fix: batch all reads, then all writes
const h = el.offsetHeight; // single read
for (let i = 0; i < 100; i++) {
  el.style.height = (h + i) + 'px'; // writes only
}

// Best fix: CSS transforms — composited on GPU, NO reflow
el.style.transform = 'translateY(10px)'; // skip layout entirely!

// Properties that DON'T trigger reflow (GPU composited):
// transform, opacity, filter, will-change

What triggers reflow: offsetWidth/Height, getBoundingClientRect(), scrollTop, getComputedStyle(), adding/removing DOM nodes, font changes.

💡 Use requestAnimationFrame to batch DOM reads and writes in the correct phase. Libraries like FastDOM enforce this pattern.
Practice this question →
Q9Advanced

What is lazy loading and code splitting?

💡 Hint: Lazy loading: load resources only when needed; code splitting: divide bundle into smaller chunks

Lazy loading defers loading of non-critical resources until they're actually needed.

Code splitting breaks your JS bundle into smaller chunks loaded on demand.

// ─── Native Lazy Loading (images, iframes) ─────────────────
<img src="hero.jpg" loading="eager" />   // load immediately
<img src="below-fold.jpg" loading="lazy" /> // load when near viewport

// ─── JS Code Splitting (Webpack/Vite) ─────────────────────
// Static import — included in main bundle
import { utils } from './utils.js';

// Dynamic import — separate chunk, loaded on demand
async function loadChart() {
  const { Chart } = await import('./HeavyChart.js'); // separate bundle
  new Chart(element, data);
}

// Route-based splitting (React Router)
const Dashboard = React.lazy(() => import('./Dashboard'));
const Settings  = React.lazy(() => import('./Settings'));

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings"  element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

// ─── IntersectionObserver lazy loading ────────────────────
const observer = new IntersectionObserver(([entry]) => {
  if (entry.isIntersecting) {
    entry.target.src = entry.target.dataset.src;
    observer.unobserve(entry.target);
  }
});
document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img));
💡 Impact: code splitting cuts initial bundle by 30–70% for most apps. Rule of thumb: everything not needed for the landing page view should be lazily loaded.
Practice this question →

Other Javascript Interview Topics

Rendering Strategies →Core JS →Type System →React Fundamentals →Functions →Microfrontends →Generics →Async JS →Hooks →Objects →Monorepo →Arrays →'this' Keyword →Utility Types →Error Handling →Modern JS →Bundle Optimization →DOM & Events →State Management →Classes & OOP →Caching Strategies →Component Patterns →Advanced Types →Authentication →React Router →Forms →Advanced Patterns →Frontend Security →Concurrent React →Server Components →Testing →Ecosystem →Network Optimization →Core Web Vitals →Browser APIs →

Ready to practice Performance?

Get AI feedback on your answers, predict code output, and fix real bugs.

Start Free Practice →