Skip to content

10 - Performance Metrics & Memory ​


1. First Input Delay (FID) ​

What: Measures the time from when a user FIRST interacts with the page (click, tap, key press) to when the browser can BEGIN processing that event.

Page loads → User clicks button at t=3000ms
Main thread is busy (parsing JS) until t=3500ms
Browser starts handling click at t=3500ms
FID = 3500 - 3000 = 500ms (BAD — should be <100ms)

Good: <100ms | Needs improvement: 100-300ms | Poor: >300ms

What causes high FID:

  • Long JavaScript execution during page load
  • Large bundles being parsed and compiled
  • Third-party scripts blocking the main thread

FID is being replaced by INP (Interaction to Next Paint) as of March 2024. FID only measures the FIRST interaction. INP measures ALL interactions.


2. Interaction to Next Paint (INP) ​

What: Measures responsiveness across ALL interactions during the page's lifecycle. It's the worst interaction latency (at the 98th percentile) — from input to the next visual update.

INP = max(
  Input Delay +           (how long event waits in queue)
  Processing Time +       (how long event handler takes)
  Presentation Delay      (how long until browser paints the result)
)

Example:
  Click button → 20ms wait → 80ms handler → 15ms paint = 115ms INP

Good: <200ms | Needs improvement: 200-500ms | Poor: >500ms

Optimizing INP:

tsx
// 1. Keep event handlers fast
const handleClick = () => {
  // Quick state update (UI responds fast)
  setIsOpen(true);

  // Defer heavy work
  startTransition(() => {
    processHeavyComputation(); // non-urgent, won't block INP
  });
};

// 2. Yield to browser during long tasks
async function processItems(items: Item[]) {
  for (let i = 0; i < items.length; i++) {
    processItem(items[i]);
    if (i % 100 === 0) {
      await scheduler.yield(); // let browser handle pending events
    }
  }
}

// 3. Use CSS `content-visibility: auto` for off-screen content
// Browser skips rendering off-screen elements → faster paint

3. Cumulative Layout Shift (CLS) ​

What: Measures visual stability — how much the page content shifts unexpectedly during loading.

Page loads: [Header] [Content]
Image loads: [Header] [IMAGE - pushes content down] [Content]
                                  ↑ layout shift!

CLS = impact fraction × distance fraction
    = 0.5 (50% of viewport affected) × 0.25 (shifted 25% of viewport)
    = 0.125

Good: <0.1 | Needs improvement: 0.1-0.25 | Poor: >0.25

Common causes + fixes:

tsx
// 1. Images without dimensions → ALWAYS set width/height
<img src="hero.jpg" width={800} height={400} alt="..." />
// Or use aspect-ratio:
<img src="hero.jpg" style={{ aspectRatio: '16/9', width: '100%' }} alt="..." />

// 2. Fonts loading late (FOUT) → preload + font-display
<link rel="preload" href="/fonts/Inter.woff2" as="font" type="font/woff2" crossorigin />
// @font-face { font-display: optional; } — no swap, no shift

// 3. Dynamic content injected above viewport → reserve space
{showBanner && <div style={{ height: 60 }}><Banner /></div>}
// Even better: always render the container, toggle content

// 4. Skeleton loaders → match the final layout size exactly
<div style={{ height: 400, width: '100%' }}>
  {isLoading ? <Skeleton /> : <Chart data={data} />}
</div>

4. Largest Contentful Paint (LCP) ​

What: Measures loading performance — the time when the largest visible content element finishes rendering.

LCP candidates: Images, video posters, background images, block-level text.

Good: <2.5s | Needs improvement: 2.5-4s | Poor: >4s

Optimizing LCP:

html
<!-- 1. Preload the LCP image -->
<link rel="preload" href="/hero.webp" as="image" fetchpriority="high">

<!-- 2. Use modern formats + responsive images -->
<picture>
  <source srcset="/hero.avif" type="image/avif">
  <source srcset="/hero.webp" type="image/webp">
  <img src="/hero.jpg" width="1200" height="600" alt="..." fetchpriority="high">
</picture>

<!-- 3. Don't lazy-load the LCP element! -->
<img src="/hero.webp" loading="eager" fetchpriority="high" alt="...">
<!-- loading="lazy" on LCP image = worse LCP -->
tsx
// 4. Server-side render above-the-fold content
// Don't client-render the hero section

