Deepen PracticeOrdered learning track

React Compiler-Era Performance

Learn React Hooks, State Management, Component Composition, Context Passing, Component Communications & Orchestration - Part 083

Performance React di era React Compiler: automatic memoization, perubahan mental model useMemo/useCallback/memo, purity requirements, compiler-compatible code, directives, migration strategy, profiling, dan failure modes.

7 min read1385 words
PrevNext
Lesson 83123 lesson track68–101 Deepen Practice
#react#react-compiler#performance#memoization+3 more

Part 083 — React Compiler-Era Performance

React Compiler mengubah baseline performance React.

Sebelum compiler, engineer sering melakukan ritual ini:

const value = useMemo(() => compute(input), [input]);
const onSubmit = useCallback(() => submit(input), [input]);
const Card = memo(function Card(props) {
  return <div>{props.title}</div>;
});

Kadang benar. Sering hanya noise.

Di era compiler, pertanyaan berubah dari:

Apa yang harus saya memoize secara manual?

menjadi:

Apakah code saya cukup pure, explicit, dan compiler-compatible sehingga React bisa mengoptimalkannya dengan aman?

Compiler bukan alasan untuk berhenti memahami performance. Justru sebaliknya: compiler membuat performance engineering bergeser dari micro memoization ke program shape, state topology, render purity, dan boundary design.


1. Mental Model

React Compiler adalah build-time optimizer.

Tujuannya bukan mengubah model React, tetapi mengoptimalkan code React yang mengikuti rules React.

Compiler hanya aman bila component dan hook dapat dianalisis sebagai fungsi yang pure terhadap input React:

props + state + context -> JSX

Jika render membaca/mengubah hal mutable sembarangan, menulis ke external system, mengandalkan waktu/random, atau melakukan mutation yang melanggar purity, compiler kehilangan ruang aman untuk optimasi.


2. Apa yang Berubah dari Era Manual Memoization

Sebelum compiler, performance React sering terlihat seperti ini:

profile -> temukan rerender -> tambahkan memo/useMemo/useCallback -> ukur lagi

Di era compiler, baseline yang lebih baik adalah:

pastikan purity -> bentuk state topology benar -> aktifkan compiler -> profile -> memo/manual optimize hanya untuk kasus tersisa

Perbandingan:

AreaSebelum CompilerDengan Compiler
Component render skipbanyak React.memo manualbanyak kasus dapat dioptimalkan otomatis
Value/function identitybanyak useMemo / useCallbackbanyak identity stabil dapat dihasilkan compiler
Early return casessulit dimemoize manual setelah branch tertentucompiler bisa menganalisis lebih dalam pada code valid
Correctnesstetap harus benar tanpa memotetap harus benar tanpa memo
Bottleneck besarstate topology/list/context tetap perlu desaintetap perlu desain
Debuggingmanual memo sering menyembunyikan bau desaincompiler memaksa code lebih pure dan eksplisit

Golden rule tetap sama:

Memoization adalah optimization, bukan correctness mechanism.

Kalau aplikasi hanya benar saat memakai useMemo, biasanya masalahnya bukan performance, tetapi invariant atau mutation.


3. Compiler Tidak Menghilangkan Semua Masalah Performance

Compiler membantu pada kelas masalah tertentu:

unnecessary recomputation
unnecessary rerender karena identity yang bisa dianalisis
manual memoization boilerplate

Compiler tidak otomatis menyelesaikan:

context value terlalu volatile
state diletakkan terlalu tinggi
list ribuan row tanpa virtualization
DOM/layout thrashing
network waterfall
server-state invalidation salah
expensive chart/DOM library
large JSON parsing di main thread
subscription eksternal yang terlalu broad
query key salah
component impure

Dengan kata lain:

Compiler mengurangi biaya render yang tidak perlu.
Compiler tidak memperbaiki arsitektur state yang salah.

4. Performance Model Baru

Compiler membuat pertanyaan performance menjadi lebih struktural:

Kenapa update ini menyentuh subtree sebesar ini?
Kenapa consumer context terlalu banyak?
Kenapa state high-frequency berada di provider global?
Kenapa derived data mahal dihitung di banyak tempat?
Kenapa expensive DOM tetap dimount semua?

5. Compiler-Compatible Code

Compiler paling efektif pada code yang mengikuti rules ini:

component pure
hook pure kecuali effect/event boundary
props/state tidak dimutasi
render tidak punya side effect
object/function dependency eksplisit
state shape tidak ambigu
external mutable object tidak dibaca sebagai sumber render tanpa subscription

Contoh buruk:

let globalCounter = 0;

function CounterLabel() {
  globalCounter += 1;
  return <span>{globalCounter}</span>;
}

