Asynchronous Content Loading #

Asynchronous content loading adalah teknik memuat konten secara bertahap — tidak semua sekaligus saat halaman pertama dibuka, tapi secara dinamis saat dibutuhkan. Ini adalah salah satu teknik yang paling berpengaruh pada performa dan UX aplikasi web modern: halaman bisa tampil dalam hitungan milidetik dengan shell-nya, sementara konten yang lebih berat dimuat di latar belakang. Tapi teknik ini juga punya jebakan tersendiri: waterfall request yang tidak perlu, fetch yang tidak di-cancel saat komponen unmount, scroll event tanpa throttle yang membebani browser, dan infinite loop request saat error tidak ditangani dengan benar. Artikel ini membahas pola-pola yang benar dan salah, dari parallel fetch sampai Intersection Observer, dari Optimistic UI sampai WebSocket.

Evolusi Asynchronous Loading #

Memahami sejarahnya membantu memahami mengapa tools modern didesain seperti sekarang.

AJAX (2005) — XMLHttpRequest:
  var xhr = new XMLHttpRequest()
  xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status == 200) {
      document.getElementById('content').innerHTML = xhr.responseText
    }
  }
  xhr.open('GET', '/api/data', true)
  xhr.send()
  → Bekerja, tapi verbose dan tidak elegant untuk error handling

fetch API (2015) — Promise-based:
  fetch('/api/data')
    .then(res => res.json())
    .then(data => updateDOM(data))
    .catch(err => handleError(err))
  → Lebih bersih, tapi Promise chaining masih bisa berbelit

async/await (2017) — Syntactic sugar di atas Promise:
  async function loadData() {
    try {
      const res = await fetch('/api/data')
      const data = await res.json()
      updateDOM(data)
    } catch (err) {
      handleError(err)
    }
  }
  → Kode terasa sequential tapi tetap asynchronous

React Query / SWR (2019+) — Data fetching library:
  const { data, isLoading, error } = useQuery(['data'], fetchData)
  → Caching, deduplication, background refetch, error handling otomatis
  → State management untuk server data sudah termasuk

Pola Waterfall vs Parallel Request #

Waterfall adalah salah satu penyebab terbesar page load yang lambat — setiap request menunggu yang sebelumnya selesai, padahal bisa dijalankan bersamaan.

sequenceDiagram
    participant B as Browser
    participant API as API Server

    Note over B,API: WATERFALL — Sequential (lambat!)
    B->>API: GET /api/user
    API-->>B: user data (300ms)
    B->>API: GET /api/orders (menunggu user selesai!)
    API-->>B: orders data (250ms)
    B->>API: GET /api/recommendations (menunggu orders selesai!)
    API-->>B: recommendations (400ms)
    Note over B: Total: 950ms

    Note over B,API: PARALLEL — Concurrent (cepat!)
    B->>API: GET /api/user
    B->>API: GET /api/orders (tidak menunggu!)
    B->>API: GET /api/recommendations (tidak menunggu!)
    API-->>B: user data (300ms)
    API-->>B: orders data (250ms)
    API-->>B: recommendations (400ms)
    Note over B: Total: 400ms (waktu request terlama)
// ANTI-PATTERN: Waterfall request — await berurutan
func loadDashboard() error {
    user, err := fetchUser()            // 300ms
    if err != nil {
        return err
    }
    orders, err := fetchOrders()        // 250ms (mulai setelah user selesai)
    if err != nil {
        return err
    }
    reco, err := fetchRecommendations() // 400ms (mulai setelah orders selesai)
    if err != nil {
        return err
    }
    // Total: 950ms
    return renderDashboard(user, orders, reco)
}