// 5. Minimize render-blocking CSS
// Inline critical CSS, defer non-critical
<style>{criticalCSS}</style>
<link rel="stylesheet" href="non-critical.css" media="print" onload="this.media='all'">

// 6. Reduce server response time (TTFB)
// CDN, edge rendering, caching

5. Browser Memory Leak Detection ​

What: Memory leaks in frontend apps cause increasing memory usage over time, leading to jank, crashes, and poor user experience.

Common leak sources in React:

  1. Uncleared intervals/timeouts:
tsx
// LEAK: interval runs forever
useEffect(() => {
  setInterval(() => fetchData(), 5000);
  // Missing: return () => clearInterval(id);
}, []);
  1. Unremoved event listeners:
tsx
// LEAK: listener on window never removed
useEffect(() => {
  window.addEventListener('resize', handleResize);
  // Missing: return () => window.removeEventListener('resize', handleResize);
}, []);
  1. Uncancelled async operations:
tsx
// LEAK: setState after unmount
useEffect(() => {
  fetchData().then(data => setData(data)); // component may have unmounted!
  // Fix: use AbortController or boolean flag
}, []);
  1. Closures holding references:
tsx
// LEAK: huge data array captured in closure, never released
const hugeData = generateHugeArray();
element.addEventListener('click', () => {
  console.log(hugeData.length); // hugeData can't be GC'd while listener exists
});

Detection tools:

  • Chrome DevTools → Memory tab → Heap snapshot
  • Performance Monitor (Chrome) → JS heap size over time
  • performance.memory API (Chrome only)

Process:

  1. Take heap snapshot (baseline)
  2. Perform the suspected leaking operation (navigate, open/close modal)
  3. Force garbage collection (click the trash icon)
  4. Take another heap snapshot
  5. Compare — look for objects that should have been freed

6. Detached DOM Nodes ​

What: DOM elements removed from the document but still referenced in JavaScript. The GC can't collect them because JS holds a reference.

tsx
// LEAK: Detached DOM node
let cachedElement: HTMLElement | null = null;

function openModal() {
  const modal = document.createElement('div');
  modal.innerHTML = '<p>Hello</p>';
  document.body.appendChild(modal);
  cachedElement = modal; // reference stored
}

function closeModal() {
  cachedElement?.remove(); // removed from DOM
  // But cachedElement still points to it!
  // The entire modal subtree stays in memory.

  cachedElement = null; // FIX: release the reference
}

In React: Usually handled by React's reconciler, but leaks happen with:

  • Refs to elements that were removed
  • Event listeners on elements stored in variables
  • Third-party libraries that cache DOM references

Finding detached nodes in Chrome DevTools:

  1. Memory tab → Heap Snapshot
  2. Filter by "Detached" in the Class filter
  3. Look for Detached HTMLDivElement, Detached HTMLElement, etc.
  4. Check Retainers tab to see what's holding the reference

7. Garbage Collection Timing ​

What: JavaScript's garbage collector (GC) automatically frees memory for objects that are no longer reachable. Understanding GC helps avoid performance issues.

V8's GC strategy (Chrome/Node):

Young Generation (Scavenger):
  - Small, fast GC for short-lived objects
  - Most objects die young (temporary arrays, intermediate results)
  - Runs frequently (~every few ms), quick (~1-2ms pause)

Old Generation (Mark-Sweep-Compact):
  - For objects that survived multiple young gen GCs
  - Runs less frequently, but can take 5-50ms
  - Can cause visible jank if triggered during animation

When GC causes problems:

Frame budget: 16ms (60fps)
GC pause: 20ms
Result: dropped frame → visible jank

This happens when:
1. Creating many temporary objects in a render loop
2. Large arrays being allocated and freed rapidly
3. Many event handlers creating closures

Minimizing GC pressure:

tsx
// BAD: New array every frame
function animate() {
  const positions = items.map(item => ({ x: item.x + 1, y: item.y })); // new objects
  updatePositions(positions);
  requestAnimationFrame(animate);
}

// GOOD: Reuse objects (object pooling)
const positionBuffer = new Float64Array(items.length * 2); // pre-allocated
function animate() {
  for (let i = 0; i < items.length; i++) {
    positionBuffer[i * 2] = items[i].x + 1;     // no allocation
    positionBuffer[i * 2 + 1] = items[i].y;
  }
  updatePositions(positionBuffer);
  requestAnimationFrame(animate);
}

In React: React's reconciler creates many temporary fiber objects. This is why React uses structural sharing and object pooling internally.

Frontend interview preparation reference.