Masalah:

  • render melakukan mutation,
  • output bergantung pada jumlah render,
  • concurrent rendering dapat membuat hasil tidak intuitif,
  • compiler tidak punya model aman.

Versi benar:

function CounterLabel({ count }: { count: number }) {
  return <span>{count}</span>;
}

Input eksplisit. Output deterministik.


6. Purity Lebih Penting daripada useMemo

Code ini sering tampak “optimized”, tetapi sebenarnya rapuh:

function ProductList({ products }: { products: Product[] }) {
  const visible = useMemo(() => {
    products.sort((a, b) => a.name.localeCompare(b.name));
    return products;
  }, [products]);

  return <Rows rows={visible} />;
}

useMemo tidak menyelamatkan mutation.

sort memutasi array asal. Kalau products berasal dari props/query cache/store, ini bisa merusak referential integrity dan membuat cache/entity invariant bocor.

Versi benar:

function ProductList({ products }: { products: Product[] }) {
  const visible = [...products].sort((a, b) => a.name.localeCompare(b.name));
  return <Rows rows={visible} />;
}

Di era compiler, bentuk ini lebih sehat. Compiler punya kesempatan untuk mengoptimalkan kalkulasi yang pure. Manual memoization bisa dipertimbangkan lagi hanya bila profiling membuktikan kalkulasinya mahal dan belum cukup dioptimalkan.


7. Apa Nasib useMemo, useCallback, dan memo?

Mereka tidak hilang. Tetapi frekuensinya harus turun.

Gunakan manual memoization saat ada alasan eksplisit:

ToolMasih Valid Saat
useMemokalkulasi mahal, dependency jelas, hasil dipakai expensive subtree, compiler tidak cukup atau belum aktif
useCallbackfunction identity menjadi bagian dari external subscription, imperative API, memo child boundary, atau dependency effect valid
memolibrary boundary, compiled/uncompiled boundary, child sangat mahal, props stabil dan comparison murah
custom equalitydata kecil, comparison aman, measured benefit jelas

Jangan gunakan hanya karena:

takut rerender
lint noise
kebiasaan lama
mengira function baru selalu mahal
mengira object literal selalu masalah

Function allocation biasanya bukan bottleneck utama. Yang mahal adalah render subtree besar, DOM update, layout, expensive calculation, dan fan-out state.


8. Manual Memoization yang Masih Arsitektural

Ada memoization yang bukan sekadar micro-optimization, tetapi boundary contract.

Contoh context value:

function PermissionProvider({ children }: { children: React.ReactNode }) {
  const permissions = usePermissionSnapshot();

  const value = useMemo(() => ({
    can(action: string, resource: string) {
      return evaluatePermission(permissions, action, resource);
    },
  }), [permissions]);

  return (
    <PermissionContext value={value}>
      {children}
    </PermissionContext>
  );
}

Kenapa masih masuk akal?

  • provider value identity memengaruhi seluruh consumer,
  • context fan-out bisa besar,
  • API capability harus stabil,
  • permissions adalah dependency aktual.

Tetapi jika permissions berubah sangat sering, useMemo tidak cukup. Solusinya bukan memo lebih banyak, tetapi pecah context atau pindah ke selector/external store.


9. Compiler Directives: Escape Hatch, Bukan Gaya Hidup

React Compiler menyediakan directive seperti:

function Component() {
  "use memo";
  return <div />;
}

Dan:

function Component() {
  "use no memo";
  return <div />;
}

Gunakan directive sebagai boundary eksplisit:

DirectiveMakna Arsitektural
"use memo"component/hook ini sengaja ingin dianalisis/dioptimalkan oleh compiler
"use no memo"component/hook ini sengaja dikecualikan sementara atau karena incompatible boundary

Jangan menabur directive tanpa alasan. Directive harus punya comment/ADR kecil jika dipakai di code production:

function ThirdPartyChartBridge(props: ChartBridgeProps) {
  "use no memo";
  // This component wraps a mutable third-party chart instance.
  // Keep it outside compiler optimization until the adapter is refactored.
  return <ChartBridgeImpl {...props} />;
}

10. Compiler Boundary dengan Library dan Third-Party Code

Codebase production jarang full homogeneous.

Biasanya ada:

app code compiled
internal component library maybe compiled
legacy package not compiled
third-party package not compiled
chart/map/editor library imperative
microfrontend boundary

Compiler boundary strategy:

At boundary, manual memoization masih bisa berguna:

const editorConfig = useMemo(() => ({
  readonly,
  language,
  theme,
}), [readonly, language, theme]);

return <LegacyEditor config={editorConfig} />;

Alasannya bukan karena semua object literal buruk, tetapi karena legacy child mungkin melakukan expensive re-initialization saat identity berubah.


11. React Compiler dan State Topology

