Senior 12 min · March 05, 2026

React Performance — 3-Second Freeze from No Memoisation

A 20+ chart dashboard froze for 2-3 seconds on each keystroke because no memoisation.

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • React re-renders children by default when a parent updates, even if their props didn't change
  • React.memo stops re-renders by shallow-comparing props; useCallback and useMemo stabilise references for those comparisons
  • Lazy loading with React.lazy + Suspense splits the bundle, reducing initial download size
  • Virtualisation keeps DOM count constant for large lists, regardless of dataset size
  • Biggest mistake: wrapping everything in useMemo/useCallback without profiling first — the overhead often exceeds the gain
✦ Definition~90s read
What is React Performance Optimisation?

React performance optimisation is the practice of preventing unnecessary re-renders and reducing the computational cost of component updates in React applications. The core problem is that React's default behaviour re-renders an entire component subtree whenever a parent's state changes, even if most child components receive identical props.

Imagine a restaurant kitchen where every time one customer changes their order, the chef throws away every dish on the pass and starts cooking the entire menu again from scratch.

This becomes critical in production apps with complex UIs, large lists, or frequent state updates — a single unoptimised parent can cascade into a multi-second freeze as the reconciler diffs thousands of virtual DOM nodes. The tools to fix this are React.memo (which skips re-rendering when props haven't changed via shallow comparison), useMemo (which caches expensive computation results across renders), and useCallback (which stabilises function references to prevent child memoisation from breaking).

These aren't free — they add comparison overhead and memory pressure, so you apply them only after profiling identifies the bottleneck.

Beyond memoisation, the ecosystem provides structural solutions. Code splitting with React.lazy and Suspense lets you defer loading non-critical components until they're needed, shrinking the initial bundle and reducing reconciliation scope. Virtualisation libraries like react-window or react-virtuoso render only the visible rows in a long list — for a 10,000-item table, that drops the DOM nodes from 10,000 to ~20, cutting re-render cost by orders of magnitude.

The React DevTools Profiler is the definitive tool for this work: it records flame graphs showing exactly which components re-rendered, why (via the 'rendered by' and 'why did this render?' hooks), and how long each took. Without profiling, optimisation is guesswork — you'll likely memoise the wrong thing and make performance worse.

When not to use these techniques: if your app has fewer than a few hundred components or state updates are infrequent (e.g., a static blog), memoisation overhead often exceeds its benefit. Similarly, premature code splitting adds network round trips for marginal gains.

The rule of thumb is to measure first — if a re-render takes under 1ms and happens rarely, leave it alone. Real-world numbers: a typical e-commerce product listing with 50 items and 3 levels of nested components can see a 10x render time reduction (from 300ms to 30ms) by wrapping the list item component in React.memo and stabilising its onClick callback.

The trade-off is that you now maintain shallow comparison contracts across your component tree — a single inline object or arrow function in props silently breaks the optimisation.

Plain-English First

Imagine a restaurant kitchen where every time one customer changes their order, the chef throws away every dish on the pass and starts cooking the entire menu again from scratch. That's what an un-optimised React app does — it re-renders every component even if nothing about it changed. React performance optimisation is the set of techniques that tell the chef: 'Table 4 only changed their dessert — leave the starters alone.' The goal isn't to make your code faster in theory; it's to stop React doing work it doesn't need to do.

React's declarative model is a gift — you describe what the UI should look like and React figures out how to get there. But that abstraction has a cost. In a large production app with hundreds of components, deeply nested state, and real-time data flowing in from WebSockets, React can easily end up doing tens of thousands of unnecessary renders per second. Users notice this as janky animations, sluggish inputs and frames dropping below 60fps. That's when 'React is fast enough' stops being true.

The root cause is almost always the same: developers treat re-renders as free. They're not. Every re-render means React has to call your component function, build a new virtual DOM tree, diff it against the previous one (reconciliation), and then commit any changes to the real DOM. Most of the time, the diff shows nothing changed — yet you still paid the cost of the function call and the tree construction. The optimisation techniques in this article all share a single goal: give React the information it needs to skip that work entirely.

By the end of this article you'll understand exactly how the React reconciler decides what to re-render and why, when to reach for React.memo, useMemo and useCallback (and critically, when NOT to), how to use code splitting and virtualisation to handle scale, and the production gotchas that bite even experienced engineers. Every example is pulled from patterns we've seen in real codebases.

Why React Re-Renders Are Not Free

React performance optimisation is the practice of preventing unnecessary re-renders that cause the UI to freeze or drop frames. The core mechanic: every time a component's state or props change, React re-runs the entire component function and reconciles the virtual DOM. Without memoisation, a parent re-render cascades to all children, even if their props haven't changed — this is O(n) per render where n is the subtree size, and for a deep tree of 500+ components, that can block the main thread for 3 seconds or more.

In practice, the key property is referential equality. React uses Object.is to compare props; if you pass a new object or inline function on every render, memoisation fails. useMemo and useCallback stabilise references, while React.memo skips re-rendering when props are shallow-equal. The real cost isn't the re-render itself — it's the layout thrashing and DOM diffing that follows, especially in lists, charts, or form-heavy UIs.

Use memoisation when a component renders often (e.g., on every keystroke in a search input) or when its subtree is expensive (e.g., a data grid with 1000 rows). In production, the most common mistake is over-memoising: wrapping everything in useMemo adds memory overhead and comparison cost. The rule: measure first with React DevTools profiler, then memoise only the hot paths that show up as red bars.

Memoisation Is Not Free
useMemo and useCallback themselves cost memory and comparison time. Only apply them when you've measured a re-render bottleneck — premature memoisation can actually slow things down.
Production Insight
A financial dashboard with 200 live-updating widgets froze for 3 seconds every time a single ticker updated — because every widget re-rendered. The symptom: React DevTools showed every widget re-rendering on each tick, with a flame graph of 500ms+ per render. The rule: if a component re-renders more than once per second and its subtree takes >10ms to render, memoise it.
Key Takeaway
Unnecessary re-renders cascade O(n) — memoisation breaks that chain by stabilising props.
React.memo, useMemo, and useCallback are tools for referential equality, not for hiding bad architecture.
Measure before you memoise: the profiler is your only reliable guide to what's actually slow.
React Performance Optimization Flow THECODEFORGE.IO React Performance Optimization Flow From re-render causes to memoisation and profiling Re-render Cost Reconciler diffing triggers unnecessary work React.memo Prevents re-render on unchanged props useMemo & useCallback Memoize values and callbacks to avoid new refs Code Splitting React.lazy + Suspense for dynamic imports List Virtualization Only render visible items in long lists Profile First Measure with React DevTools before optimizing ⚠ Premature optimization adds complexity without gains Always profile first; memoisation has its own overhead THECODEFORGE.IO
thecodeforge.io
React Performance Optimization Flow
React Performance Optimisation

How the React reconciler actually decides what to re-render

Before you optimise anything, you need a mental model of what React is actually doing. When state or props change, React re-renders the component that owns that state — and by default, every child of that component re-renders too, regardless of whether their own props changed. This is called a cascading re-render and it's the single biggest source of preventable work in a React app.

React's reconciler (the Fiber architecture since React 16) works in two phases. The render phase is pure and interruptible — React calls your component functions and builds a Fiber tree representing the new UI. The commit phase is synchronous and side-effectful — React applies DOM mutations, runs layout effects, then passive effects. Only DOM nodes that actually changed get touched in the commit phase. But you still paid the full render phase cost for every component in the subtree.

The key insight is this: React uses referential equality (===) to decide whether props changed. A plain object literal {} created inside a parent component is a new reference every render, even if its contents are identical. That's why memoisation isn't just about expensive calculations — it's primarily about reference stability. Unstable references are the root cause of most unnecessary re-renders.

ReconcilerDemo.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import React, { useState } from 'react';

function ProductCard({ product, onAddToCart }) {
  console.log(`[ProductCard] rendering: ${product.name}`);
  return (
    <div className="product-card">
      <h3>{product.name}</h3>
      <p>${product.price}</p>
      <button onClick={() => onAddToCart(product.id)}>Add to cart</button>
    </div>
  );
}

function ProductListPage() {
  const [searchQuery, setSearchQuery] = useState('');
  const [cartCount, setCartCount] = useState(0);

  const featuredProduct = { id: 1, name: 'Mechanical Keyboard', price: 149 };
  const handleAddToCart = (productId) => {
    console.log(`Adding product ${productId} to cart`);
    setCartCount(prev => prev + 1);
  };

  return (
    <div>
      <p>Cart: {cartCount} items</p>
      <input
        value={searchQuery}
        onChange={e => setSearchQuery(e.target.value)}
        placeholder="Search products..."
      />
      <ProductCard
        product={featuredProduct}
        onAddToCart={handleAddToCart}
      />
    </div>
  );
}

export default ProductListPage;
Output
// Type 'key' into the search input — watch the console:
[ProductCard] rendering: Mechanical Keyboard
[ProductCard] rendering: Mechanical Keyboard
[ProductCard] rendering: Mechanical Keyboard
[ProductCard] rendering: Mechanical Keyboard
// ProductCard re-rendered 3 times for state it doesn't own or care about
Watch Out:
Install the React DevTools browser extension and enable 'Highlight updates when components render'. Run it on your production app before reaching for any optimisation API. You'll almost certainly find components re-rendering 10x more than you expected — but now you'll know exactly where to look.
Production Insight
In a production dashboard, a single state update can cascade through hundreds of components.
If you don't use React.memo, every child re-renders — even leaf components with stable props.
Rule: Profile first, then React.memo the leaves, stabilise the props.
Key Takeaway
React re-renders children by default.
The reconciler's render phase costs you time even if the commit phase skips DOM updates.
Stabilise references with useMemo/useCallback to let React.memo work.

React.memo, useMemo and useCallback — what each one actually does

These three APIs are frequently used interchangeably by developers who've half-read the docs. They solve different problems and conflating them leads to both under-optimised and over-engineered code.

React.memo is a higher-order component that wraps a component and tells React: 'Only re-render this component if its props have changed.' It does a shallow equality check on the props object. If every prop passes ===, React reuses the last rendered output entirely — it doesn't even call your component function.

useCallback memoises a function reference. It returns the same function object across renders as long as its dependency array hasn't changed. Its primary job is to create stable references to pass as props to memoised children — without it, React.memo is nearly useless because a new function reference counts as a changed prop.

useMemo memoises the return value of a computation. Use it for expensive calculations that shouldn't run on every render, or to stabilise object and array references that get passed as props. The dependency array works identically to useCallback — the memoised value is only recomputed when a dependency changes.

The rule of thumb: if you're passing a callback to a memoised child, use useCallback. If you're passing a derived object or doing expensive maths, use useMemo. Don't use either just because you can.

MemoisedProductCard.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import React, { useState, useCallback, useMemo } from 'react';

const ProductCard = React.memo(function ProductCard({ product, onAddToCart }) {
  console.log(`[ProductCard] rendering: ${product.name}`);
  return (
    <div className="product-card">
      <h3>{product.name}</h3>
      <p>${product.price.toFixed(2)}</p>
      <button onClick={() => onAddToCart(product.id)}>Add to cart</button>
    </div>
  );
});

function ProductListPage() {
  const [searchQuery, setSearchQuery] = useState('');
  const [cartCount, setCartCount] = useState(0);
  const [inventory] = useState([
    { id: 1, name: 'Mechanical Keyboard', price: 149, category: 'peripherals' },
    { id: 2, name: 'USB-C Hub', price: 49, category: 'peripherals' },
    { id: 3, name: 'Monitor Arm', price: 89, category: 'accessories' },
  ]);

  const featuredProduct = useMemo(
    () => inventory.find(item => item.id === 1),
    [inventory]
  );

  const filteredInventory = useMemo(() => {
    console.log('[useMemo] recomputing filtered inventory');
    return inventory
      .filter(item =>
        item.name.toLowerCase().includes(searchQuery.toLowerCase())
      )
      .sort((a, b) => a.price - b.price);
  }, [inventory, searchQuery]);

  const handleAddToCart = useCallback((productId) => {
    console.log(`Adding product ${productId} to cart`);
    setCartCount(prev => prev + 1);
  }, []);

  return (
    <div>
      <p>Cart: {cartCount} items</p>
      <input
        value={searchQuery}
        onChange={e => setSearchQuery(e.target.value)}
        placeholder="Search products..."
      />
      <ProductCard
        product={featuredProduct}
        onAddToCart={handleAddToCart}
      />
      <h2>All Products ({filteredInventory.length})</h2>
      {filteredInventory.map(product => (
        <ProductCard
          key={product.id}
          product={product}
          onAddToCart={handleAddToCart}
        />
      ))}
    </div>
  );
}

export default ProductListPage;
Output
// Initial render:
[useMemo] recomputing filtered inventory
[ProductCard] rendering: Mechanical Keyboard
[ProductCard] rendering: USB-C Hub
[ProductCard] rendering: Monitor Arm
// Type 'key' into the search input:
[useMemo] recomputing filtered inventory
[ProductCard] rendering: Mechanical Keyboard
// USB-C Hub and Monitor Arm did NOT re-render — React.memo worked
// The featured ProductCard at the top also did NOT re-render
Interview Gold:
Interviewers love asking 'when would you NOT use useMemo?' The honest answer: most of the time. Memoisation has overhead — it allocates memory for the cache, runs dependency comparisons, and adds cognitive load. For cheap calculations (string concatenation, simple arithmetic), the memoisation cost often exceeds the savings. Only reach for it when React DevTools shows a genuine problem.
Production Insight
A common production scare: you add useCallback on every callback, and React.memo on every component, but performance barely improves.
That's because the parent still re-renders all child instances — memo only skips the render phase if props are stable.
Rule: Apply memo only where props are actually stable; otherwise the comparison overhead is wasted.
Key Takeaway
React.memo skips render phase for unchanged props.
useCallback and useMemo stabilise references for that shallow comparison.
Don't memoise everything — profile first, then apply surgically.

Code splitting with React.lazy, Suspense and dynamic imports

Memoisation fights unnecessary re-renders. Code splitting fights the other performance killer: loading too much JavaScript upfront. In a typical React SPA, bundlers like Webpack or Vite produce a single JavaScript bundle. A user visiting your landing page downloads code for your admin dashboard, your analytics charts, and every other route — most of which they'll never touch. This bloats the initial bundle, delays Time to Interactive, and kills Lighthouse scores.

Code splitting lets you split your bundle into smaller chunks that load on demand. React.lazy and Suspense are the built-in APIs for this. React.lazy takes a function that calls a dynamic import() — a browser-native API that returns a Promise resolving to a module. React defers rendering that component until the module has loaded, and Suspense defines what to show in the meantime.

The sweet spot for lazy loading is route-level splitting — each page becomes its own chunk. But it's also valuable for heavy third-party components like rich text editors, PDF viewers, or chart libraries that aren't needed on initial load. The rule: if a user's critical path doesn't need it within the first three seconds, consider lazy loading it.

A critical gotcha: React.lazy only works with default exports. Named exports require a small wrapper. Also, always wrap lazy components high enough in the tree that the Suspense fallback doesn't cause layout shift.

AppWithCodeSplitting.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import React, { Suspense, lazy, useState } from 'react';

const AnalyticsDashboard = lazy(() => import('./pages/AnalyticsDashboard'));

const RichTextEditor = lazy(() =>
  import('./components/RichTextEditor').then(module => ({
    default: module.RichTextEditor
  }))
);

function DashboardSkeleton() {
  return (
    <div aria-busy="true" aria-label="Loading analytics dashboard">
      <div style={{ height: 48, background: '#e5e7eb', borderRadius: 8, marginBottom: 16 }} />
      <div style={{ height: 300, background: '#e5e7eb', borderRadius: 8 }} />
    </div>
  );
}

function App() {
  const [activeView, setActiveView] = useState('home');

  return (
    <div>
      <nav>
        <button onClick={() => setActiveView('home')}>Home</button>
        <button onClick={() => setActiveView('analytics')}>Analytics</button>
        <button onClick={() => setActiveView('editor')}>Editor</button>
      </nav>

      {activeView === 'home' && (
        <main>
          <h1>Welcome to the Dashboard</h1>
          <p>Select a section to get started.</p>
        </main>
      )}

      {activeView === 'analytics' && (
        <Suspense fallback={<DashboardSkeleton />}>
          <AnalyticsDashboard dateRange="last-30-days" />
        </Suspense>
      )}

      {activeView === 'editor' && (
        <Suspense fallback={<div>Loading editor...</div>}>
          <RichTextEditor initialContent="Start writing..." />
        </Suspense>
      )}
    </div>
  );
}

export default App;
Output
// Browser Network tab when user clicks 'Analytics' for the first time:
// GET /assets/AnalyticsDashboard-Bx3kP2qR.js 180 kB (downloaded once, cached after)
// Skeleton renders immediately, then dashboard appears when chunk is ready
// Subsequent clicks on 'Analytics':
// No network request — chunk is already in browser cache
// Dashboard renders immediately (no Suspense fallback shown)
Pro Tip:
Use prefetching to eliminate the loading delay for predictable navigation. Add a mouseenter handler on nav links that calls import('./pages/AnalyticsDashboard') — this preloads the chunk while the user is still moving their cursor. By the time they click, the chunk is already cached and Suspense resolves instantly. Vite also supports / @vite-prefetch / and / webpackPrefetch: true / magic comments for automatic prefetching.
Production Insight
A team once lazy-loaded a chart component but didn't provide a fallback with matching dimensions.
The result: Layout shift on every navigation to the analytics page, causing Cumulative Layout Shift (CLS) spikes.
Rule: Always match the fallback's dimensions to the lazy component's expected size to prevent CLS.
Key Takeaway
React.lazy + Suspense reduces initial bundle by deferring heavy modules.
Route-level splitting is the sweet spot; also lazy load heavy third-party components.
Prevent layout shift by dimension-matching the Suspense fallback.

Virtualisation for long lists — only render what the user can see

No amount of memoisation saves you if you're trying to render 10,000 DOM nodes at once. The browser has to create, style, and layout each one. A list with 5,000 items might take 2–3 seconds just to mount — and that's before any interaction.

Virtualisation (also called windowing) solves this by only rendering the DOM nodes currently visible in the viewport, plus a small overscan buffer above and below. As the user scrolls, nodes are recycled — elements that scroll off the top are repositioned and reused for content coming in from the bottom. The DOM stays small (typically 20–50 nodes) regardless of how many items are in the data set.

@tanstack/react-virtual is the modern, framework-agnostic choice. It's headless — it gives you the calculations, you control the markup. react-window and react-virtualized are older but still widely used in production and worth knowing for legacy codebases.

Critical consideration: virtualisation breaks native browser 'find in page' (Ctrl+F) because non-rendered items aren't in the DOM. For accessibility and search-critical content, consider server-side pagination instead. Also, fixed-height rows are significantly simpler to implement than variable-height rows — @tanstack/react-virtual supports both, but dynamic measurement has its own complexity.

VirtualisedProductList.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import React, { useRef, useMemo } from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';

function generateProductCatalogue(count) {
  return Array.from({ length: count }, (_, index) => ({
    id: index + 1,
    name: `Product ${index + 1}`,
    price: parseFloat((Math.random() * 500 + 10).toFixed(2)),
    category: ['electronics', 'clothing', 'books', 'tools'][index % 4],
    inStock: index % 7 !== 0,
  }));
}

function ProductRow({ product, style }) {
  return (
    <div
      style={{
        ...style,
        display: 'flex',
        alignItems: 'center',
        padding: '0 16px',
        borderBottom: '1px solid #e5e7eb',
        background: product.inStock ? '#fff' : '#fef2f2',
      }}
    >
      <span style={{ flex: 1, fontWeight: 600 }}>{product.name}</span>
      <span style={{ width: 100, color: '#6b7280' }}>{product.category}</span>
      <span style={{ width: 80, textAlign: 'right' }}>${product.price}</span>
      <span style={{ width: 80, textAlign: 'right', color: product.inStock ? '#16a34a' : '#dc2626' }}>
        {product.inStock ? 'In stock' : 'Sold out'}
      </span>
    </div>
  );
}

function VirtualisedProductList() {
  const allProducts = useMemo(() => generateProductCatalogue(10_000), []);
  const scrollContainerRef = useRef(null);

  const virtualiser = useVirtualizer({
    count: allProducts.length,
    getScrollElement: () => scrollContainerRef.current,
    estimateSize: () => 56,
    overscan: 5,
  });

  const totalScrollHeight = virtualiser.getTotalSize();
  const visibleItems = virtualiser.getVirtualItems();

  console.log(`Rendering ${visibleItems.length} of ${allProducts.length} items in DOM`);

  return (
    <div>
      <h2>Product Catalogue ({allProducts.length.toLocaleString()} items)</h2>
      <div
        ref={scrollContainerRef}
        style={{ height: 600, overflow: 'auto', border: '1px solid #e5e7eb', borderRadius: 8 }}
      >
        <div style={{ height: totalScrollHeight, position: 'relative' }}>
          {visibleItems.map(virtualItem => {
            const product = allProducts[virtualItem.index];
            return (
              <ProductRow
                key={product.id}
                product={product}
                style={{
                  position: 'absolute',
                  top: 0,
                  left: 0,
                  width: '100%',
                  transform: `translateY(${virtualItem.start}px)`,
                  height: `${virtualItem.size}px`,
                }}
              />
            );
          })}
        </div>
      </div>
    </div>
  );
}

export default VirtualisedProductList;
Output
// In the browser console as page loads:
Rendering 16 of 10,000 items in DOM
// After scrolling halfway down:
Rendering 16 of 10,000 items in DOM
// DOM inspection in DevTools shows only ~16 ProductRow divs exist at any time
// Page mount time: ~12ms (vs ~2400ms without virtualisation)
// Memory usage: ~8 MB (vs ~180 MB without virtualisation)
Watch Out:
Never virtualise a list of fewer than ~100 items. The position:absolute layout removes items from normal document flow, which breaks things like CSS grid siblings, sticky headers within the list, and native Ctrl+F search. The overhead of setting up a virtualiser on a 50-item list is pure cost with no measurable benefit.
Production Insight
A product list page with 8,000 items was using virtually no optimisation; mount time was 2.4 seconds.
After virtualisation, mount time dropped to 12ms — same data, 200x faster.
But the team forgot to add a meaningful loading state for the overscan, causing visible whitespace during fast scroll.
Rule: set overscan high enough (10-15) to mask fetch latency if items are loaded async.
Key Takeaway
Virtualisation keeps DOM count constant regardless of list size.
Use @tanstack/react-virtual for modern apps, react-window for legacy.
Watch for Ctrl+F breakage and dimension mismatches; fixed-height rows are simplest.

Profiling React Performance: The One Tool You Must Know

All optimisation without profiling is guesswork. The React DevTools Profiler is your single source of truth. It records each render commit, shows which components re-rendered and why, and gives you the flamegraph of where time is spent. Without it, you're flying blind.

To use it: record an interaction (e.g., typing in a search input, clicking a button). The profiler shows each commit as a bar at the top. Click a commit to see a flamegraph. Components that re-rendered are highlighted; their color intensity corresponds to time spent. Hover over a component to see 'why did this render?' — often it's 'Parent re-rendered' or 'Props changed' with the specific prop.

Also enable the 'Highlight updates when components render' option in the React DevTools settings. It overlays colored borders on components that re-render. If you see a border flashing on a component that shouldn't re-render, you've found the leak.

A production-safe pattern: add a debug-only render counter using React.useRef in components. Log out render counts to the console in development. This catches cascading re-renders early without relying on the profiler every time.

useRenderCounter.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { useRef, useEffect } from 'react';

// A custom hook to track how many times a component renders (development only)
export function useRenderCounter(componentName = 'Component') {
  const renderCount = useRef(0);

  useEffect(() => {
    renderCount.current += 1;
    if (process.env.NODE_ENV === 'development') {
      console.log(`[Render ${componentName}] #${renderCount.current}`);
    }
  });

  return renderCount.current;
}

// Usage inside a component:
// const renderCount = useRenderCounter('ProductCard');
// You can use renderCount to display it on the UI if needed.
Output
// With the hook added to ProductCard, typing in the search input outputs:
[Render ProductCard] #1
[Render ProductCard] #2
[Render ProductCard] #3
[Render ProductCard] #4
// Immediately shows that ProductCard re-renders unnecessarily 3 times per keystroke.
// After adding React.memo and useCallback, only #1 appears on initial mount.
Pro Tip:
Don't ship render count logs to production. Use process.env.NODE_ENV guards (most bundlers strip dead code). In CRA, environment variables prefixed with REACT_APP_ are available; use REACT_APP_ENABLE_PROFILING for production-safe profiling flags.
Production Insight
A team was deploying unnecessary re-renders to production because they only tested on personal machines with DevTools open.
They didn't realise the profiler itself adds overhead and masks issues in production.
Rule: Always test performance on production builds with production mode, and use browser DevTools Performance tab (not React Profiler) for final validation.
Key Takeaway
Profile with React DevTools Profiler before any optimisation.
Look for components re-rendering due to 'Parent re-rendered' — those are your targets.
Use a debug render counter in development to catch cascading re-renders early.

When NOT to optimise: the hidden cost of premature optimisation

The React team's own documentation warns against premature memoisation. Here's why: every useMemo and useCallback call allocates memory for a cache entry, runs dependency comparison on every render, and increases the cognitive load of the code. For a simple calculation (e.g., const fullName = ${first} ${last}``), wrapping it in useMemo adds more overhead than the calculation itself.

Similarly, React.memo on a component that re-renders frequently with actually different props is wasted — the comparison runs every render and never skips the re-render anyway. It's pure overhead.

The rule: measure before optimising. Use the profiler to find components that re-render more than expected or take >1ms in the render phase. Apply memoisation only to those. Also, consider restructuring your component tree before reaching for optimisation APIs. Lifting state up or pushing it down often solves the problem without adding any memo calls.

Finally, remember that React 19's compiler (React Forget) aims to auto-memoise components. But that doesn't mean you can ignore these fundamentals today. The compiler understands reference stability and dependencies; you'll still need to write clean component trees.

AntiPattern.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { useMemo, useCallback } from 'react';

// Bad: useMemo on a trivial string concatenation
function UserGreeting({ firstName, lastName }) {
  const fullName = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]);
  return <h1>Hello, {fullName}</h1>;
}