// BENAR: Parallel request — goroutine + WaitGroup
func loadDashboard() error {
    type payload struct {
        user, orders, reco any
    }
    var (
        wg  sync.WaitGroup
        res payload
    )
    wg.Add(3)
    go func() { defer wg.Done(); v, err := fetchUser(); if err == nil { res.user = v } }()            // semua dimulai bersamaan
    go func() { defer wg.Done(); v, err := fetchOrders(); if err == nil { res.orders = v } }()        // semua dimulai bersamaan
    go func() { defer wg.Done(); v, err := fetchRecommendations(); if err == nil { res.reco = v } }() // semua dimulai bersamaan
    wg.Wait()
    // Total: ~400ms (hanya menunggu yang terlama)
    return renderDashboard(res.user, res.orders, res.reco)
}

// LEBIH BAIK: padanan allSettled — tidak gagal karena satu request gagal
func loadDashboard() error {
    type result struct {
        value any
        ok    bool
    }
    results := make([]result, 3)

    var wg sync.WaitGroup
    wg.Add(3)
    go func() { defer wg.Done(); v, err := fetchUser(); results[0] = result{v, err == nil} }()
    go func() { defer wg.Done(); v, err := fetchOrders(); results[1] = result{v, err == nil} }()
    go func() { defer wg.Done(); v, err := fetchRecommendations(); results[2] = result{v, err == nil} }()
    wg.Wait()

    user := results[0]
    orders := results[1]
    reco := results[2]

    return renderDashboard(Payload{
        user:   user.value,
        orders: orDefault(orders),
        reco:   orDefault(reco),
        // Render dengan data yang tersedia, meski sebagian gagal
    })
}

// Helper: kembalikan value saat ok, selain itu default yang masuk akal
func orDefault(r result) any {
    if !r.ok {
        return []any{}
    }
    return r.value
}

Intersection Observer — Lazy Load yang Efisien #

Intersection Observer adalah API browser yang memungkinkan kamu mendeteksi kapan elemen masuk ke viewport — tanpa scroll event yang mahal.

// ANTI-PATTERN: Scroll event untuk lazy load (sangat boros)
func onScroll() {
    for _, el := range document.QuerySelectorAll("[data-lazy]") {
        rect := el.GetBoundingClientRect()
        if rect.Top < window.InnerHeight() {
            loadContent(el) // getBoundingClientRect dipanggil SETIAP SCROLL EVENT!
        }
    }
}
window.AddEventListener("scroll", onScroll)
// Setiap scroll event = layout recalculation = jank!

// BENAR: Intersection Observer — browser yang handle deteksinya
var observer *IntersectionObserver
observer = newIntersectionObserver(func(entries []IntersectionEntry) {
    for _, entry := range entries {
        if entry.IsIntersecting {
            loadContent(entry.Target)
            observer.Unobserve(entry.Target) // berhenti observasi setelah dimuat
        }
    }
}, ObserverOptions{
    RootMargin: "200px", // mulai load 200px sebelum masuk viewport
    Threshold:  0,       // trigger saat pixel pertama masuk viewport
})

// Attach ke semua elemen yang perlu lazy load
for _, el := range document.QuerySelectorAll("[data-lazy]") {
    observer.Observe(el)
}
// React Hook untuk lazy loading dengan Intersection Observer
// (React hooks adalah framework API — ini menunjukkan logika browser yang setara)

type LazyLoad struct {
    ref       *Element
    isVisible bool
}

func UseLazyLoad(options ObserverOptions) (*LazyLoad, func()) {
    ll := &LazyLoad{ref: &Element{}}

    var observer *IntersectionObserver
    observer = newIntersectionObserver(func(entries []IntersectionEntry) {
        for _, entry := range entries {
            if entry.IsIntersecting {
                ll.isVisible = true
                observer.Unobserve(ll.ref) // hanya perlu trigger sekali
            }
        }
    }, options)

    observer.Observe(ll.ref)
    // Cleanup: berhenti observasi saat komponen unmount
    return ll, func() { observer.Unobserve(ll.ref) }
}

// Penggunaan:
func ProductComments(productID string) *Element {
    ll, _ := UseLazyLoad(ObserverOptions{RootMargin: "200px"})

    if ll.isVisible {
        return renderCommentsSection(productID)
    }
    return renderCommentsSkeleton()
}
// Comments hanya di-fetch saat user scroll mendekati section ini

