karawaci.kode

2026-08-14 · 10 min

Astro View Transitions vs SPA Navigation: Benchmark Nyata Blog + Dashboard Hybrid

Klien saya di Jakarta minta satu aplikasi yang fungsinya separuh blog editorial (50+ artikel, SEO penting) dan separuh dashboard analitik internal (chart real-time, filter interaktif, beberapa form). Mereka tidak mau mengelola dua codebase. Pertanyaannya langsung ke inti: Astro dengan View Transitions, atau Next.js App Router sebagai SPA?

Saya habiskan dua minggu menjalankan keduanya di production-like environment, mengukur angka nyata, lalu membuat keputusan. Ini laporannya.

Setup Benchmark

Dua implementasi dibangun dengan skenario identik: 12 halaman artikel (markdown, rata-rata 1.400 kata), 3 halaman dashboard (tabel data, line chart via Chart.js, satu form filter dengan tanggal range). Dihosting di Cloudflare Pages. Pengukuran dilakukan via WebPageTest dari node Singapura dengan profil throttling Motorola G5 + 4G LTE.

Stack A — Astro 5.x + View Transitions:

src/
  pages/
    blog/[slug].astro      # MPA route
    dashboard/
      analytics.astro
      users.astro
      reports.astro
  components/
    ChartIsland.tsx         # React island, client:load
    FilterForm.tsx          # React island, client:visible

Konfigurasi View Transitions di layout utama:

---
// layouts/Base.astro
import { ViewTransitions } from 'astro:transitions';
---
<html>
  <head>
    <ViewTransitions />
  </head>
  <body>
    <nav transition:persist>
      <!-- nav tidak di-remount saat navigasi -->
    </nav>
    <main>
      <slot />
    </main>
  </body>
</html>

Stack B — Next.js 15 App Router:

app/
  blog/[slug]/page.tsx      # RSC
  dashboard/
    analytics/page.tsx
    users/page.tsx
    reports/page.tsx
  components/
    ChartClient.tsx         # 'use client'
    FilterForm.tsx          # 'use client'

Keduanya memakai data yang identik, font yang sama (Inter via font-display: swap), dan tidak ada perbedaan logika bisnis.

Angka yang Keluar

Metrik diambil dari rata-rata 5 run WebPageTest, initial load dari homepage lalu navigasi ke halaman artikel, lalu ke dashboard analytics.

Initial Load (Homepage):

MetrikAstro + VTNext.js 15
TTFB142ms198ms
LCP1.2s1.6s
TBT (Total Blocking Time)12ms89ms
JS Bundle (gzip)18 KB187 KB
TTI1.4s2.1s

Perbedaan JS bundle adalah yang paling mencolok. Astro hanya mengirim JavaScript untuk island yang ada di halaman tersebut. Next.js membawa router, RSC protocol, dan hydration runtime sejak awal.

Navigasi ke Halaman Artikel (subsequent navigation):

MetrikAstro + VTNext.js 15
LCP0.9s0.4s
Layout Shift (CLS)0.020.01
Durasi animasi transisi300ms250ms (custom)

Di sini Next.js mulai unggul. Karena konten artikel sudah di-prefetch sebagai RSC payload (JSON, bukan HTML), navigasi kedua terasa instan — browser tidak perlu parse HTML baru, hanya swap komponen. Astro melakukan fetch HTML lengkap dari edge, tapi karena Cloudflare Pages menyimpannya di edge cache, round-trip tetap cepat. Bukan kalah telak, tapi terasa.

Navigasi ke Dashboard (halaman dengan React island berat):

MetrikAstro + VTNext.js 15
LCP1.1s0.6s
Time to Interactive (island)1.8s1.1s
JS yang di-load94 KB187 KB

Next.js menang di sini karena bundle sudah ada di memory — tidak ada re-download. Astro harus memuat ulang island JavaScript-nya walau halaman sudah dikunjungi, karena cache browser tidak menjamin modul tetap di memori.

Mengonfigurasi View Transitions untuk Shared Elements

Salah satu fitur paling berguna yang sering dilewatkan adalah animasi shared element — elemen yang “terbang” dari satu halaman ke halaman berikutnya. Untuk blog dengan thumbnail artikel yang muncul di listing lalu menjadi hero image di detail, ini signifikan secara UX:

<!-- Di halaman listing (blog/index.astro) -->
<img
  src={post.cover}
  alt={post.title}
  transition:name={`cover-${post.slug}`}
  transition:animate="fade"
/>

<!-- Di halaman detail (blog/[slug].astro) -->
<img
  src={entry.data.cover}
  alt={entry.data.title}
  transition:name={`cover-${entry.slug}`}
  class="hero-image"
/>

Browser mendeteksi kedua elemen dengan transition:name yang sama, lalu menghitung transformasi posisi dan ukurannya secara otomatis. Tidak perlu menghitung posisi manual dengan JavaScript.

Untuk dashboard, saya pakai transition:persist agar sidebar dan header tidak di-remount, yang mencegah flicker visual saat berpindah halaman:

<aside transition:persist="sidebar">
  <DashboardNav client:load />
</aside>

Trade-off yang Saya Temukan di Production

Yang membuat saya sempat frustrasi dengan View Transitions:

Scroll position tidak dipreservasi secara otomatis di semua browser. Di Firefox 130, saat kembali ke halaman listing setelah membaca artikel, posisi scroll tidak kembali ke posisi kartu yang diklik. Saya harus menambah handler manual:

// Simpan posisi scroll sebelum navigasi
document.addEventListener('astro:before-preparation', () => {
  sessionStorage.setItem(
    `scroll-${location.pathname}`,
    window.scrollY.toString()
  );
});

// Pulihkan setelah navigasi selesai
document.addEventListener('astro:page-load', () => {
  const saved = sessionStorage.getItem(`scroll-${location.pathname}`);
  if (saved) {
    window.scrollTo({ top: parseInt(saved), behavior: 'instant' });
    sessionStorage.removeItem(`scroll-${location.pathname}`);
  }
});

Yang membuat saya lebih memilih Astro untuk proyek ini:

Satu hal yang tidak muncul di benchmark tapi sangat terasa di production: cold start behavior. Next.js App Router dengan Vercel/Cloudflare mengalami cold start tiap kali serverless function belum warm. Untuk dashboard yang dikunjungi pagi hari setelah semalam tidak ada traffic, TTFB bisa menyentuh 800ms–1.2s. Astro yang sepenuhnya pre-rendered dan disajikan dari edge CDN konsisten di 80–150ms tanpa peduli kapan terakhir dikunjungi.

Untuk konten editorial yang mayoritas statis, ini perbedaan yang terasa secara nyata bagi pengguna.

Yang harus Anda relakan dengan Astro:

  • Tidak ada optimistic UI tanpa implementasi manual. Di form dashboard saya, setelah submit filter, ada full navigation cycle karena page-based routing. Saya mitigasi dengan client:load island yang mengambil data secara mandiri via fetch tanpa routing.
  • Global state yang survive navigasi butuh Nanostores atau solusi eksternal:
// stores/dashboardFilter.ts
import { atom } from 'nanostores';

export const dateRange = atom<{ from: string; to: string }>({
  from: new Date(Date.now() - 7 * 86400000).toISOString().slice(0, 10),
  to: new Date().toISOString().slice(0, 10),
});

Island React cukup subscribe ke store ini. State tetap ada saat navigasi antar halaman dashboard karena Nanostores hidup di module scope JavaScript, bukan di React component tree.

Keputusan Akhir

Untuk proyek blog + dashboard hybrid ini, saya pilih Astro dengan View Transitions. Alasannya:

  1. SEO dan initial load menang jelas. Blog editorial membutuhkan LCP yang baik untuk indexing dan Core Web Vitals. Selisih 400ms LCP di initial load adalah perbedaan yang cukup besar di peringkat pencarian.

  2. Cold start tidak ada. Dashboard internal yang diakses pagi hari tidak perlu menunggu serverless warm-up.

  3. Kompleksitas lebih rendah. Tim yang mengelola proyek ini bukan full-time frontend engineer — dua backend developer yang sesekali menyentuh UI. Astro dengan island architecture lebih mudah dipahami dan diprediksi daripada RSC mental model Next.js.

  4. Subsequent navigation cukup baik. Selisih 500ms di navigasi kedua terasa, tapi tidak cukup menyakitkan untuk kalangan pengguna internal yang sudah terbiasa dengan aplikasi web biasa.

Saya akan pilih Next.js kalau: mayoritas pengguna adalah heavy user yang berpindah halaman puluhan kali per sesi (misal aplikasi admin dengan banyak workflow berurutan), atau kalau dibutuhkan optimistic mutation yang kompleks seperti aplikasi task management real-time.

Untuk sebagian besar proyek hybrid yang saya lihat di startup Indonesia — blog produk + dashboard ringan — Astro dengan View Transitions adalah pilihan yang lebih tepat. JavaScript lebih sedikit, initial load lebih cepat, dan kompleksitas yang sepadan dengan kebutuhan nyata.

Ditulis oleh Reza Pradipta