// The concatenation is O(1), the useMemo adds allocation and dep check.
// Just write: const fullName = `${firstName} ${lastName}`;

// Also bad: useCallback on a handler passed to a non-memoised child
function Parent() {
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);

  // Child is a plain div, not React.memo. useCallback does nothing useful.
  return <div onClick={handleClick}>Click me</div>;
}
Output
// In React DevTools Profiler, these components show no render time difference.
// The useMemo/useCallback overhead is visible as additional function calls in flamegraph.
// Removing them has zero negative impact and simplifies the code.
Interview Gold:
A great answer to 'when not to use useMemo' is: when the computation is cheap (string concat, simple arithmetic), when the component re-renders with different props every time (memoisation never skips), and when you haven't profiled. The best optimisation is often restructuring — moving state down, splitting components, or using keys properly.
Production Insight
A team wrapped all computations in useMemo and all callbacks in useCallback as a 'best practice'.
They had a form with 50+ fields, each with useCallback, making the code 3x longer.
Performance didn't improve because the form re-rendered on every keystroke anyway — the overhead of 50 useCallback deps checks added measurable latency.
Rule: Only memoise if the profiler shows a specific component is a bottleneck.
Key Takeaway
Premature memoisation is an antipattern.
Measure with the profiler first, then optimise only the bottlenecks.
Restructuring the component tree is often more effective than adding memoisation hooks.

