Belajar Redux - Performance & Memoized Selectors
Episode 13 of 23

Belajar Redux - Performance & Memoized Selectors

Episode ini membahas performa Redux: reselect dan createSelector untuk memoization, komposisi derived state, menghindari re-render berlebihan dengan shallowEqual, memecah state granular, serta teknik profiling menggunakan Redux DevTools.

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

Pendahuluan

Redux menawarkan performa yang baik secara default, tetapi cara kalian menulis selector sangat menentukan seberapa sering komponen merender ulang. Selector yang mengembalikan array atau objek baru setiap dipanggil akan membuat useSelector menganggap state berubah — padahal data sama saja. Episode 13 membahas cara mengendalikan hal ini dengan reselect dan createSelector.

Tujuannya sederhana: memastikan komponen hanya merender ketika data yang ia baca benar-benar berubah. Kita akan belajar memoization, komposisi selector, shallow equality, struktur state granular, dan cara memverifikasi semuanya dengan Redux DevTools.

Memahami Memoization Selector

Masalah Selector yang Membuat Referensi Baru

Perhatikan selector yang memfilter setiap kali dipanggil:

JSSelector yang selalu membuat array baru
import { createSelector } from "@reduxjs/toolkit"
 
export const selectVisiblePosts = createSelector(
  [(state) => state.posts.items],
  (items) => items.filter((post) => post.published),
)

Tanpa createSelector, kode items.filter(...) menghasilkan array baru pada setiap pemanggilan. useSelector membandingkan hasil dengan ===; karena referensi berbeda, komponen merender ulang setiap kali ada action — bahkan yang tidak menyentuh posts sama sekali.

Cara createSelector Bekerja

createSelector mengingat input terakhir dan hasilnya. Ia mengevaluasi fungsi kombinasi hanya jika salah satu input selector mengembalikan nilai berbeda:

JSAnatomi createSelector
import { createSelector } from "@reduxjs/toolkit"
 
const selectItems = (state) => state.posts.items
const selectKeyword = (state) => state.posts.keyword
 
export const selectFilteredPosts = createSelector(
  [selectItems, selectKeyword],
  (items, keyword) =>
    items.filter((post) => post.title.toLowerCase().includes(keyword)),
)

Selama items dan keyword tidak berubah, hasil filter di-cache dan direferensikan ulang. Komponen yang memakai selectFilteredPosts baru merender ketika keyword atau item benar-benar berganti.

Komposisi Derived State

Selector dari Selector

Selector memoized bisa disusun bertingkat. Hasil satu selector menjadi input selector berikutnya, sehingga komputasi mahal tidak diulang:

JSKomposisi derived state
import { createSelector } from "@reduxjs/toolkit"
 
const selectAllUsers = (state) => state.users.items
const selectActiveUserId = (state) => state.users.activeId
 
export const selectActiveUser = createSelector(
  [selectAllUsers, selectActiveUserId],
  (users, activeId) => users.find((u) => u.id === activeId),
)
 
export const selectActiveUserStats = createSelector(
  [selectActiveUser],
  (user) => {
    if (!user) return null
    return { postCount: user.postIds.length, followerCount: user.followers }
  },
)

selectActiveUserStats hanya dihitung ulang ketika selectActiveUser mengembalikan referensi baru — yaitu saat activeId atau daftar users berubah. Komposisi ini menjaga performa tetap linear terhadap jumlah data yang benar-benar berubah.

Memilih Data Minimal

Semakin besar slice yang diambil selector, semakin banyak komponen yang ikut merender. Pilih hanya field yang dibutuhkan komponen:

Selector minimal di komponen
import { useAppSelector } from "../../app/hooks"
 
export function UserName() {
  const name = useAppSelector((state) => state.users.items[0]?.name)
  return <p>{name}</p>
}

useAppSelector membandingkan hasil dengan ===. Mengembalikan state.users.items[0]?.name (sebuah string) berarti komponen hanya merender ketika nama user pertama berubah — bukan setiap kali users lain bertambah.

shallowEqual dan State Granular

Membandingkan Koleksi dengan shallowEqual

Kadang selector harus mengembalikan objek atau array yang disusun dari beberapa field. shallowEqual membandingkan satu level properti sehingga perubahan pada satu kunci tidak memicu re-render hanya karena objeknya baru:

JSMenggunakan shallowEqual
import { useAppSelector } from "../../app/hooks"
import { shallowEqual } from "react-redux"
 
export function ProfileHeader() {
  const profile = useAppSelector(
    (state) => ({
      name: state.users.items[0]?.name,
      avatar: state.users.items[0]?.avatar,
    }),
    shallowEqual,
  )
  return <header>{profile.name}</header>
}

Tanpa shallowEqual, objek baru di setiap panggilan membuat komponen merender terus-menerus. Dengan shallowEqual, komponen hanya merender ketika name atau avatar benar-benar berubah nilainya.

Memecah State Granular

Merancang state yang granular membantu selector bekerja lebih efisien. Simpan data yang berubah dengan frekuensi berbeda di kunci terpisah:

State granular
{
  "posts": {
    "items": [ ... ],
    "editingId": null,
    "filters": { "keyword": "", "sortBy": "newest" }
  }
}

Mengetik di kotak pencarian hanya mengubah filters.keyword, sehingga selector yang membaca posts.items tetap memakai hasil memoized. Membiarkan semua data dalam satu array besar akan membuat setiap perubahan menandai seluruh array berubah.

Profiling dengan Redux DevTools

Membaca Action Trace dan Perubahan State

Redux DevTools menyediakan informasi yang berguna untuk menemukan sumber re-render:

Install ekstensi DevTools di browser
npm view @reduxjs/toolkit version

Perhatikan tiga hal di tab Action:

  • State diff: field mana saja yang berubah per action. Jika action mengetik huruf mengubah banyak field besar, berarti slice terlalu coarse-grained.
  • Trace: kode yang memdispatch action, berguna menelusuri penyebab perubahan tak terduga.
  • Time-travel: jump ke state tertentu untuk membuktikan apakah komponen merender karena perubahan itu.

Membuktikan Re-render

Untuk memverifikasi hasil optimasi, beri log pada fungsi render:

JSDebug render komponen
export function PostList() {
  const posts = useAppSelector(selectFilteredPosts)
  console.log("PostList render", posts.length)
  return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>
}

Jika console.log muncul saat action yang tidak relevan di-dispatch, berarti selector kalian belum termemoize. Jika muncul hanya saat data yang dibaca berubah, optimasi berhasil.

Warning

Memoization bukan sihir: hasil cache direset setiap kali input berubah, dan createSelector tetap memanggil input selectors pada setiap evaluasi. Jangan menghitung ulang sesuatu yang berat di dalam input selector itu sendiri.

Penutup

Performansi Redux sebagian besar ditentukan oleh selector. createSelector menyediakan memoization untuk derived state, komposisi selector mencegah komputasi berulang, shallowEqual menangani koleksi yang dibaca sebagian, dan state granular mengurangi lingkup re-render. Dengan profil yang dibaca dari Redux DevTools, kalian bisa membuktikan setiap optimasi benar-benar bekerja.

Inti yang harus dibawa pulang:

  • createSelector meng-cache hasil hingga salah satu input selector berubah.
  • Komposisikan selector agar komputasi mahal hanya dijalankan saat perlu.
  • Pilih data minimal yang benar-benar dibaca komponen.
  • shallowEqual membandingkan properti level satu untuk objek dan array.
  • Pecah state per domain agar perubahan kecil tidak memicu re-render luas.
  • Gunakan state diff dan trace di Redux DevTools untuk memverifikasi performa.

Episode 14 berikutnya membawa Redux ke server rendering — kalian akan membuat store per-request di Next.js App Router, memakai Provider untuk SSR, menghidrasi state, dan mengintegrasikan RTK Query dengan Server Components.

Belajar Redux - Performance & Memoized Selectors | Belajar Redux