Compiler tidak bisa memperbaiki state yang diletakkan terlalu tinggi.

Buruk:

function AppShell() {
  const [draftSearch, setDraftSearch] = useState("");

  return (
    <AppProviders>
      <Header search={draftSearch} onSearchChange={setDraftSearch} />
      <MainRoutes draftSearch={draftSearch} />
      <Footer />
    </AppProviders>
  );
}

Jika draftSearch hanya dipakai di satu search page, state ini terlalu tinggi. Setiap ketikan menyentuh app shell.

Lebih baik:

function SearchPage() {
  const [draftSearch, setDraftSearch] = useState("");

  return (
    <SearchLayout>
      <SearchBox value={draftSearch} onChange={setDraftSearch} />
      <SearchResults draftSearch={draftSearch} />
    </SearchLayout>
  );
}

Compiler bisa mengurangi beberapa biaya render, tetapi state topology yang benar tetap mengurangi render radius secara fundamental.


12. React Compiler dan Context

Compiler tidak mengubah fakta bahwa context consumer membaca value context sebagai input render.

Jika provider value berubah, consumer yang membaca context perlu dihitung ulang terhadap value baru.

Buruk:

<AppContext value={{ user, permissions, theme, flags, draftSearch, setDraftSearch }}>
  <App />
</AppContext>

Masalah:

  • terlalu banyak domain dalam satu context,
  • value object baru mudah berubah,
  • state high-frequency bercampur dengan low-frequency capability,
  • consumer sulit diketahui dependency sebenarnya.

Lebih baik:

<AuthContext value={authValue}>
  <ThemeContext value={themeValue}>
    <FeatureFlagContext value={flagValue}>
      <App />
    </FeatureFlagContext>
  </ThemeContext>
</AuthContext>

Atau untuk state high-frequency:

context hanya membawa store/capability stabil
component subscribe ke slice lewat selector

Compiler membantu render lokal, bukan memperbaiki context taxonomy.


13. React Compiler dan External Store

External store tetap harus mengikuti kontrak snapshot.

Kalau getSnapshot selalu membuat object baru, render bisa loop atau selalu miss:

function getSnapshot() {
  return { ...store.getState() }; // buruk jika dipanggil setiap render tanpa caching
}

Benar:

let currentState = initialState;
let currentSnapshot = currentState;

function getSnapshot() {
  return currentSnapshot;
}

function setState(updater: (state: State) => State) {
  const next = updater(currentState);
  if (Object.is(next, currentState)) return;
  currentState = next;
  currentSnapshot = next;
  listeners.forEach(listener => listener());
}

Compiler tidak mengganti useSyncExternalStore. External subscription harus benar secara semantic.


14. Profiling di Era Compiler

Jangan berasumsi compiler menyelesaikan semua hal. Tetap ukur.

Profiling workflow:

1. Reproduce interaction lambat.
2. Aktifkan React DevTools Profiler / browser performance tools.
3. Identifikasi apakah bottleneck render, commit, layout, network, JS CPU, atau memory.
4. Cek render radius dan komponen yang sering render.
5. Cek context/provider/store/query update source.
6. Cek apakah compiler aktif untuk area itu.
7. Refactor topology/boundary sebelum menambah manual memo.
8. Ukur ulang.

Diagnosis matrix:

SymptomKemungkinan PenyebabFix Pertama
input lagexpensive subtree ikut update urgentcolocate state, useDeferredValue, split component
semua halaman rerenderstate di root/provider globalturunkan state, split provider
context consumer banyak rerenderprovider value volatilesplit context, memo value, selector store
chart re-initconfig identity berubah atau imperative adapter burukstable config, adapter effect cleanup
list lambatterlalu banyak DOM/rendervirtualization, row memo, selector per row
update cache lambatinvalidation terlalu luasquery key hierarchy, targeted update
memory naikmemo cache/listener leakcleanup, reduce cache cardinality

15. Migration Strategy ke Compiler-Era Codebase

Jangan migrasi dengan “hapus semua memo”. Itu sembrono.

Gunakan staged migration:

Langkah praktis:

1. Tambahkan lint rules React Hooks/Compiler.
2. Aktifkan compiler di area non-kritis dulu.
3. Perbaiki mutation/render impurity yang terdeteksi.
4. Jangan hapus memo manual yang menjaga library boundary sebelum diukur.
5. Tambahkan regression test untuk workflow penting.
6. Profile before/after untuk interaksi mahal.
7. Dokumentasikan exception dengan ADR/comment singkat.

16. Manual Memo Removal Checklist

Sebelum menghapus useMemo:

Apakah kalkulasi mahal?
Apakah hasil dikirim ke memo child?
Apakah hasil dipakai sebagai dependency effect/subscription?
Apakah object identity dipakai third-party bridge?
Apakah compiler aktif di file/component ini?
Apakah ada benchmark/profile before-after?

Sebelum menghapus useCallback:

Apakah callback dikirim ke memo child?
Apakah callback dipakai sebagai dependency effect?
Apakah callback didaftarkan ke external system?
Apakah callback identity menjadi bagian dari imperative contract?

Sebelum menghapus memo:

Apakah component expensive?
Apakah props biasanya stabil?
Apakah child berada di compiled boundary?
Apakah parent sering render karena state lain?
Apakah custom equality aman?

Jika jawabannya tidak jelas, jangan refactor berdasarkan selera. Ukur.


17. Compiler-Era Component API Design

Agar compiler dan performance natural, desain component API harus menghindari konfigurasi yang terlalu dinamis.

Lebih baik:

<DataTable
  rows={rows}
  columns={columns}
  getRowId={(row) => row.id}
  onRowSelect={handleRowSelect}
/>

Daripada:

<DataTable
  config={{
    rows,
    columns,
    callbacks: {
      onSelect: handleRowSelect,
    },
    behavior: {
      selectable: true,
      virtualized: true,
    },
  }}
/>

Konfigurasi object besar sering membuat dependency tidak jelas:

Apa yang berubah?
Apa yang mahal?
Apa yang harus invalidate?
Apa yang harus preserve?

API eksplisit membantu manusia, compiler, profiler, dan tests.


18. Common Failure Modes

18.1 Compiler dijadikan alasan tidak profile

Salah:

Compiler sudah aktif, berarti performance aman.

Benar:

Compiler mengurangi sebagian overhead. Bottleneck tetap harus diukur.

18.2 Menghapus manual memo yang melindungi boundary legacy

Jika component membungkus chart/editor/map imperative, identity mungkin mengontrol re-initialization. Hapus memo hanya setelah adapter benar.

18.3 Code impure lalu berharap compiler menyelamatkan

Compiler tidak membuat code impure menjadi benar. Perbaiki purity dulu.

18.4 Provider global tetap terlalu volatile

Compiler tidak mengubah semantic context. Provider volatile tetap membuat consumer membaca value baru.

18.5 Query/cache invalidation dianggap render problem

Kadang UI lambat atau salah bukan karena render, tetapi karena cache invalidation terlalu luas atau stale.

18.6 Custom equality lebih mahal dari render

Deep compare besar dapat lebih mahal daripada render yang ingin dihindari.


19. Production Review Checklist

Gunakan checklist ini saat review PR performance di era compiler:

[ ] Component render pure dan deterministic.
[ ] Tidak ada mutation props/state/query data.
[ ] State berada di owner tersempit yang benar.
[ ] Context tidak mencampur state volatile dan capability low-frequency.
[ ] External store memakai snapshot stabil.
[ ] Manual memoization punya alasan eksplisit.
[ ] Compiler exception punya comment/ADR kecil.
[ ] Performance claim didukung profiling.
[ ] Large list memakai virtualization/row boundary bila perlu.
[ ] Async/server-state bottleneck tidak diperlakukan sebagai render-only problem.

20. Latihan Implementasi

Ambil satu page React yang terasa lambat.

Tulis audit berikut:

Interaction:
Expected latency:
Observed latency:
Update source:
Render radius:
Compiler enabled? yes/no
Manual memoization present:
Context providers touched:
External stores touched:
Server queries touched:
Main bottleneck class:
Refactor candidate:
Measured after:

Kemudian lakukan refactor kecil:

1. Turunkan state yang terlalu tinggi, atau
2. Split context yang volatile, atau
3. Pindahkan derived data ke selector, atau
4. Tambahkan deferred/transition boundary, atau
5. Stabilkan legacy bridge props, atau
6. Hapus memo manual yang terbukti tidak perlu.

Jangan lakukan semua sekaligus. Performance engineering yang baik butuh isolasi sebab-akibat.


21. Kesimpulan

React Compiler menaikkan baseline performance, tetapi tidak menggantikan arsitektur.

Engineer kuat di era compiler bukan yang paling banyak menulis useMemo, tetapi yang bisa menjawab:

Apakah code ini pure?
Apakah state topology-nya benar?
Apakah provider terlalu volatile?
Apakah expensive subtree punya boundary tepat?
Apakah bottleneck render, commit, layout, network, atau cache?
Apakah manual memo ini masih punya alasan setelah compiler aktif?

Performance React modern adalah gabungan:

purity
+ state ownership
+ compiler-compatible code
+ context/store/query boundary
+ profiling discipline
+ targeted manual optimization

Compiler mengurangi boilerplate. Ia tidak menghapus kebutuhan berpikir.


References

Lesson Recap

You just completed lesson 83 in deepen practice. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.