How to Measure First (Before You Touch a Line of Code)

You wouldn't fix a car engine by staring at it and guessing. Same goes for React. Before you memo anything, before you reach for useCallback, you need hard data. Without measurements, you're optimising ghosts. The React DevTools Profiler is your first stop. Fire it up, record a session, and actually look for long render times or components that re-render when nothing changed. Chrome's Performance tab gives you the bigger picture — long tasks, layout thrashing, jank. A 2ms render is not your problem. A 200ms layout recalculation is. Write a quick render counter to confirm your hunches. See which components are actually expensive. The golden rule: measure once, optimise the bottleneck, measure again. If you can't measure improvement, you didn't fix anything — you just added complexity.

ProfileBottleneck.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// io.thecodeforge — javascript tutorial

import { useRef, useEffect } from 'react';

function ExpensiveList({ items }) {
  const renderCount = useRef(0);
  
  useEffect(() => {
    renderCount.current++;
    console.log(`ExpensiveList rendered ${renderCount.current} times`);
  });

  // Actually expensive: sorting 10k items every render
  const sorted = items.slice().sort((a, b) => b.score - a.score);
  
  return sorted.map(item => <div key={item.id}>{item.name}</div>);
}
Output
ExpensiveList rendered 1 times
ExpensiveList rendered 2 times
ExpensiveList rendered 3 times
Production Trap:
Don't leave render counters in production. They add overhead. Strip them with environment checks or a build flag.
Key Takeaway
Never optimise without a baseline. Measure first, then fix, then prove the fix with numbers.

