Start HereOrdered learning track

Latency, Throughput, and Waterfalls

Learn React Client-Server Communication - Part 006

Mental model performa request di React: latency, throughput, waterfall, critical path, route transitions, request parallelism, cache, prefetch, streaming, dan observability.

23 min read4459 words
PrevNext
Lesson 0672 lesson track01–13 Start Here
#react#client-server#performance#http+4 more

Part 006 — Latency, Throughput, and Waterfalls

Di Part 005 kita membedah lifecycle request-response. Sekarang kita lihat konsekuensi performanya.

Banyak aplikasi React tidak lambat karena satu API sangat lambat. Ia lambat karena banyak keputusan kecil membentuk critical path yang buruk:

  • fetch dimulai setelah render padahal bisa dimulai sebelum render,
  • request penting dibuat serial padahal bisa paralel,
  • data kecil menunggu bundle besar,
  • route transition menunggu endpoint yang tidak dibutuhkan untuk first view,
  • setiap row memicu request sendiri,
  • mutation sukses lalu seluruh halaman direfetch,
  • CORS preflight terjadi berulang karena header custom yang tidak perlu,
  • JSON terlalu besar dan parse/render-nya lebih mahal dari network,
  • cache tidak dipakai karena resource identity tidak stabil,
  • prefetch dilakukan terlalu agresif dan mengganggu request yang lebih penting.

Part ini membangun mental model untuk membaca dan mendesain performa komunikasi client-server di React.


1. Thesis Utama

Performance React client-server bukan hanya “API cepat”.

Performance adalah hasil dari tiga hal:

  1. Latency — berapa lama satu unit kerja selesai dari sudut user.
  2. Throughput — berapa banyak unit kerja bisa diproses dalam periode tertentu tanpa collapse.
  3. Waterfall — urutan dependency yang menentukan apakah kerja berjalan paralel atau serial.

Formula sederhananya:

perceived_speed = useful_work_started_early + critical_work_parallelized + unnecessary_work_removed + stale_data_reused_safely

Aplikasi terasa cepat bukan karena semua request selalu cepat.

Aplikasi terasa cepat karena:

  • request penting dimulai lebih awal,
  • request tidak penting ditunda,
  • data yang sudah cukup baik dipakai dulu,
  • dependency serial dikurangi,
  • payload diperkecil,
  • streaming/progressive rendering dipakai saat cocok,
  • user diberi feedback sesuai lifecycle.

2. Latency Bukan Satu Angka

Ketika seseorang bilang “API ini 800 ms”, tanyakan:

“800 ms itu bagian mana?”

Request timing terdiri dari beberapa fase.

DevTools bisa menampilkan sebagian besar fase network. Tetapi aplikasi React juga punya fase setelah response:

  • response.json() parsing,
  • schema validation,
  • cache write,
  • selector/derived computation,
  • reconciliation,
  • component render,
  • DOM update,
  • paint,
  • effect lanjutan.

Jadi “network selesai” tidak sama dengan “user melihat data”.


3. Vocabulary yang Harus Presisi

IstilahMaknaKesalahan umum
latencywaktu satu operasi dari start sampai selesaidisamakan dengan bandwidth
RTTround-trip time client-server-clientdianggap sama dengan server processing
TTFBtime to first byte responsedianggap sama dengan total response time
download timewaktu menerima bodydiabaikan saat payload besar
parse timewaktu mengubah bytes menjadi objectsering tidak terlihat di network tab
render timewaktu React/DOM menampilkan datasering disalahkan ke API
throughputjumlah kerja per satuan waktudisamakan dengan latency rendah
concurrencyjumlah kerja bersamaandianggap selalu meningkatkan speed
queueingrequest menunggu giliransering tersembunyi sebagai “stalled”
jittervariasi latencydiabaikan sampai UX terasa random
critical pathdependency yang menentukan selesai paling awaltidak terlihat jika hanya lihat endpoint individu

Engineer kuat tidak berhenti di “endpoint X lambat”.

Ia memetakan di mana waktu hilang.


4. Waterfall: Musuh Utama Aplikasi Data-Heavy

Waterfall terjadi saat operasi yang bisa dimulai lebih awal malah menunggu operasi sebelumnya.

Contoh buruk:

Jika setiap request 300 ms, total bukan 300 ms. Total bisa mendekati 1200 ms sebelum UI berguna.

Waterfall bukan hanya network problem. Ia sering berasal dari desain component.

Contoh:

function Dashboard() {
  const me = useMe();

  if (!me.data) return <Spinner />;

  return <OrgPanel orgId={me.data.orgId} />;
}

function OrgPanel({ orgId }: { orgId: string }) {
  const org = useOrg(orgId);

  if (!org.data) return <Spinner />;

  return <ProjectList orgId={org.data.id} />;
}

function ProjectList({ orgId }: { orgId: string }) {
  const projects = useProjects(orgId);

  if (!projects.data) return <Spinner />;

  return <Tasks projectId={projects.data[0]?.id} />;
}

Component tree membuat data dependency serial.

Pertanyaannya bukan “hook-nya salah?”

Pertanyaannya:

“Apakah dependency ini benar-benar harus serial?”


5. Jenis Waterfall di React

5.1 Render-then-fetch waterfall

Pola:

load JS -> render component -> useEffect runs -> fetch -> render data

Masalahnya: request baru dimulai setelah JavaScript load, parse, execute, dan React render pertama.