Infinite Scroll vs Pagination — Pilihan yang Tepat #

Infinite scroll dan pagination adalah dua pendekatan berbeda untuk memuat data dalam jumlah besar secara inkremental. Keduanya cocok untuk use case yang berbeda.

flowchart LR
    subgraph Pagination["Pagination — Load on Click"]
        P1["Halaman 1 (20 item)"]
        P2["[Klik tombol 'Halaman 2']"]
        P3["Halaman 2 (20 item baru)"]
        P1 --> P2 --> P3
    end

    subgraph InfScroll["Infinite Scroll — Load on Scroll"]
        I1["20 item pertama"]
        I2["User scroll ke bawah..."]
        I3["Fetch 20 item berikutnya"]
        I4["User scroll lagi..."]
        I5["Fetch 20 item berikutnya"]
        I1 --> I2 --> I3 --> I4 --> I5
    end
// Implementasi Infinite Scroll dengan Intersection Observer
// (state dan hooks React ditampilkan sebagai struct state biasa — padanan framework)
type InfiniteProductListState struct {
    products  []Product
    page      int
    hasMore   bool
    isLoading bool
    loadMore  *Element // sentinel element
}

// Trigger fetch saat "sentinel" element masuk viewport
func (s *InfiniteProductListState) setupObserver() {
    observer := newIntersectionObserver(func(entries []IntersectionEntry) {
        for _, entry := range entries {
            if entry.IsIntersecting && s.hasMore && !s.isLoading {
                s.page++ // setPage(p => p + 1)
            }
        }
    }, ObserverOptions{RootMargin: "300px"}) // mulai fetch 300px sebelum bottom

    observer.Observe(s.loadMore)
    // cleanup saat unmount: observer.Disconnect()
}

// Fetch saat page berubah
func (s *InfiniteProductListState) loadPage() {
    if s.page == 1 && len(s.products) > 0 {
        return
    }

    s.isLoading = true
    go func() {
        newProducts := fetchProducts(s.page)
        s.products = append(s.products, newProducts...)
        s.hasMore = len(newProducts) == 20 // ada lebih banyak jika dapat 20 item
        s.isLoading = false
    }()
}

func (s *InfiniteProductListState) render() *Element {
    return renderFragment(
        renderProductGrid(s.products),
        // Sentinel element — ketika ini masuk viewport, trigger fetch
        s.loadMore,
        s.isLoading && renderLoadingSpinner(),
        !s.hasMore && renderParagraph("Semua produk sudah ditampilkan"),
    )
}
Kapan menggunakan Infinite Scroll:
  ✓ Feed yang tidak linear (social media, news feed)
  ✓ Konten yang user terus consume tanpa tujuan spesifik
  ✓ Mobile app experience
  ✗ Tidak cocok untuk konten yang perlu di-navigate ulang
    (user tidak bisa kembali ke posisi yang sama setelah reload)

Kapan menggunakan Pagination:
  ✓ Hasil pencarian (user perlu tahu "ini halaman 3 dari 15")
  ✓ Daftar yang perlu di-bookmark atau di-share
  ✓ Admin panel dan data table
  ✓ Konten yang user mungkin ingin skip langsung ke halaman tertentu
  ✗ Kurang mulus dari infinite scroll untuk browsing kasual

Debounce dan Throttle untuk Event-Driven Fetch #

Fetch yang dipicu oleh interaksi user — search, scroll, resize — perlu dibatasi agar tidak overload server.

// Debounce: tunggu user berhenti sebelum eksekusi
// Ideal untuk: search input, form auto-save
type Debounce struct {
    timer *time.Timer
    value string
}

func (d *Debounce) Update(value string, delay time.Duration) {
    if d.timer != nil {
        d.timer.Stop() // reset timer setiap value berubah
    }
    d.timer = time.AfterFunc(delay, func() {
        d.value = value // setDebouncedValue(value)
    })
}