Optimize Context Usage (Or Watch Your Entire App Re-Render)

React Context is not a state management library, no matter how many tutorials say otherwise. It's a dependency injection tool with a nasty side effect: when the context value changes, every single consumer re-renders. Not the ones that use the changed piece of state — all of them. If you put a frequently-updated value like a timer or a mouse position into a single context, your whole tree re-renders on every tick. The fix is brutal but effective: split your contexts by responsibility. One context for auth data, one for theme, one for UI state. For values that change multiple times per second, skip context entirely — use a ref or a lightweight external store like Zustand. Also, memoize your context value object. If you pass a new object reference every render, you're causing re-renders even when the data didn't change. Your consumers don't care about your object identity crisis.

SplitContexts.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// io.thecodeforge — javascript tutorial

// ❌ Single context for everything
const AppContext = createContext();

function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('dark');
  const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
  
  // Every mouse move re-renders ALL consumers
  return (
    <AppContext.Provider value={{ user, theme, mousePos }}>
      {children}
    </AppContext.Provider>
  );
}

// ✅ Split contexts
const UserContext = createContext();
const ThemeContext = createContext();

function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('dark');
  
  return (
    <UserContext.Provider value={user}>
      <ThemeContext.Provider value={theme}>
        {children}
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}
Output
Profile shows: Theme change re-renders only ThemeConsumer, not UserAvatar
Senior Shortcut:
If a context value updates more than once per second, don't put it in context. Use a ref + subscription pattern or a state library that isolates updates.
Key Takeaway
Split contexts by update frequency. A single context that changes often will re-render your whole app tree.

