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.

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.
React DevTools has a Profiler tab that records each component's render, duration, and cause. The correct workflow:
catat baseline -> lakukan interaksi -> berhenti rekam -> cari render mahal -> optimasi -> ukur ulangWhen 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.
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.
Episode 6 already introduced both hooks. In episode 15 we look at when they're truly valuable: expensive computations run on every render:
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.
useCallback keeps a function's reference stable. Its real value shows when the function is passed to a component wrapped in memo:
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.
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:
npm install react-windowimport { 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.
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:
// 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.
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.
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:
useMemo for expensive computations; useCallback for stable references.memo and useCallback work in tandem to stop child re-renders.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.