Untuk data route-critical, ini sering terlambat.

Alternatif:

  • route loader,
  • server rendering,
  • Server Components,
  • prefetch on link hover/viewport,
  • initial data hydration,
  • query preloading,
  • HTML hints/framework preloads.

5.2 Parent-child data waterfall

Parent fetch dulu, child baru tahu parameter fetch berikutnya.

Kadang legitimate. Misalnya child butuh ID dari response parent.

Tapi sering tidak legitimate karena parameter sebenarnya sudah ada dari URL, session, atau route metadata.

5.3 N+1 UI waterfall

List fetch, lalu setiap item fetch detail sendiri.

Ini biasanya harus diperbaiki di API contract:

  • include projection,
  • batch endpoint,
  • graph query,
  • summary endpoint,
  • server-side aggregation,
  • pagination dengan embedded summary.

5.4 Mutation-refetch waterfall

Mutation selesai, lalu aplikasi invalidate terlalu luas.

PATCH /case/123/status
GET /cases
GET /cases/123
GET /cases/123/audit
GET /dashboard-counts
GET /notifications

Kadang benar. Sering terlalu mahal.

Harus diputuskan:

  • response mutation sudah membawa representation baru?
  • query mana yang benar-benar terdampak?
  • apa yang cukup diupdate lokal?
  • mana yang perlu background refetch?
  • mana yang harus menunggu user membuka panel?

5.5 Bundle-data waterfall

Data cepat, tetapi component code belum ada.

Atau sebaliknya, component code ada, data belum ada.

Route transition modern harus melihat keduanya:

Jika code dan data bisa dimulai bersamaan, jangan buat salah satunya menunggu yang lain.


6. Latency Budget

Tanpa budget, performance tuning menjadi opini.

Buat budget berdasarkan user interaction.

InteractionTarget desainCatatan
keystroke filtering local< 100 ms perceivedjangan network per keystroke tanpa debounce/cache
autocomplete remotecepat dan cancellablestale result tidak boleh menang
route transition utamadata penting seawal mungkinloader/prefetch/cache sangat berpengaruh
background refreshtidak boleh mengganggu UIindikator subtle, no layout reset
form submitfeedback langsungserver ack bisa lebih lama
file uploadprogress + cancelthroughput dan retry chunk penting
export/report asyncack cepat + polling/SSEjangan block UI menunggu report selesai
dashboard initial loadprogressive sectionsjangan satu endpoint lambat menahan semua

Budget tidak harus angka universal. Yang penting adalah urutan prioritas.

Contoh untuk page detail:

0-100 ms   : route transition feedback
100-500 ms : shell/skeleton + cached data if available
500-1500 ms: primary detail visible
1.5s+      : secondary panels progressively load
background: audit/log/recommendation widgets

Ini lebih useful daripada target naif “semua API harus < 200 ms”.


7. Throughput: Saat Banyak Kerja Berjalan Bersamaan

Latency membahas satu operasi. Throughput membahas kapasitas.

Di client-server React, throughput muncul di beberapa tempat:

  • user mengetik cepat dan memicu banyak search request,
  • infinite scroll memuat banyak page,
  • tab terbuka banyak dan semua refetch on focus,
  • dashboard memuat 30 widget,
  • upload banyak file,
  • WebSocket menerima event burst,
  • mutation queue offline disinkronkan saat reconnect,
  • banyak user membuka halaman yang sama setelah deploy.

Throughput yang buruk bisa membuat latency memburuk karena queueing.

Ini loop berbahaya: overload → timeout → retry → overload lebih parah.


8. Concurrency Tidak Selalu Lebih Cepat

Parallel request bisa mengurangi waterfall. Tapi concurrency berlebihan bisa merusak.

Trade-off:

StrategyBenefitRisiko
serialsederhana, dependency jelaslatency total tinggi
full parallelcritical path lebih pendekbandwidth/CPU/server pressure
bounded parallelkontrol resourcebutuh scheduler
batchrequest count rendahpayload besar, coupling, partial failure sulit
streamingfirst useful byte lebih cepatprotocol/UI complexity
cache reusezero network latencystale/consistency risk

Contoh bounded concurrency untuk operasi non-critical:

async function mapWithConcurrency<T, R>(
  items: T[],
  concurrency: number,
  worker: (item: T, index: number) => Promise<R>,
): Promise<R[]> {
  const results: R[] = new Array(items.length);
  let nextIndex = 0;

  async function runWorker() {
    while (nextIndex < items.length) {
      const index = nextIndex;
      nextIndex += 1;
      results[index] = await worker(items[index], index);
    }
  }

  const workers = Array.from(
    { length: Math.min(concurrency, items.length) },
    () => runWorker(),
  );

  await Promise.all(workers);
  return results;
}

Jangan gunakan ini untuk menutupi API N+1 yang seharusnya diperbaiki di contract.

Bounded concurrency adalah mitigasi, bukan alasan mempertahankan desain buruk.


9. Membaca Waterfall DevTools dengan Benar

Waterfall network biasanya punya bagian seperti:

  • queueing/stalled,
  • DNS lookup,
  • initial connection,
  • SSL/TLS,
  • request sent,
  • waiting/TTFB,
  • content download.

Interpretasi:

GejalaKemungkinan
queueing/stalled tinggiterlalu banyak request, priority, connection reuse, browser scheduling
DNS/connection/TLS sering munculorigin tersebar, koneksi tidak reused, cold connection
request sent lamaupload besar, network lambat
waiting/TTFB lamaserver processing, upstream dependency, queue server, cold start
download lamapayload besar, bandwidth rendah, compression buruk
network cepat tapi UI lambatparse/render/main thread/caching issue
banyak request kecil serialcomponent/route waterfall
preflight banyakCORS + custom method/header/content-type

PerformanceResourceTiming bisa membantu mengukur resource timing di browser.

Contoh observer:

type ResourceMetric = {
  name: string;
  initiatorType: string;
  duration: number;
  startTime: number;
  transferSize: number;
  encodedBodySize: number;
  decodedBodySize: number;
};

function observeResourceTiming(onMetric: (metric: ResourceMetric) => void) {
  const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      if (!(entry instanceof PerformanceResourceTiming)) {
        continue;
      }

      onMetric({
        name: entry.name,
        initiatorType: entry.initiatorType,
        duration: entry.duration,
        startTime: entry.startTime,
        transferSize: entry.transferSize,
        encodedBodySize: entry.encodedBodySize,
        decodedBodySize: entry.decodedBodySize,
      });
    }
  });

  observer.observe({ type: "resource", buffered: true });

  return () => observer.disconnect();
}

Catatan penting: untuk cross-origin resources, detail timing tertentu bisa dibatasi kecuali server mengirim header yang mengizinkan timing detail. Jadi jangan heran jika angka tidak lengkap di beberapa origin.


10. Instrumented Fetch untuk App-Level Latency

Resource timing memberi sudut browser. Tapi app tetap perlu mengukur lifecycle sendiri.

Contoh wrapper:

type FetchMetric = {
  requestId: string;
  method: string;
  path: string;
  status?: number;
  ok?: boolean;
  durationMs: number;
  errorKind?: "aborted" | "network" | "timeout" | "http" | "parse";
};

async function measuredFetch(
  request: Request,
  emitMetric: (metric: FetchMetric) => void,
): Promise<Response> {
  const startedAt = performance.now();
  const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID();

  try {
    const response = await fetch(request);

    emitMetric({
      requestId,
      method: request.method,
      path: new URL(request.url).pathname,
      status: response.status,
      ok: response.ok,
      durationMs: performance.now() - startedAt,
    });

    return response;
  } catch (error) {
    const errorKind =
      error instanceof DOMException && error.name === "AbortError"
        ? "aborted"
        : "network";

    emitMetric({
      requestId,
      method: request.method,
      path: new URL(request.url).pathname,
      durationMs: performance.now() - startedAt,
      errorKind,
    });

    throw error;
  }
}

App-level metric menjawab:

  • endpoint mana paling sering gagal,
  • endpoint mana paling lambat di real user,
  • request mana paling sering aborted,
  • route mana memicu request terbanyak,
  • status mana paling umum,
  • mutation mana sering conflict,
  • apakah deploy baru menaikkan latency.

Jangan hanya mengandalkan local DevTools.

Real user punya device, network, geography, extension, memory pressure, dan tab behavior yang berbeda.


11. Teknik Mengurangi Latency: Urutan yang Benar

Optimasi paling efektif biasanya bukan micro-optimization.

Urutan berpikir:

11.1 Remove request

Request tercepat adalah request yang tidak dikirim.

Contoh:

  • resource sudah fresh di cache,
  • data bisa dihitung dari response sebelumnya,
  • route tidak membutuhkan panel sekunder sampai dibuka,
  • keystroke bisa difilter lokal dulu,
  • duplicate in-flight request bisa didedup.

11.2 Start earlier

Request yang sama bisa terasa lebih cepat jika dimulai sebelum user menunggu.

  • prefetch saat link terlihat,
  • prefetch saat hover/focus,
  • load route data during navigation,
  • server render route-critical data,
  • warm query cache setelah login,
  • speculative load berdasarkan strong signal.

11.3 Parallelize

Request independen jangan dibuat serial.

const [caseDetail, auditSummary, workflow] = await Promise.all([
  fetchCaseDetail(caseId),
  fetchAuditSummary(caseId),
  fetchWorkflowState(caseId),
]);

Tetapi jangan paralelkan request yang tidak dibutuhkan untuk first useful render.

11.4 Shrink

Payload besar membayar biaya dua kali:

  1. transfer,
  2. parse/render.

Strategi:

  • pagination,
  • projection fields,
  • summary endpoint,
  • compression,
  • avoid huge nested arrays,
  • use cursor not giant offset lists,
  • avoid returning full domain aggregate for small UI update,
  • send server-computed view model when appropriate.

11.5 Stream or defer

Jika data besar atau beberapa bagian lambat, jangan tahan seluruh page.

  • stream HTML/RSC payload,
  • defer non-critical panels,
  • use Suspense boundaries,
  • load secondary data lazily,
  • show progressive sections.

12. Prefetch: Powerful tetapi Berbahaya

Prefetch adalah memulai request sebelum user benar-benar butuh.

Bagus jika signal kuat:

  • user hover link,
  • link masuk viewport,
  • route berikutnya sangat mungkin,
  • wizard step berikutnya deterministik,
  • search result detail kemungkinan dibuka,
  • dashboard tab default hampir selalu dibuka.

Buruk jika terlalu agresif:

  • menghabiskan bandwidth,
  • mencemari cache,
  • membuat server load naik,
  • mengganggu request penting,
  • memuat data sensitif sebelum diperlukan,
  • melanggar ekspektasi access/audit bila endpoint punya side effect buruk.