Debounce Rapid Updates (Stop Chasing Every Keystroke)

Real-time search, slider drags, and scroll handlers fire updates faster than your UI can handle. Each update triggers a re-render, potentially a network request, and maybe a full reconciliation. The result: a janky mess. The fix is throttling your input. Debouncing is ideal for inputs where you want the final value — wait until the user stops typing for 300ms, then fire the update. For animations or scrolls, use requestAnimationFrame or a throttle to drop intermediate updates. Don't debounce inside the component that's consuming the state; debounce the handler that's producing it. Otherwise you're still rendering the intermediate values. Libraries like lodash.debounce or use-debounce are fine, but a 5-line custom hook with setTimeout does the same thing without the dependency.

DebouncedSearch.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// io.thecodeforge — javascript tutorial

import { useState, useCallback, useEffect } from 'react';

function useDebounce(value, delay = 300) {
  const [debouncedValue, setDebouncedValue] = useState(value);
  
  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);
  
  return debouncedValue;
}

function SearchBar() {
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearch = useDebounce(searchTerm, 300);
  
  // Only fires once user stops typing for 300ms
  useEffect(() => {
    if (debouncedSearch) {
      fetchResults(debouncedSearch);
    }
  }, [debouncedSearch]);
  
  return (
    <input 
      type="text"
      value={searchTerm}
      onChange={e => setSearchTerm(e.target.value)}
      placeholder="Type to search..."
    />
  );
}
Output
User types 'react' quickly: 5 input changes, 1 fetch call when they pause
Production Trap:
Debounce API calls, not state updates. If you debounce the setState, the UI feels laggy. Let the input update instantly, debounce the expensive side effect.
Key Takeaway
Debounce rapid inputs at the handler level. Let the UI stay responsive, throttle the expensive work.