func (d *Debounce) Value() string { return d.value }

// Penggunaan untuk search:
func searchBar() {
    query := ""
    debounced := &Debounce{}
    debounced.Update(query, 300*time.Millisecond) // tunggu 300ms setelah user stop typing

    // Padanan useQuery — fetch hanya jika minimal 3 karakter
    var results []Product
    if len(debounced.Value()) > 2 {
        results = searchProducts(debounced.Value())
    }

    // Render: input terikat ke query + daftar hasil
    renderSearchInput(query, func(newQuery string) { query = newQuery })
    renderSearchResults(results)
}
// Throttle: batasi frekuensi eksekusi
// Ideal untuk: scroll position tracking, resize handler, mouse move
func throttle(fn func(), limitMs int64) func() {
    lastCall := int64(0)
    return func() {
        now := time.Now().UnixMilli()
        if now-lastCall >= limitMs {
            lastCall = now
            fn()
        }
    }
}

// Throttle scroll position untuk fetch konten
handleScroll := throttle(func() {
    scrollPercent := (window.ScrollY() / document.Body().ScrollHeight()) * 100
    if scrollPercent > 80 {
        fetchMoreContent()
    }
}, 200) // maksimal dipanggil sekali setiap 200ms

window.AddEventListener("scroll", handleScroll)
Perbedaan debounce vs throttle:

Debounce:
  User terus mengetik → tidak ada fetch
  User berhenti 300ms → fetch dimulai
  Ideal untuk: mencegah fetch saat user masih mengetik

Throttle:
  User terus scroll → fetch dipanggil maksimal setiap 200ms
  Tidak peduli apakah user berhenti atau tidak
  Ideal untuk: update yang perlu berkala selama aksi berlangsung

Cancel Request saat Komponen Unmount #

Di SPA, komponen bisa unmount sebelum request selesai. Jika tidak di-cancel, setState akan dipanggil pada komponen yang sudah tidak ada — menyebabkan memory leak dan error.

// ANTI-PATTERN: Request tidak di-cancel saat unmount
func ProductDetail(productID string) {
    product, err := fetchProduct(productID)
    if err == nil {
        renderProduct(product) // ERROR: komponen mungkin sudah unmount!
    }
}

// BENAR: Gunakan context cancellation (padanan Go dari AbortController)
func ProductDetail(productID string) {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // Cleanup: cancel request saat komponen unmount atau productId berubah

    product, err := fetchProduct(ctx, productID)
    if err != nil {
        if !errors.Is(err, context.Canceled) {
            // AbortError adalah expected — ignore
            // Error lain perlu di-handle
            log.Error("Fetch failed:", err)
        }
        return
    }
    renderProduct(product)
}

// Menggunakan React Query — AbortController sudah built-in
func ProductDetail(productID string) {
    // Padanan React Query: cache dengan key ['product', productID],
    // otomatis pass AbortSignal dan cancel saat query key berubah
    product := useQuery("product", productID, func(ctx context.Context) (*Product, error) {
        return fetchProduct(ctx, productID)
    })
    renderProduct(product)
}

Optimistic UI — Update Sebelum Konfirmasi Server #

Optimistic UI adalah teknik di mana UI diupdate segera saat user melakukan aksi, tanpa menunggu response server. Jika server gagal, UI di-revert.

// Contoh: like button dengan Optimistic UI
type LikeButtonState struct {
    postID       string
    liked        bool
    count        int
    initialCount int
    isLoading    bool
}

func (s *LikeButtonState) HandleLike() {
    wasLiked := s.liked

    // Optimistic update — ubah UI segera
    s.liked = !s.liked
    if wasLiked {
        s.count--
    } else {
        s.count++
    }

    s.isLoading = true
    if err := toggleLike(s.postID); err != nil {
        // Gagal — revert ke state sebelumnya
        s.liked = wasLiked
        s.count = s.initialCount
        showToast("Gagal memperbarui. Coba lagi.")
    }
    // Berhasil — UI sudah benar (tidak perlu ubah apa-apa)
    s.isLoading = false
}

