Learn ReactJS - Performance Optimization
Episode 15 of 24

Learn ReactJS - Performance Optimization

This episode covers profiling with React DevTools and browser tools, memoization with useMemo and useCallback for expensive computations, virtualization for long lists, and patterns for avoiding unnecessary re-renders in rendering.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

A slow app is an app losing users. Episode 15 covers performance optimization from the right angle: measure first, optimize afterwards. Without measurement, optimization is just guessing that adds complexity.

We'll use React DevTools and browser tools for profiling, apply memoization with useMemo and useCallback, use virtualization for very long lists, and close with patterns for avoiding unnecessary re-renders.

Profiling with React DevTools and Browser Tools

The Profiler in React DevTools

React DevTools has a Profiler tab that records each component's render, duration, and cause. The correct workflow:

Profiling workflow
catat baseline -> lakukan interaksi -> berhenti rekam -> cari render mahal -> optimasi -> ukur ulang

When you find a slow component, the first question isn't "how do I optimize", but "does this component need to re-render". The Profiler answers that question with evidence, not feelings.

The Browser Performance Panel

For issues involving layout and painting, use the browser's Performance panel. Record an interaction, then examine the flame chart to find functions eating up time. Combine it with the Rendering tab, which highlights elements being repainted on every frame.

Memoization with useMemo and useCallback

useMemo for Expensive Computations

Episode 6 already introduced both hooks. In episode 15 we look at when they're truly valuable: expensive computations run on every render:

JSuseMemo for expensive computation
import { useMemo } from "react"
 
function Statistik({ transaksi }) {
  const total = useMemo(() => {
    return transaksi.reduce((acc, t) => acc + t.nilai, 0)
  }, [transaksi])
 
  return <p>Total: {total}</p>
}

useMemo(() => reduce(...), [transaksi]) recalculates only when transaksi changes. Without it, reduce runs on every render even when the data hasn't changed.

When useCallback Pays Off

useCallback keeps a function's reference stable. Its real value shows when the function is passed to a component wrapped in memo:

JSmemo plus useCallback
import { memo, useCallback, useState } from "react"
 
const Tombol = memo(function Tombol({ onClick }) {
  return <button onClick={onClick}>Klik</button>
})
 
function App() {
  const [jumlah, setJumlah] = useState(0)
 
  const handleKlik = useCallback(() => setJumlah((n) => n + 1), [])
 
  return (
    <div>
      <Tombol onClick={handleKlik} />
      <p>{jumlah}</p>
    </div>
  )
}

memo makes Tombol re-render only when props change. Without useCallback, handleKlik gets a new reference every render and memo becomes useless. The two techniques work in tandem.

Virtualization for Long Lists

The Problem of Rendering Thousands of Items

Rendering 10,000 items in a single map overwhelms the browser: thousands of DOM nodes created at once. Virtualization solves this by only rendering the items visible on screen:

Install react-window
npm install react-window
JSVirtual list with react-window
import { FixedSizeList } from "react-window"
 
function DaftarBesar({ items }) {
  return (
    <FixedSizeList
      height={400}
      width="100%"
      itemCount={items.length}
      itemSize={35}
    >
      {({ index, style }) => (
        <div style={style}>{items[index].nama}</div>
      )}
    </FixedSizeList>
  )
}

FixedSizeList from react-window only renders the items visible in a viewport of height={400}. As the user scrolls, old items are discarded and new ones rendered — hundreds of nodes, not thousands. For varying item sizes, use VariableSizeList.

Avoiding Unnecessary Renders

Move State Down

State declared too high causes the whole subtree to re-render when only one part changes. Move state as close as possible to the component that uses it:

JSState moved down
// buruk: state di atas menyebabkan seluruh daftar re-render
function App() {
  const [teks, setTeks] = useState("")
  return (
    <div>
      <input value={teks} onChange={(e) => setTeks(e.target.value)} />
      <DaftarBesar items={items} />
    </div>
  )
}
 
// baik: input dibungkus komponen sendiri
function InputNama() {
  const [teks, setTeks] = useState("")
  return <input value={teks} onChange={(e) => setTeks(e.target.value)} />
}

Moving state into the InputNama component keeps DaftarBesar from re-rendering on every keystroke. This rule often gives the biggest improvement with the smallest change.

The Children Pattern for Stable Subtrees

For cases where state can't be moved, wrap the stable part as children so React doesn't re-render it. Combine this technique with memo and Profiler measurements for the right decisions.

Warning

Don't memoize everything. memo, useMemo, and useCallback add comparison overhead and confuse code readers. Measure with the Profiler, optimize what's actually slow, then measure again.

Conclusion

Episode 15 completed your React performance toolkit: profiling with React DevTools and browser tools, memoization with useMemo, useCallback, and memo, virtualization with react-window, and patterns for avoiding re-renders by moving state down.

Key takeaways:

  • Measure with the Profiler first, then optimize — don't guess.
  • useMemo for expensive computations; useCallback for stable references.
  • memo and useCallback work in tandem to stop child re-renders.
  • Virtualization solves lists of tens of thousands of items.
  • Move state down so subtrees don't re-render.
  • Premature optimization adds complexity without measurable benefit.

In the next episode, episode 16, we'll cover testing & quality — unit testing with Jest and React Testing Library, snapshot testing, integration testing for component behavior, and E2E testing with Cypress or Playwright. Code quality is guaranteed, not hoped for.

Learn ReactJS - Performance Optimization | Learn ReactJS