Web Workers: Offload Heavy Math So Your UI Doesn't Choke

React's single-threaded nature means one expensive computation freezes every tab, every animation, every button click. You've felt it — a complex filter, a data transform, a chart calculation that steals the frame budget. Don't optimize React logic; move the work off the thread entirely.

Web Workers run JavaScript in a separate OS-level thread. They have zero access to the DOM, zero access to React state, and that's exactly the point. The main thread stays at 60fps while your worker crunches numbers. You communicate via postMessage — fire an input in, get results back asynchronously.

Production teams use workers for CSV parsing, image processing, financial calculations, anything above ~10ms synchronous work. The cost: serialization overhead when passing data. Keep payloads small, transfer ArrayBuffers by reference when possible. Don't reach for a library — the native Worker API is tiny and battle-tested.

fibWorker.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// io.thecodeforge — javascript tutorial

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

worker.postMessage(42); // ask for fib(42)

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

// fibWorker.js (separate file)
self.onmessage = (e) => {
  const n = e.data;
  
  const fib = (x) => (x <= 1 ? x : fib(x - 1) + fib(x - 2));
  
  self.postMessage(fib(n)); // send back
};
Output
Result: 267914296
New Thread Tax:
Creating a Worker per component mount is wasteful. Pool workers or reuse a single long-lived worker. Serialization for every message adds ~0.5-2ms — batch your data.
Key Takeaway
If a synchronous function blocks the main thread longer than 16ms, that function belongs in a Web Worker.

Optimizing Images: The Asset That Silently Kills Your Lighthouse Score

Images account for 70%+ of page weight on most React apps. Your users aren't waiting for JSON — they're waiting for that 3MB hero image you downscaled with CSS. CSS width: 300px doesn't shrink the bytes; the browser still decodes the full resolution. This is the single cheapest win in performance you'll ever get.

Prefer next-gen formats: WebP covers 96% of browsers, AVIF covers ~85% with better compression. Use the <picture> element with multiple sources — let the browser pick the smallest supported format. Apply responsive images with srcSet: serve 400w, 800w, 1200w versions and let the viewport decide. This alone drops image weight 40-70%.

Lazy-load below-the-fold images with loading="lazy" — it's native, zero dependencies, works in all modern browsers. Give explicit width + height to prevent layout shift (CLS). For icons, inline SVGs avoid an HTTP request altogether. If you use a CDN (Cloudinary, Imgix, or a custom thumbnailer), append transformation parameters in the URL instead of resizing in React.

OptimizedImage.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// io.thecodeforge — javascript tutorial

function OptimizedImage({ src, alt, width, height }) {
  // CDN transforms appended as URL params
  const webpUrl = `${src}?format=webp&w=${width}&q=75`;
  const fallbackUrl = `${src}?format=jpg&w=${width}&q=80`;

  return (
    <picture>
      <source srcSet={webpUrl} type="image/webp" />
      <img
        src={fallbackUrl}
        alt={alt}
        width={width}
        height={height}
        loading="lazy"
        decoding="async"
      />
    </picture>
  );
}
Output
Renders <picture> with WebP source, lazy-loaded <img> — browser chooses best format, weight reduces 40-60%
Senior Shortcut:
Run your images through squoosh-cli in CI. One command converts everything to WebP AVIF with lossy settings. Never commit raw JPEGs again.
Key Takeaway
Don't resize images with CSS. Resize at the server or build step. Bytes in = bytes on the wire.

SSR: When Your Spinny Spinner Needs 2s to Disappear

Client-side rendering means the user stares at a blank or loading state while your bundle downloads, parses, and runs. For content-heavy apps, that first paint can take 3-5 seconds on slow networks. Server-side rendering flips the script: send HTML that's already rendered. The user sees real content on the first paint.

React's renderToString turns your component tree into a string of HTML on the server. The browser paints it immediately. Then the JS bundle hydrates — attaches event listeners, makes the page interactive. The catch: the server has to do work per request, which costs CPU and increases Time to First Byte (TTFB).

Static generation (SSG) solves this for pages that don't change per user — build once, serve from CDN. Next.js and Remix handle this natively. For dynamic content (user dashboards), SSR is worth it if your content-to-code ratio is high. If 90% of your page is interactive UI, SSR adds complexity for little gain. Measure the LCP improvement before you refactor.

ssrExample.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// io.thecodeforge — javascript tutorial

import React from 'react';
import { renderToString } from 'react-dom/server';

function App({ user }) {
  return (
    <html>
      <body>
        <h1>Hello, {user.name}</h1>
        <script src="/bundle.js" defer />
      </body>
    </html>
  );
}

// On the server:
const html = renderToString(<App user={{ name: 'Ada' }} />);
console.log(html);
Output
<html><body><h1>Hello, Ada</h1><script src="/bundle.js" defer></script></body></html>
Hydration Hell:
Server-rendered HTML must match client-rendered output exactly, or React throws warnings and re-renders the whole tree. For content that differs (user-specific data), use suppressHydrationWarning sparingly.
Key Takeaway
SSR improves perceived performance when content > interactivity. For dashboards or apps, consider streaming SSR or partial hydration.
● Production incidentPOST-MORTEMseverity: high

The Dashboard That Froze for 3 Seconds on Every Keystroke

Symptom
Typing in a search input caused the entire dashboard (20+ charts, tables, and widgets) to freeze for 2-3 seconds. Console showed React was calling every component function on every keystroke, even those that didn't depend on the search query.
Assumption
The assumption was that React was efficient enough out of the box; if a component's props don't change, React won't re-render it.
Root cause
No memoisation on any component. The search state was kept in a top-level context, and every child re-rendered due to cascading render. Additionally, inline object literals and arrow functions in JSX created new references every render, making React.memo useless even if it had been applied.
Fix
Applied React.memo to stable chart components, wrapped expensive callbacks in useCallback, and stabilised data objects with useMemo. Also moved the search-specific state down into a dedicated component to isolate the re-render scope. The freeze dropped from 3 seconds to under 200ms.
Key lesson
  • Always profile before optimising — React DevTools Profiler shows exactly which components re-render and why.
  • Stabilise references of props passed to memoised children: useMemo for objects/arrays, useCallback for functions.
  • Move state as close as possible to where it's used to limit re-render scope.
Production debug guideSymptom → Action guide for identifying and fixing React performance issues4 entries
Symptom · 01
Component re-renders even though its props look the same
Fix
Open React DevTools > Profiler > Highlight updates when components render. Check the component's props in the 'Change' column — look for new object/array/function references on each render.
Symptom · 02
useMemo or useCallback seems to have no effect
Fix
Verify the dependency array. If you omitted a dependency, the memoised value is stale. If you included a new object reference each render, memoisation is useless. Use eslint-plugin-react-hooks/exhaustive-deps to catch both.
Symptom · 03
React.memo component still re-renders when parent re-renders
Fix
Check every prop for referential instability. Inline objects ({ theme: 'dark' }) create new references. Move static values outside the component, or wrap dynamic ones in useMemo. Also check that you aren't passing a new function reference (useCallback needed).
Symptom · 04
Large list re-renders on every state update, causing frame drops
Fix
If the list has 100+ items, apply virtualisation (@tanstack/react-virtual) to only render visible rows. For smaller lists, use React.memo on list item components and stabilise their props.
★ Quick React Performance Debugging Cheat SheetCommands and actions to diagnose and fix performance issues fast.
Janky UI on state updates
Immediate action
Open browser DevTools > Performance tab, record interaction, look for long 'Function Call' or 'Rendering' blocks.
Commands
React DevTools Profiler -> Record -> Trigger the interaction -> Inspect 'Commits' > 'What caused this update'
Add console.log('%c[RENDER] ComponentName', 'color:blue') in every component to see which ones re-render on each action.
Fix now
Use React.memo on leaf components, stabilise props with useMemo/useCallback, and avoid creating new objects/functions in render.
Slow initial page load+
Immediate action
Check Network tab — if a single JS bundle is > 500KB, it's a code splitting candidate.
Commands
In Chrome DevTools: Run Lighthouse > Performance > Metrics > Total Blocking Time
Run `npx source-map-explorer build/static/js/main.*.js` to see what's in your bundle.
Fix now
Add React.lazy on route-level components and heavy third-party libs (chart libs, PDF viewers) that aren't needed above the fold.
Memory grows over time, page becomes sluggish+
Immediate action
Take heap snapshot in Chrome DevTools > Memory, compare two snapshots after interaction.
Commands
In React DevTools Profiler, enable 'Record why each component rendered' and look for 'Parent re-rendered' cause.
Check for missing cleanup in useEffect: return a cleanup function that removes event listeners, subscriptions, or aborts fetch requests.
Fix now
Ensure all effects have cleanup functions, avoid storing large objects in state that aren't needed, and use WeakMap for caches if applicable.
React Performance Techniques Comparison
TechniqueWhat it preventsWhen to useHidden cost
React.memoUnnecessary re-renders when props haven't changedMemoised children receiving stable props from a frequently-re-rendering parentShallow props comparison on every render — wasted if props change often
useMemoExpensive recalculations and unstable object/array referencesComputationally heavy derivations, or objects/arrays passed as props to memoised childrenMemory allocation for the cache, dependency array comparison, cognitive overhead
useCallbackNew function references causing memoised children to re-renderCallbacks passed as props to React.memo'd components or as dependencies in other hooksSame memory overhead as useMemo — pointless without React.memo on the receiving component
React.lazy + SuspenseOversized initial JS bundle delaying Time to InteractiveRoute-level splits, heavy optional features (charts, editors, PDF viewers)Extra network request on first use — mitigate with prefetching
VirtualisationRendering thousands of DOM nodes at once causing slow mount and scrollLists with 100+ dynamic items where pagination isn't viableBreaks Ctrl+F search, complicates variable-height rows, removes items from document flow