// Render: button dengan ❤️/🤍 + count, disabled saat loading
func (s *LikeButtonState) render() *Element {
    heart := "🤍"
    if s.liked {
        heart = "❤️"
    }
    return renderButton(heart+" "+strconv.Itoa(s.count), s.isLoading, s.HandleLike)
}
Kapan Optimistic UI tepat:
  ✓ Aksi yang jarang gagal (like, follow, bookmark)
  ✓ Aksi yang UI-nya sederhana dan mudah di-revert
  ✓ Aksi yang hasil idealnya sudah jelas sebelum konfirmasi server

Kapan Optimistic UI tidak tepat:
  ✗ Aksi finansial (payment, transfer) — terlalu berisiko jika revert
  ✗ Aksi yang bisa berubah di server (harga yang mungkin sudah berubah)
  ✗ Aksi dengan validasi kompleks di server yang tidak bisa diprediksi

Real-Time Update — Polling vs WebSocket vs SSE #

Untuk konten yang berubah secara real-time, ada tiga pendekatan dengan trade-off berbeda.

// 1. Polling — request berkala ke server
func usePolling(fetchFn func() ([]byte, error), interval time.Duration) []byte {
    var data []byte

    data, _ = fetchFn() // fetch pertama kali

    ticker := time.NewTicker(interval)
    go func() {
        for range ticker.C {
            data, _ = fetchFn()
        }
    }()

    // Cleanup: ticker.Stop() saat komponen unmount
    return data
}

// Gunakan dengan backoff untuk mengurangi beban saat tab tidak aktif
func useSmartPolling(fetchFn func()) {
    interval := 5 * time.Second // mulai dengan 5 detik

    var poll func()
    poll = func() {
        fetchFn()
        // Slow down polling saat tab tidak aktif
        if document.Hidden {
            interval = 30 * time.Second
        } else {
            interval = 5 * time.Second
        }
        time.AfterFunc(interval, poll)
    }
    time.AfterFunc(interval, poll)
}

// 2. Server-Sent Events (SSE) — server push satu arah
func useServerSentEvents(url string) []byte {
    var data []byte
    eventSource := newEventSource(url)

    eventSource.OnMessage = func(event Event) {
        data = json.Unmarshal(event.Data) // setData(JSON.parse(...))
    }

    eventSource.OnError = func() {
        // SSE otomatis reconnect saat koneksi putus
    }

    // Cleanup: eventSource.Close() saat komponen unmount
    return data
}

// 3. WebSocket — bidirectional real-time
// Cocok untuk: chat, collaborative editing, live game
// Lihat dokumentasi framework untuk implementasi yang lebih lengkap
Panduan memilih:

Polling:
  ✓ Data berubah tidak terlalu sering (setiap beberapa detik)
  ✓ Infrastruktur sederhana — tidak perlu koneksi persistent
  ✗ Boros request jika data tidak berubah

SSE (Server-Sent Events):
  ✓ Server perlu push update ke client secara real-time
  ✓ Satu arah cukup (server → client)
  ✓ Otomatis reconnect, lebih sederhana dari WebSocket
  Contoh: live feed, notifikasi, progress tracking

WebSocket:
  ✓ Bidirectional: client dan server saling kirim pesan
  ✓ Low-latency untuk komunikasi intensif
  Contoh: chat, collaborative editing, multiplayer game

Error Handling dan Retry #

Error yang tidak ditangani dengan baik menyebabkan spinner infinite atau blank page — pengalaman yang lebih buruk dari loading yang lambat.

// Pattern error handling yang komprehensif
type DataState struct {
    data       any
    err        error
    isLoading  bool
    retryCount int
}