Prefetch harus punya policy:

type PrefetchPolicy = {
  enabled: boolean;
  minConfidence: number;
  maxAgeMs: number;
  networkAware: boolean;
  requiresUserIntent: boolean;
};

Rule of thumb:

  • prefetch read-only resource,
  • jangan prefetch mutation,
  • hormati auth/permission boundary,
  • batalkan prefetch non-critical bila user melakukan action penting,
  • beri stale time yang jelas,
  • jangan prefetch semua link dalam halaman besar.

13. Cache: Latency Optimization dengan Consistency Cost

Cache mengurangi latency karena menghindari network atau mengizinkan stale-while-refresh.

Tapi cache membawa cost:

  • data bisa stale,
  • invalidation sulit,
  • user bisa melihat representation lama,
  • permission berubah tapi cache belum bersih,
  • mutation impact tidak selalu diketahui,
  • tab lain bisa berbeda.

Model cache sehat:

Pertanyaan sebelum cache:

  1. Berapa lama data boleh stale?
  2. Apa event yang membuat data invalid?
  3. Apakah mutation response cukup untuk update cache?
  4. Apakah data user-specific?
  5. Apakah data permission-sensitive?
  6. Apakah cache harus survive reload/tab?
  7. Apa yang terjadi saat background refetch gagal?

Cache bukan hanya performance tool. Cache adalah consistency protocol.


14. Batching vs Overfetching vs Underfetching

Tiga masalah sering tertukar.

Underfetching

Client harus call banyak endpoint untuk satu screen.

GET /case/123
GET /case/123/assignee
GET /case/123/audit-summary
GET /case/123/sla
GET /case/123/tags

Overfetching

Client menerima field terlalu banyak.

{
  "id": "case-123",
  "title": "...",
  "fullAuditLog": ["... thousands ..."],
  "allAttachments": ["..."],
  "internalNotes": ["..."],
  "hugeNestedDomainAggregate": {}
}

Bad batching

Client menggabungkan semua agar request count rendah, tetapi membuat payload lambat dan coupling tinggi.

Better design:

  • primary view model untuk above-the-fold,
  • secondary panels lazy/deferred,
  • batch endpoint untuk repeated small resources,
  • projection parameter bila contract stabil,
  • GraphQL/fragments jika domain cocok,
  • route-specific BFF jika banyak composition di client.

Contoh route view model:

type CaseDetailPageModel = {
  case: {
    id: string;
    title: string;
    status: string;
    priority: string;
    assigneeName: string;
  };
  sla: {
    state: "OK" | "WARNING" | "BREACHED";
    dueAt: string;
  };
  permissions: {
    canEditPriority: boolean;
    canSubmit: boolean;
  };
};

Ini bukan berarti semua endpoint harus BFF. Artinya route-critical composition tidak selalu harus terjadi di browser.


15. CORS Preflight sebagai Latency Tax

Cross-origin request tertentu memicu preflight: browser mengirim OPTIONS untuk mengecek apakah actual request diizinkan.

Preflight bisa benar dan perlu. Tapi ia juga bisa menjadi latency tax jika terjadi tanpa sadar.

Pemicu umum:

  • method selain simple method tertentu,
  • custom headers,
  • content type tertentu,
  • credentialed cross-origin request dengan policy spesifik.

Dari sisi React engineer:

  • jangan tambah custom header tanpa alasan,
  • pahami bahwa Authorization atau X-* header bisa mengubah CORS behavior,
  • gunakan same-origin API/BFF jika cocok,
  • cache preflight di server bila aman,
  • jangan debugging hanya actual request; lihat OPTIONS juga.

Waterfall dengan preflight:

Jika endpoint kecil tapi setiap call butuh preflight, latency relatif bisa naik signifikan.


16. HTTP/2 dan HTTP/3 Tidak Menghapus Waterfall Aplikasi

Transport modern membantu multiplexing dan connection behavior.

Tetapi transport tidak menghapus dependency yang dibuat aplikasi.

Jika code kamu melakukan:

const user = await fetchUser();
const org = await fetchOrg(user.orgId);
const projects = await fetchProjects(org.id);

HTTP/2 atau HTTP/3 tidak bisa membuat fetchProjects mulai sebelum org.id diketahui.

Transport bisa membantu request independen berjalan lebih efisien, tetapi dependency graph tetap ditentukan aplikasi.

Jadi optimasi utama tetap:

  • perbaiki data dependency,
  • mulai request lebih awal,
  • paralelkan yang independen,
  • ubah contract bila terlalu banyak round trip,
  • gunakan cache/stale data saat aman.

17. React-Specific Critical Path

Untuk React app, critical path sering terlihat seperti ini:

Jika data route-critical hanya dimulai di useEffect, ia berada setelah banyak pekerjaan.

Alternatif route-data critical path:

Alternatif server-rendered/streaming path:

Tidak ada satu path terbaik untuk semua kasus.

Gunakan trade-off:

ModelKuat untukRisiko
client fetch in effectsimple local datarender-then-fetch waterfall
query prefetchroute transition cepatcache complexity
route loadernavigation-bound dataroute coupling
SSRfirst paint/contentserver load/hydration complexity
RSCserver-side data access + less client JSframework/runtime boundary
streamingprogressive contentUI/protocol complexity

18. Skeleton, Spinner, dan Perceived Latency

Performance tidak hanya angka. Perceived latency penting.

