Tutorials Logic, IN info@tutorialslogic.com

React Performance memo, lazy, Suspense: Tutorial, Examples, FAQs & Interview Tips

React Performance memo, lazy, Suspense

React in React is best learned by connecting the rule to an interactive form or modal. Start with the smallest component or hook, observe the output, and then add one realistic constraint so the concept becomes practical.

The key habit for this lesson is to watch props, state, and rendered JSX as it changes. That makes the topic easier to debug, easier to explain in interviews, and easier to use in real code without memorizing isolated syntax.

Why Performance Matters in React

React performance optimization is the process of reducing wasted rendering work, keeping user interactions responsive, and loading only the code that is needed at the right time. Many React applications feel fast in the beginning, but as components grow, lists get larger, dashboards add charts, and more routes are bundled together, users may begin to notice typing lag, slow navigation, or delayed updates.

The goal is not to avoid re-rendering completely. React is built around re-rendering. The real goal is to avoid unnecessary work. If a component re-renders because something the user can see actually changed, that is normal. But if a heavy component re-renders even though its input did not change, or if the browser is forced to paint thousands of rows that are not visible, that is wasted work and a good place to optimize.

A smart performance strategy in React usually follows three steps: measure the problem, identify the real bottleneck, and apply the smallest optimization that solves that specific issue.

Common Causes of Slow React Screens

  • Parent components update often and force heavy children to re-render
  • Expensive filtering, sorting, grouping, or calculations run on every render
  • Functions or object props change identity on every render
  • Large route modules are included in the initial bundle even when not immediately needed
  • Huge tables or lists render all rows at once
  • State is placed too high in the tree, causing unrelated branches to update
  • Slow updates block urgent interactions such as typing

Main React Performance Tools

Technique What it does Best use case
React.memo() Skips child re-render when props are unchanged Expensive child components with stable props
useMemo() Caches expensive computed values Sorting, filtering, derived totals, grouped data
useCallback() Keeps function references stable Callbacks passed to memoized children
lazy() + Suspense Code-splits and loads features later Routes, dashboards, modals, heavy widgets
useTransition() Marks updates as non-urgent Slow filters, large list updates, heavy screen changes
useDeferredValue() Lets a derived value update later Search inputs with expensive result rendering
Virtualization Renders only visible list rows Very large lists and tables
State colocation Keeps state near where it is used Reducing broad unnecessary tree updates

Example 1: Preventing Unnecessary Re-renders

A common React performance problem is that a parent component changes one small piece of state and accidentally causes a heavy child to render again. If the child is pure and receives the same data as before, wrapping it in React.memo() can help React skip that work. If the child also receives functions or calculated arrays, we often combine React.memo with useCallback and useMemo.

If the user clicks the counter button, the parent component re-renders. But the table and rows can be skipped when their props stay the same. This is a good example of performance optimization that preserves behavior while reducing unnecessary rendering.

React.memo, useMemo, and useCallback

React.memo, useMemo, and useCallback
import { useCallback, useMemo, useState } from 'react'
import ProductTable from './ProductTable'

const initialProducts = [
    { id: 1, name: 'Laptop', price: 65000, category: 'Electronics' },
    { id: 2, name: 'Keyboard', price: 1800, category: 'Accessories' },
    { id: 3, name: 'Mouse', price: 950, category: 'Accessories' },
]

function ProductPage() {
    const [search, setSearch] = useState('')
    const [counter, setCounter] = useState(0)
    const [products, setProducts] = useState(initialProducts)

    const filteredProducts = useMemo(() => {
        console.log('Filtering products')
        return products.filter(product =>
            product.name.toLowerCase().includes(search.toLowerCase())
        )
    }, [products, search])

    const totalPrice = useMemo(() => {
        return filteredProducts.reduce((sum, product) => sum + product.price, 0)
    }, [filteredProducts])

    const handleDelete = useCallback((id) => {
        setProducts(current => current.filter(product => product.id !== id))
    }, [])

    return (
        <div>
            <input
                value={search}
                onChange={e => setSearch(e.target.value)}
                placeholder="Search product"
            />

            <button onClick={() => setCounter(c => c + 1)}>
                Counter: {counter}
            </button>

            <p>Visible products: {filteredProducts.length}</p>
            <p>Visible total: Rs. {totalPrice}</p>

            <ProductTable products={filteredProducts} onDelete={handleDelete} />
        </div>
    )
}

export default ProductPage

Example 1: Preventing Unnecessary Re-renders

Example 1: Preventing Unnecessary Re-renders
import { memo } from 'react'
import ProductRow from './ProductRow'

const ProductTable = memo(function ProductTable({ products, onDelete }) {
    console.log('Rendering ProductTable')

    return (
        <table>
            <thead>
                <tr>
                    <th>Name</th>
                    <th>Category</th>
                    <th>Price</th>
                    <th>Action</th>
                </tr>
            </thead>
            <tbody>
                {products.map(product => (
                    <ProductRow key={product.id} product={product} onDelete={onDelete} />
                ))}
            </tbody>
        </table>
    )
})

export default ProductTable

Example 1: Preventing Unnecessary Re-renders

Example 1: Preventing Unnecessary Re-renders
import { memo } from 'react'

const ProductRow = memo(function ProductRow({ product, onDelete }) {
    console.log('Rendering row:', product.id)

    return (
        <tr>
            <td>{product.name}</td>
            <td>{product.category}</td>
            <td>Rs. {product.price}</td>
            <td>
                <button onClick={() => onDelete(product.id)}>Delete</button>
            </td>
        </tr>
    )
})

export default ProductRow

Example 2: Improving Initial Load with Code Splitting

Sometimes the app is not slow because of re-rendering. Sometimes it is slow because too much JavaScript is downloaded before the first page becomes interactive. Route-based code splitting is one of the easiest and most valuable optimizations in React applications.

This pattern loads the code only when the user actually opens that page or feature. It reduces the initial bundle size and helps the application become usable sooner.

lazy and Suspense

lazy and Suspense
import { lazy, Suspense } from 'react'
import { Route, Routes } from 'react-router-dom'
import Home from './pages/Home'

const Dashboard = lazy(() => import('./pages/Dashboard'))
const Reports = lazy(() => import('./pages/Reports'))
const Admin = lazy(() => import('./pages/Admin'))

function Loader() {
    return <p>Loading page...</p>
}

function App() {
    return (
        <Suspense fallback={<Loader />}>
            <Routes>
                <Route path="/" element={<Home />} />
                <Route path="/dashboard" element={<Dashboard />} />
                <Route path="/reports" element={<Reports />} />
                <Route path="/admin" element={<Admin />} />
            </Routes>
        </Suspense>
    )
}

export default App

Example 2: Improving Initial Load with Code Splitting

Example 2: Improving Initial Load with Code Splitting
import { lazy, Suspense, useState } from 'react'

const RevenueChart = lazy(() => import('./RevenueChart'))

function Reports() {
    const [showChart, setShowChart] = useState(false)

    return (
        <section>
            <h2>Reports</h2>
            <button onClick={() => setShowChart(true)}>
                Open Chart
            </button>

            {showChart && (
                <Suspense fallback={<p>Loading chart...</p>}>
                    <RevenueChart />
                </Suspense>
            )}
        </section>
    )
}

export default Reports

Example 3: Keeping Typing Responsive with useTransition

When a user types into a search field, the input should feel immediate. If a large list is filtered on every keystroke and the filter is expensive, the UI may feel delayed. useTransition() lets React treat the input update as urgent and the results update as non-urgent.

useTransition and useDeferredValue

useTransition and useDeferredValue
import { useMemo, useState, useTransition } from 'react'

const items = Array.from({ length: 10000 }, (_, index) => ({
    id: index + 1,
    name: `Item ${index + 1}`
}))

function SearchPage() {
    const [input, setInput] = useState('')
    const [query, setQuery] = useState('')
    const [isPending, startTransition] = useTransition()

    const results = useMemo(() => {
        return items.filter(item =>
            item.name.toLowerCase().includes(query.toLowerCase())
        )
    }, [query])

    function handleChange(event) {
        const value = event.target.value
        setInput(value)

        startTransition(() => {
            setQuery(value)
        })
    }

    return (
        <div>
            <input value={input} onChange={handleChange} placeholder="Search..." />
            {isPending && <p>Updating results...</p>}
            <ul style={{ opacity: isPending ? 0.6 : 1 }}>
                {results.slice(0, 20).map(item => (
                    <li key={item.id}>{item.name}</li>
                ))}
            </ul>
        </div>
    )
}

export default SearchPage

Example 3: Keeping Typing Responsive with useTransition

Example 3: Keeping Typing Responsive with useTransition
import { useDeferredValue, useMemo } from 'react'

function FilteredList({ query, items }) {
    const deferredQuery = useDeferredValue(query)
    const isStale = query !== deferredQuery

    const filtered = useMemo(() => {
        return items.filter(item =>
            item.name.toLowerCase().includes(deferredQuery.toLowerCase())
        )
    }, [items, deferredQuery])

    return (
        <div style={{ opacity: isStale ? 0.5 : 1 }}>
            <p>Matches: {filtered.length}</p>
            <ul>
                {filtered.slice(0, 10).map(item => (
                    <li key={item.id}>{item.name}</li>
                ))}
            </ul>
        </div>
    )
}

Example 4: Virtualizing Large Lists

If thousands of DOM nodes are rendered at once, the browser itself becomes the bottleneck. In that case, memoization alone is not enough. List virtualization renders only the rows visible in the viewport.

Virtualized List with react-window

Virtualized List with react-window
import { FixedSizeList } from 'react-window'

const rows = Array.from({ length: 5000 }, (_, index) => ({
    id: index,
    label: `Row ${index + 1}`
}))

function Row({ index, style }) {
    return <div style={style}>{rows[index].label}</div>
}

function BigList() {
    return (
        <FixedSizeList
            height={400}
            width={500}
            itemCount={rows.length}
            itemSize={40}
        >
            {Row}
        </FixedSizeList>
    )
}

export default BigList

How to Measure React Performance

  • Use React DevTools Profiler to identify slow components and repeated renders
  • Use browser Performance tools to inspect scripting, layout, and paint time
  • Test a production build because development mode can exaggerate work
  • Check bundle size when the first page feels slow
  • Confirm the bottleneck before adding memoization or other complexity

Common Mistakes to Avoid

Mistake Problem Better approach
Using useMemo for every expression Adds complexity without real benefit Memoize only expensive work
Wrapping tiny components in React.memo automatically May not improve anything Memoize expensive or frequently repeated children
Passing new objects or callbacks each render Breaks memoization opportunities Use stable values where needed
Rendering huge lists directly Creates too many DOM nodes Use virtualization
Optimizing before measuring May solve the wrong problem Profile first

Best Practices

  • Keep state close to the components that actually need it
  • Memoize expensive calculations, not cheap expressions
  • Use React.memo only when prop stability makes it meaningful
  • Split large bundles with lazy loading
  • Virtualize large lists and data-heavy tables
  • Use transitions or deferred values when urgent interactions should stay smooth
  • Prefer readable optimizations that future developers can understand

Summary

React performance optimization is about reducing unnecessary work, not avoiding rendering completely. A fast React application usually comes from a mix of better state placement, selective memoization, stable callbacks, code splitting, and virtualization for large lists. The strongest improvements usually come from solving the real bottleneck rather than applying every optimization tool everywhere.

If you remember one principle, let it be this: measure first, then optimize the part that is actually slow. That keeps your code simpler, your optimization choices more effective, and your React application easier to maintain over time.

Applied guide for React

Use React when the program needs a clear answer to a specific problem, not because the keyword looks familiar. In a real React task, first name the input, then name the transformation, then name the output. This small discipline shows whether the topic is being used correctly or only copied from an example.

A reliable practice flow is: create the smallest working component or hook, add one normal case, add one edge case such as empty, invalid, and repeated submissions, and then confirm the result with React DevTools and test output. If the result surprises you, reduce the code until the behavior is visible again.

The most common trap here is validating only the happy path. Avoid it by writing one sentence before the code that explains why React is the right choice. After the code runs, verify the lesson by doing this: submit the form with both valid and invalid values.

  • Identify the exact problem solved by React.
  • Trace props, state, and rendered JSX before and after the main operation.
  • Keep one intentionally broken version and explain the fix.
  • Connect the example to an interactive form or modal so the idea feels concrete.
Key Takeaways
  • I can explain where React fits inside an interactive form or modal.
  • I can point to the exact props, state, and rendered JSX affected by this topic.
  • I tested a normal case and an edge case involving empty, invalid, and repeated submissions.
  • I verified the result with React DevTools and test output instead of assuming it worked.
  • I can describe the main mistake: validating only the happy path.
Common Mistakes to Avoid
WRONG Validating only the happy path.
RIGHT Write the expected behavior first, then make the example prove it.
A one-line expectation turns the code from copied syntax into a testable idea.
WRONG Practicing only the perfect input.
RIGHT Also test empty, invalid, and repeated submissions before considering the lesson complete.
The edge case is where most interview follow-up questions begin.
WRONG Looking only at the final output.
RIGHT Trace props, state, and rendered JSX through each important step.
Tracing makes debugging faster because you can see the first incorrect state.

Practice Tasks

  • Build one small component or hook that demonstrates React in an interactive form or modal.
  • Change the example to include empty, invalid, and repeated submissions and record the difference.
  • Break the example by deliberately validating only the happy path, then write the corrected version.
  • Explain the finished example in five bullet points: input, operation, output, failure case, and verification.

Frequently Asked Questions

Use it when the problem matches the behavior shown in the example and when the result can be verified through React DevTools and test output.

Start with a tiny case, then test empty, invalid, and repeated submissions. The main warning sign is validating only the happy path.

Trace props, state, and rendered JSX, predict the result, run the example, and compare your prediction with the actual output.

Ready to Level Up Your Skills?

Explore 500+ free tutorials across 20+ languages and frameworks.