func useDataWithRetry(fetchFn func() (any, error), maxRetries int) (DataState, func()) {
    state := DataState{isLoading: true}

    var load func(retryCount int)
    load = func(retryCount int) {
        state.isLoading = true
        state.err = nil

        data, err := fetchFn()
        if err != nil {
            if retryCount < maxRetries {
                // Exponential backoff: 1s, 2s, 4s
                delay := time.Duration(math.Pow(2, float64(retryCount))) * time.Second
                time.AfterFunc(delay, func() { load(retryCount + 1) })
            } else {
                state.data = nil
                state.err = err
                state.isLoading = false
                state.retryCount = retryCount
            }
            return
        }
        state.data = data
        state.err = nil
        state.isLoading = false
        state.retryCount = retryCount
    }

    load(0) // load awal
    return state, func() { load(0) } // retry
}

// Penggunaan:
func ProductList() *Element {
    state, retry := useDataWithRetry(fetchProducts, 3)

    if state.isLoading {
        return renderProductSkeleton()
    }
    if state.err != nil {
        return renderErrorState("Gagal memuat produk", retry)
    }
    return renderGrid(state.data)
}
Retry otomatis harus menggunakan exponential backoff — jangan retry dengan interval tetap. Jika semua client melakukan retry pada interval tetap (misal setiap 1 detik), server yang sudah down justru di-hammer dengan ribuan request secara bersamaan saat mulai recovery. Exponential backoff (1s, 2s, 4s, 8s) menyebar beban dan memberikan waktu server untuk recover.

Anti-Pattern Asynchronous Loading #

Request Waterfall yang Tidak Perlu #

// ✗ Anti-pattern: fetch sequential padahal tidak ada dependency
func loadPage() {
    user, _ := fetchUser()
    products, _ := fetchProducts()      // tidak butuh user, tapi menunggu!
    categories, _ := fetchCategories()  // tidak butuh keduanya!
}

// ✓ Solusi: parallel ketika tidak ada dependency
func loadPage() {
    var (
        wg         sync.WaitGroup
        user       any
        products   any
        categories any
    )
    wg.Add(3)
    go func() { defer wg.Done(); user, _ = fetchUser() }()
    go func() { defer wg.Done(); products, _ = fetchProducts() }()
    go func() { defer wg.Done(); categories, _ = fetchCategories() }()
    wg.Wait()
}

Fetch Tanpa Loading State #

// ✗ Anti-pattern: tidak ada feedback selama loading
func ProductList() *Element {
    products := fetchProducts()
    // Selama fetch: daftar kosong, user tidak tahu apa yang terjadi
    return renderGrid(products)
}

// ✓ Solusi: loading, error, dan empty state yang bermakna
func ProductList() *Element {
    // Padanan useQuery: cache dengan key ['products']
    data, isLoading, err := useQuery("products", fetchProducts)

    if isLoading {
        return renderProductSkeleton(8)
    }
    if err != nil {
        return renderErrorState("Gagal memuat", func() { /* refetch */ })
    }
    if len(data) == 0 {
        return renderEmptyState("Belum ada produk")
    }

    return renderGrid(data)
}

Scroll Event tanpa Throttle #

// ✗ Anti-pattern: fetch di setiap scroll event
window.AddEventListener("scroll", func() {
    fetchMoreIfNearBottom() // bisa dipanggil ratusan kali per detik!
})

// ✓ Solusi: gunakan Intersection Observer atau throttle
throttledHandler := throttle(fetchMoreIfNearBottom, 200)
window.AddEventListener("scroll", throttledHandler)
// Atau lebih baik: gunakan Intersection Observer

Fetch yang Tidak Di-cancel saat Komponen Unmount #

// ✗ Anti-pattern: setState setelah unmount
func load() {
    data, _ := fetchData() // komponen mungkin sudah unmount!
    setData(data)
}

// ✓ Solusi: context cancellation (padanan AbortController)
func load() error {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // cleanup saat unmount

    resp, err := fetch(ctx, "/api/data")
    if err != nil {
        if errors.Is(err, context.Canceled) {
            return nil // AbortError adalah expected — ignore
        }
        return err // error lain diteruskan
    }
    setData(resp.JSON())
    return nil
}