Tetapi jangan salah gunakan skeleton.

Skeleton bagus jika:

  • layout sudah cukup pasti,
  • data akan datang relatif cepat,
  • user perlu tahu area sedang dimuat,
  • tidak menyebabkan layout shift besar.

Skeleton buruk jika:

  • ditampilkan terlalu lama,
  • seluruh page skeleton padahal cached data tersedia,
  • skeleton menggantikan data lama saat background refetch,
  • skeleton menyembunyikan error/conflict,
  • skeleton dipakai untuk menutupi waterfall buruk.

Better states:

SituationUI
no data yet, first loadskeleton
cached data staleshow cached data + refresh indicator
background refetchsubtle status, no reset
slow secondary panelsection-level skeleton
mutation pendingbutton-level pending / optimistic row state
offlineoffline banner + queued indicator

Jangan membuat user membayar latency dua kali: menunggu data lalu kehilangan konteks karena UI reset.


19. Case Study — Regulatory Case Dashboard

Misal dashboard punya:

  • open case count,
  • high priority list,
  • SLA breach list,
  • assigned-to-me list,
  • recent audit events,
  • notification summary,
  • chart by status,
  • search box.

Naive design:

Masalah:

  • terlalu banyak serial dependency,
  • semua widget dianggap sama penting,
  • tidak ada route-critical distinction,
  • tidak ada stale cache,
  • satu widget lambat bisa menahan whole dashboard bila state digabung,
  • background refresh bisa reset semua.

Better design:

Possible endpoint contract:

type DashboardCriticalModel = {
  counts: {
    open: number;
    highPriority: number;
    slaBreached: number;
  };
  myCases: Array<{
    id: string;
    title: string;
    status: string;
    priority: string;
    updatedAt: string;
  }>;
  slaBreaches: Array<{
    id: string;
    title: string;
    dueAt: string;
    assigneeName: string;
  }>;
};

Secondary widgets bisa lazy:

function DashboardPage() {
  const critical = useDashboardCriticalQuery();

  return (
    <DashboardShell>
      <CriticalDashboard data={critical.data} />
      <DeferredPanel title="Recent audit events">
        <RecentAuditEvents />
      </DeferredPanel>
      <DeferredPanel title="Case status chart">
        <CaseStatusChart />
      </DeferredPanel>
    </DashboardShell>
  );
}

Intinya bukan “buat satu endpoint besar”.

Intinya: pisahkan data yang menentukan first useful page dari data yang bisa menyusul.


20. Search Box: Latency, Cancellation, dan Throughput

Remote search adalah contoh kecil yang memuat banyak prinsip.

Naive:

useEffect(() => {
  fetch(`/api/search?q=${term}`)
    .then((r) => r.json())
    .then(setResults);
}, [term]);

Masalah:

  • request per keystroke,
  • tidak ada debounce,
  • tidak ada abort,
  • stale response bisa menang,
  • server terkena burst,
  • tidak ada minimum query length,
  • tidak ada cache untuk term yang sama,
  • loading flicker.

Better design:

function useDebouncedValue<T>(value: T, delayMs: number): T {
  const [debounced, setDebounced] = React.useState(value);

  React.useEffect(() => {
    const id = window.setTimeout(() => setDebounced(value), delayMs);
    return () => window.clearTimeout(id);
  }, [value, delayMs]);

  return debounced;
}

function SearchBox() {
  const [term, setTerm] = React.useState("");
  const debouncedTerm = useDebouncedValue(term.trim(), 250);
  const [results, setResults] = React.useState<string[]>([]);
  const [loading, setLoading] = React.useState(false);

  React.useEffect(() => {
    if (debouncedTerm.length < 3) {
      setResults([]);
      return;
    }

    const controller = new AbortController();

    async function run() {
      setLoading(true);

      try {
        const response = await fetch(
          `/api/search?q=${encodeURIComponent(debouncedTerm)}`,
          { signal: controller.signal },
        );

        if (!response.ok) {
          throw new Error(`Search failed: ${response.status}`);
        }

        setResults((await response.json()) as string[]);
      } catch (error) {
        if (error instanceof DOMException && error.name === "AbortError") {
          return;
        }

        throw error;
      } finally {
        if (!controller.signal.aborted) {
          setLoading(false);
        }
      }
    }

    void run();

    return () => controller.abort();
  }, [debouncedTerm]);

  return (
    <div>
      <input value={term} onChange={(event) => setTerm(event.target.value)} />
      {loading ? <span>Searching...</span> : null}
      <ul>{results.map((item) => <li key={item}>{item}</li>)}</ul>
    </div>
  );
}

Production version sebaiknya memakai query cache, request dedup, stale result guard, dan metric.


21. Mutation Performance: Jangan Selalu Refetch Everything

Setelah mutation, ada beberapa strategi:

StrategiCocok untukRisiko
update cache from responseresponse membawa representation finalharus tahu query terdampak
invalidate specific queriesimpact jelas tapi butuh server truthnetwork tambahan
invalidate broad scopecorrectness lebih mudahrefetch storm
optimistic updateUX cepatrollback/conflict complexity
background reconciliationuser tetap lanjutstale sementara

Contoh mutation update priority:

type UpdatePriorityResult = {
  case: {
    id: string;
    priority: "LOW" | "MEDIUM" | "HIGH";
    version: number;
    updatedAt: string;
  };
};

Jika response sudah membawa case representation baru, client bisa:

  • update detail cache case:123,
  • update item di list cache yang sedang visible,
  • invalidate aggregate counts jika priority mempengaruhi count,
  • background refetch audit panel.

Jangan otomatis:

invalidateQueries(); // semua query di seluruh app

Itu benar hanya sebagai fallback kasar, bukan default engineering.


22. Payload Size dan Parse Cost

Payload kecil bukan hanya menghemat bandwidth. Ia juga menghemat parse, memory, GC, dan render.

Gejala payload terlalu besar:

  • download lama di network tab,
  • response.json() terasa berat,
  • main thread freeze setelah response,
  • list render lambat,
  • mobile device jauh lebih lambat dari laptop,
  • memory naik drastis,
  • DevTools menunjukkan JSON besar tapi UI hanya memakai sedikit field.

Checklist payload:

  • apakah response mengirim field yang tidak dipakai?
  • apakah nested array terlalu dalam?
  • apakah pagination benar-benar diterapkan?
  • apakah ada duplicate object besar?
  • apakah date/string format terlalu verbose?
  • apakah attachment/base64 ikut dalam JSON?
  • apakah server mengirim audit log penuh padahal UI hanya butuh count?
  • apakah compression aktif untuk textual payload?

Anti-pattern:

{
  "case": {
    "id": "123",
    "title": "...",
    "auditLog": ["10000 items"],
    "attachments": [{ "base64": "..." }],
    "fullWorkflowHistory": ["..."],
    "allPermissionsForAllUsers": ["..."]
  }
}

Better:

{
  "case": {
    "id": "123",
    "title": "...",
    "status": "OPEN",
    "auditSummary": {
      "totalEvents": 183,
      "lastEventAt": "2026-07-07T04:30:00Z"
    },
    "attachmentSummary": {
      "total": 12
    }
  }
}

Load detail panel saat user membukanya.


23. Request Priority dan Criticality

Tidak semua request sama penting.

Classify:

ClassContohPolicy
critical navigationroute model, auth/session shellstart early, avoid blocking by secondary
interaction responsesave button, search resultdeadline/cancel/retry carefully
background freshnessrefetch stale datalow disruption
speculativeprefetch likely routeabort/drop under pressure
analyticsRUM/loggingsend later/batch/beacon if suitable
secondary panelaudit widget, chartlazy/defer

Dalam code, jangan semua request memakai policy sama.

type RequestCriticality = "critical" | "interactive" | "background" | "speculative";

type NetworkPolicy = {
  timeoutMs: number;
  retry: boolean;
  canPrefetch: boolean;
  showGlobalLoading: boolean;
};

const policies: Record<RequestCriticality, NetworkPolicy> = {
  critical: {
    timeoutMs: 5000,
    retry: true,
    canPrefetch: true,
    showGlobalLoading: true,
  },
  interactive: {
    timeoutMs: 3000,
    retry: false,
    canPrefetch: false,
    showGlobalLoading: false,
  },
  background: {
    timeoutMs: 8000,
    retry: true,
    canPrefetch: false,
    showGlobalLoading: false,
  },
  speculative: {
    timeoutMs: 2000,
    retry: false,
    canPrefetch: true,
    showGlobalLoading: false,
  },
};

Nilai angka di atas bukan universal. Yang penting adalah request class punya kebijakan berbeda.


24. Backpressure dari Client

Client juga harus sopan terhadap server.

Backpressure berarti client tidak terus mengirim kerja saat sistem tidak siap.

Contoh:

  • disable submit saat mutation pending,
  • debounce search,
  • cancel obsolete request,
  • stop polling saat tab hidden bila tidak critical,
  • exponential backoff untuk retry,
  • respect Retry-After,
  • cap parallel uploads,
  • pause sync queue saat offline/poor network,
  • avoid retry storm after outage.

Bad retry:

while (true) {
  try {
    return await fetchData();
  } catch {
    // immediately retry forever
  }
}

Better conceptual retry:

function sleep(ms: number) {
  return new Promise((resolve) => window.setTimeout(resolve, ms));
}

async function retryWithBackoff<T>(
  operation: () => Promise<T>,
  options: { attempts: number; baseDelayMs: number; signal?: AbortSignal },
): Promise<T> {
  let lastError: unknown;

  for (let attempt = 1; attempt <= options.attempts; attempt += 1) {
    if (options.signal?.aborted) {
      throw options.signal.reason;
    }

    try {
      return await operation();
    } catch (error) {
      lastError = error;

      if (attempt === options.attempts) {
        break;
      }

      const jitter = Math.random() * options.baseDelayMs;
      const delay = options.baseDelayMs * 2 ** (attempt - 1) + jitter;
      await sleep(delay);
    }
  }

  throw lastError;
}

Retry akan dibahas lebih dalam di Part 014.


25. Polling dan Throughput Cost

Polling terlihat sederhana:

setInterval(fetchNotifications, 5000);

Tapi pada skala besar:

100,000 active clients / 5 seconds = 20,000 requests per second

Polling harus punya policy:

  • interval adaptif,
  • pause saat tab hidden bila aman,
  • backoff saat error,
  • jitter agar tidak semua client serentak,
  • stop saat user logout,
  • switch ke SSE/WebSocket jika use case realtime dan scale cocok,
  • poll lightweight endpoint, bukan full payload besar.

Jitter contoh:

function withJitter(baseMs: number, ratio = 0.2): number {
  const spread = baseMs * ratio;
  return baseMs - spread / 2 + Math.random() * spread;
}

Tanpa jitter, client bisa membuat thundering herd setiap interval.


26. Waterfall Review Checklist

Saat melihat route lambat, jangan langsung rewrite.

Gunakan checklist:

26.1 Request count

  • Berapa request terjadi untuk satu route transition?
  • Berapa yang critical?
  • Berapa yang secondary?
  • Berapa yang duplicate?
  • Berapa yang speculative?

26.2 Dependency graph

  • Request mana yang benar-benar bergantung pada response sebelumnya?
  • Dependency mana yang hanya akibat component nesting?
  • Parameter mana sudah tersedia dari URL/session?
  • Bisa loader/server compose lebih awal?

26.3 Timing phase

  • Waktu hilang di queueing, DNS, connection, TTFB, download, parse, atau render?
  • Apakah cross-origin timing visible?
  • Apakah ada preflight?

26.4 Payload

  • Berapa transfer size?
  • Berapa decoded size?
  • Apakah response overfetch?
  • Apakah pagination/projection ada?

26.5 Cache

  • Apakah data fresh sudah tersedia?
  • Mengapa request tetap dikirim?
  • Apakah stale time terlalu pendek?
  • Apakah invalidation terlalu luas?

26.6 UX

  • Apakah user melihat blank page?
  • Apakah data lama bisa dipakai sementara?
  • Apakah secondary panel boleh menyusul?
  • Apakah loading state terlalu kasar?

27. Pattern: Route Critical Data First

Untuk route data-heavy, pecah menjadi:

  1. critical model,
  2. secondary widgets,
  3. background freshness,
  4. speculative next navigation.

Contoh:

type CasePageCritical = {
  case: {
    id: string;
    title: string;
    status: string;
    priority: string;
  };
  permissions: {
    canEdit: boolean;
    canEscalate: boolean;
  };
  sla: {
    state: string;
    dueAt: string;
  };
};

Secondary:

  • full audit log,
  • attachment list,
  • related cases,
  • comments,
  • recommendation engine output.

Jangan tahan title/status/permission hanya karena recommendation panel lambat.


28. Pattern: Parallel Route Loader

Jika memakai route-level loading, jangan serialkan hal independen.

Bad:

export async function loader({ params }: LoaderArgs) {
  const detail = await getCaseDetail(params.caseId);
  const audit = await getAuditSummary(params.caseId);
  const workflow = await getWorkflowState(params.caseId);

  return { detail, audit, workflow };
}

Better:

export async function loader({ params }: LoaderArgs) {
  const caseId = requireCaseId(params);

  const [detail, audit, workflow] = await Promise.all([
    getCaseDetail(caseId),
    getAuditSummary(caseId),
    getWorkflowState(caseId),
  ]);

  return { detail, audit, workflow };
}

Even better jika audit/workflow tidak critical:

export async function loader({ params }: LoaderArgs) {
  const caseId = requireCaseId(params);

  return {
    detail: await getCaseDetail(caseId),
    auditPromise: getAuditSummary(caseId),
    workflowPromise: getWorkflowState(caseId),
  };
}

Framework detail berbeda-beda, tapi prinsipnya sama: jangan block critical render oleh data sekunder.


29. Pattern: Request Coalescing

Jika banyak component meminta resource yang sama, request harus digabung.

Bad:

CaseHeader -> GET /cases/123
CaseSidebar -> GET /cases/123
CaseActions -> GET /cases/123

Better:

  • query cache dengan query key sama,
  • route loader menyediakan data,
  • parent boundary fetch sekali,
  • request dedup in fetch client.

Contoh dedup sederhana:

class InFlightDeduper {
  private readonly inFlight = new Map<string, Promise<unknown>>();

  run<T>(key: string, operation: () => Promise<T>): Promise<T> {
    const existing = this.inFlight.get(key);

    if (existing) {
      return existing as Promise<T>;
    }

    const promise = operation().finally(() => {
      this.inFlight.delete(key);
    });

    this.inFlight.set(key, promise);
    return promise;
  }
}

Dedup hanya aman jika request truly equivalent:

  • same method,
  • same URL,
  • same query,
  • same auth context,
  • same headers that affect representation,
  • same body untuk mutation sangat hati-hati.

Untuk mutation, dedup berbeda dari idempotency.


30. Pattern: Stale-While-Revalidate UI

Saat cache punya data lama, jangan selalu kosongkan UI.

UX benefit:

  • user langsung melihat konteks,
  • layout stabil,
  • background refresh tidak mengganggu,
  • perceived latency turun drastis.

Risiko:

  • user bisa melihat data lama,
  • action buttons mungkin tidak valid lagi,
  • permission-sensitive data harus hati-hati,
  • mutation perlu version/conflict handling.

Jadi stale-while-revalidate cocok untuk read model yang tolerate stale. Tidak cocok untuk semua boundary.


31. Pattern: Progressive Panels

Untuk halaman kompleks, gunakan section-level lifecycle.

function CaseDetailPage() {
  return (
    <CasePageLayout>
      <CaseHeader />
      <CaseMainFacts />
      <CaseActions />

      <ProgressiveSection title="Audit trail">
        <AuditTrail />
      </ProgressiveSection>

      <ProgressiveSection title="Related cases">
        <RelatedCases />
      </ProgressiveSection>
    </CasePageLayout>
  );
}

Setiap section punya:

  • data requirement,
  • loading state,
  • error state,
  • retry behavior,
  • cache key,
  • observability.

Jangan membuat satu global isLoading untuk seluruh page jika data punya criticality berbeda.


32. Performance Failure Modes

Failure modeGejalaSolusi struktural
render-then-fetchblank/skeleton lamaloader/prefetch/server render
parent-child waterfallrequest serialhoist data requirement / route compose
N+1 item requestbanyak request kecilbatch/projection/summary endpoint
overfetchdownload/parse besarprojection/pagination/view model
broad invalidationrefetch stormtargeted invalidation/update cache
retry stormoutage makin burukbackoff/jitter/respect retry-after
prefetch overloadbandwidth/server naikconfidence-based prefetch
polling herdtraffic spike berkalajitter/adaptive interval/SSE
stale overwriteUI balik ke data lamacancellation/query identity
hidden parse costnetwork cepat UI freezeshrink payload / worker / pagination
secondary blockingpage menunggu widgetprogressive panels/defer
duplicate requestsame resource fetched repeatedlycache/dedup/coalescing

33. Observability Dimensions

Untuk setiap route/interaction penting, ukur:

MetricArti
request count per routeapakah route terlalu chatty
critical request durationrequest yang menentukan first useful render
total route transition timeperceived navigation cost
abort raterequest obsolete/navigation/search churn
retry rateinstability atau policy terlalu agresif
status distribution4xx/5xx/429/conflict behavior
payload sizetransfer/parse risk
cache hit rateefektivitas stale/fresh policy
refetch after mutation countinvalidation cost
time to data visiblenetwork + parse + render
background refetch failure ratestale data risk

Metric tanpa dimension kurang berguna.

Tambahkan dimension seperti:

  • route id,
  • endpoint group,
  • request criticality,
  • network type bila tersedia,
  • device class bila tersedia,
  • app version,
  • feature flag,
  • status code,
  • cache hit/miss,
  • aborted vs failed.

34. A Practical Waterfall Review Example

Misal DevTools menunjukkan:

GET /api/session               220 ms
GET /api/me                    180 ms
GET /api/cases?assignedTo=42   420 ms
GET /api/cases/counts          300 ms
GET /api/permissions           250 ms
GET /api/notifications         500 ms
GET /api/audit/recent          900 ms

Naive conclusion:

audit endpoint lambat.

Better analysis:

  1. Apakah audit critical untuk first render?
  2. Apakah /session, /me, dan /permissions bisa digabung atau dihydrate?
  3. Apakah /cases benar-benar harus menunggu /me, atau user id sudah ada di session model?
  4. Apakah counts dan cases bisa paralel?
  5. Apakah notifications perlu route-blocking?
  6. Apakah audit bisa section-level deferred?
  7. Apakah /permissions harus separate endpoint atau bagian dari route view model?

Potential redesign:

GET /api/dashboard/critical    450 ms
GET /api/notifications         background
GET /api/audit/recent          deferred panel

Atau:

loader parallel:
  session+permissions hydrated/server-side
  cases + counts parallel
  audit deferred

Performance work sering berarti mengubah dependency graph, bukan mengoptimalkan satu endpoint.


35. Engineering Invariants

Simpan invariant berikut:

  1. Latency bukan satu angka. Pisahkan queueing, connection, TTFB, download, parse, render, dan paint.
  2. Waterfall adalah dependency bug sampai terbukti sebaliknya. Banyak serial request berasal dari struktur UI, bukan kebutuhan domain.
  3. Request tercepat adalah request yang tidak dikirim. Cache, dedup, dan local computation adalah optimasi besar.
  4. Mulai lebih awal sering lebih efektif daripada membuat endpoint sedikit lebih cepat. Prefetch/loader/server render bisa mengubah critical path.
  5. Parallelism punya batas. Terlalu banyak concurrency membuat queueing, contention, dan retry storm.
  6. Payload besar membayar biaya transfer dan CPU. Jangan ukur network saja.
  7. Cache mengurangi latency dengan biaya consistency. Jangan pakai cache tanpa invalidation model.
  8. Prefetch harus berbasis confidence. Aggressive prefetch bisa merusak throughput.
  9. Mutation performance bukan selalu refetch everything. Pilih update, invalidate targeted, atau background reconcile.
  10. Observability harus mengukur perceived lifecycle, bukan hanya server duration. User menunggu sampai data terlihat.

36. Kesimpulan

React client-server performance adalah permainan pipeline.

Kamu harus tahu:

  • request mana yang critical,
  • request mana yang bisa ditunda,
  • request mana yang bisa dihapus,
  • request mana yang bisa diparalelkan,
  • data mana yang bisa stale,
  • payload mana yang terlalu besar,
  • mutation mana yang menyebabkan refetch storm,
  • prefetch mana yang berguna dan mana yang boros,
  • error/retry mana yang memperburuk throughput,
  • metric mana yang membuktikan semua itu di real user.

Jangan hanya bertanya:

“Endpoint mana yang lambat?”

Tanyakan:

“Apa critical path user, dan keputusan client-server mana yang membuatnya panjang?”

Di Part 007, kita akan membahas client-server ownership model: siapa pemilik data, siapa pemilik state, siapa pemilik validation, siapa pemilik side effect, dan bagaimana menentukan boundary agar aplikasi React tidak berubah menjadi salinan rapuh dari backend.


Referensi Resmi

Lesson Recap

You just completed lesson 06 in start here. 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.