Key takeaways

1
React re-renders children by default whenever a parent re-renders
React.memo opts a component out of this by caching its output and only re-rendering when props change via shallow equality (===).
2
useCallback and useMemo are reference-stabilisation tools first, and computation-savers second
their most important job is preventing new object/function references from invalidating React.memo.
3
Code splitting with React.lazy + dynamic import() reduces initial bundle size at the route or feature level
combine it with mouseenter prefetching to eliminate visible loading delays on predictable navigation.
4
Virtualisation with @tanstack/react-virtual keeps the DOM node count constant regardless of dataset size
mount time and memory usage stay flat whether your list has 100 or 100,000 items.
5
Profile before optimising. Premature memoisation adds overhead and complexity. Use React DevTools Profiler to find real bottlenecks, then apply optimisations surgically.
6
Restructuring component state (lifting or lowering state) often solves performance issues better than any memoisation hook.

Common mistakes to avoid

5 patterns
×

Defining objects or arrays inline inside JSX props on a memoised component

Symptom
React.memo never skips a re-render because every render creates a new reference for the inline object/array. The shallow comparison always sees a change.
Fix
Move static objects outside the component. For dynamic ones, wrap them in useMemo so the reference stays the same across renders unless the underlying data changes.
×

Using useCallback with a stale closure by omitting state variables from the dependency array

Symptom
The callback always operates on the initial value of the omitted state variable. Updates to that variable are never seen, causing hard-to-debug bugs where clicks or events trigger stale data.
Fix
Either include all referenced state in the dependency array, or use the functional updater form (setState(prev => prev + 1)) which doesn't need to close over the current value.
×

Wrapping everything in useMemo and useCallback 'just in case'

Symptom
No measurable performance improvement, but code is harder to read and refactor. The overhead of dependency comparisons and memory allocation may actually degrade performance in hot paths.
Fix
Profile with React DevTools Profiler first, identify components with high render times or render counts, then apply memoisation surgically where the measurement shows a real gain.
×

Forgetting to add a key or using index as key when rendering lists

Symptom
When the list order changes, React can't match components to previous instances, leading to unnecessary re-renders and potential state loss in controlled inputs.
Fix
Always use a stable, unique identifier as the key (e.g., database ID). Avoid using index unless the list is static and never reordered.
×

Lazy loading components that are needed above the fold

Symptom
The initial page load is slower because the lazy component's chunk needs to be fetched before rendering, causing a visible flash or empty state for critical UI.
Fix
Only lazy load components that are not in the critical rendering path. Use route-level splitting for pages below the fold, or heavy third-party components that can appear after initial paint. For above-fold components, consider direct imports with proper code splitting via other mechanisms (e.g., React Server Components).
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
If React.memo does a shallow comparison of props, how would you handle a...
Q02SENIOR
Explain the relationship between useCallback and React.memo. Can React.m...
Q03SENIOR
A colleague says useMemo is always a good idea because 'it can only help...
Q04SENIOR
How would you debug a situation where a React app feels janky but you ca...
Q01 of 04SENIOR

If React.memo does a shallow comparison of props, how would you handle a component that receives a deeply nested object as a prop where only a nested field changes — and why is the standard advice 'flatten your state' rather than 'use a custom comparator'?

ANSWER
Shallow comparison won't detect deep changes. A custom comparator (second argument to React.memo) can perform deep comparison, but that's expensive and usually a code smell. Flattening state means splitting the deeply nested object into separate props or using multiple smaller components so that only the relevant part triggers a re-render. For example, instead of passing a user object with 20 fields, pass name, email, role as separate props. That way, React.memo compares each primitive directly, and only the display component for the changed field re-renders. In practice, if flattening isn't feasible, consider using useMemo on the parent to return a stable reference to the deeply nested object, and only update that reference when the relevant nested field changes. But that often requires manual dependency tracking and is error-prone. The cleaner path is refactoring the component hierarchy.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Does React.memo do a deep comparison of props?
02
When should I use useCallback vs useMemo?
03
Does splitting code with React.lazy hurt SEO?
04
Can virtualised lists be accessible?
05
What's the difference between the React Profiler and the browser Performance tab?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.

Follow
Verified
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's React.js. Mark it forged?

12 min read · try the examples if you haven't

Previous
React Context API
10 / 47 · React.js
Next
Redux with React