Checklist Asynchronous Content Loading #

REQUEST STRATEGY:
  □ Request yang tidak saling bergantung dilakukan secara parallel (Promise.all)
  □ Promise.allSettled digunakan jika partial failure acceptable
  □ Waterfall request hanya ada jika ada dependency yang nyata

LAZY LOADING:
  □ Konten below-the-fold tidak di-fetch saat halaman pertama dimuat
  □ Intersection Observer digunakan (bukan scroll event + getBoundingClientRect)
  □ rootMargin dikonfigurasi untuk prefetch sebelum masuk viewport

INFINITE SCROLL DAN PAGINATION:
  □ Sentinel element ada untuk memicu fetch berikutnya
  □ hasMore state ada untuk mencegah fetch saat sudah tidak ada data
  □ Loading indicator ada di bawah list saat fetch berlangsung
  □ "Sudah semua ditampilkan" state ada saat data habis

DEBOUNCE DAN THROTTLE:
  □ Search input menggunakan debounce (bukan fetch setiap keystroke)
  □ Scroll handler menggunakan throttle atau Intersection Observer
  □ Resize handler menggunakan debounce

CANCEL DAN CLEANUP:
  □ AbortController digunakan dan di-abort di useEffect cleanup
  □ Timer (setTimeout, setInterval) di-cleanup di useEffect cleanup
  □ Subscription di-cleanup di useEffect cleanup

LOADING DAN ERROR STATE:
  □ Skeleton atau loading indicator ada untuk setiap async section
  □ Error state ada dengan opsi retry
  □ Empty state ada untuk daftar yang kosong
  □ Retry menggunakan exponential backoff

OPTIMISTIC UI:
  □ Optimistic update hanya untuk aksi yang jarang gagal
  □ Revert ke state sebelumnya jika server gagal
  □ User diberitahu jika revert terjadi (toast notification)

REAL-TIME:
  □ Polling menggunakan interval yang wajar (bukan terlalu sering)
  □ Polling slowdown saat tab tidak aktif
  □ SSE atau WebSocket untuk update yang benar-benar real-time
  □ Koneksi dibersihkan saat komponen unmount

Ringkasan #

  • Parallel request menggunakan Promise.all hampir selalu lebih cepat — sequential await yang tidak perlu (waterfall) adalah salah satu penyebab paling umum page load yang lambat.
  • Intersection Observer jauh lebih efisien dari scroll event — browser menghandle deteksi visibilitas secara native tanpa layout recalculation di setiap scroll pixel.
  • Debounce untuk input, throttle untuk scroll — debounce menunggu user berhenti sebelum fetch, throttle membatasi frekuensi selama aksi berlangsung.
  • Selalu cancel request saat komponen unmount — gunakan AbortController di useEffect cleanup. React Query sudah menangani ini secara otomatis.
  • Promise.allSettled untuk partial failure — jika satu dari beberapa request gagal, render dengan data yang tersedia alih-alih gagal total.
  • Optimistic UI untuk aksi yang jarang gagal — update UI segera tanpa menunggu server, revert jika gagal. Jauh lebih responsif untuk like, bookmark, dan follow.
  • Exponential backoff untuk retry — interval tetap menyebabkan thundering herd saat server recovery. 1s, 2s, 4s, 8s menyebar beban lebih baik.
  • Infinite scroll untuk browsing, pagination untuk navigasi — infinite scroll cocok untuk feed; pagination cocok untuk hasil pencarian yang perlu di-navigate atau di-bookmark.
  • Loading, error, dan empty state wajib ada — spinner infinite atau blank page tanpa penjelasan adalah UX yang lebih buruk dari loading yang lambat.
  • Polling dengan backoff saat tab tidak aktif — jika user tidak melihat tab, polling bisa diperlambat dari 5 detik menjadi 30 detik untuk menghemat bandwidth dan server resources.

← Sebelumnya: WebP   Berikutnya: OWASP →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact