Bagi tim rekayasa perangkat lunak yang mengelola aplikasi web Next.js berskala besar, keputusan untuk beralih dari Pages Router (direktori pages/) ke App Router (direktori app/) sering kali tertunda oleh kekhawatiran akan terjadinya regresi fungsional, rusaknya sesi autentikasi, atau penurunan peringkat SEO akibat gangguan pada waktu respons server.
Namun, menunda migrasi tanpa batas waktu membawa risiko keusangan teknis: arsitektur React Server Components (RSC), integrasi streaming SSR via Suspense, dan pembaruan Core Web Vitals terbaru dirancang dengan fokus utama pada App Router.
Kunci keberhasilan migrasi pada aplikasi enterprise adalah adopsi inkremental (incremental adoption), bukan menulis ulang seluruh kode dari nol (big-bang rewrite).
Prinsip Koeksistensi Halaman: Bagaimana Next.js Menangani Kedua Router? #
Salah satu keunggulan desain Next.js adalah kemampuannya menjalankan Pages Router dan App Router secara berdampingan dalam satu build produksi. Ketika kedua direktori ada dalam repositori:
- Rute yang didefinisikan di
app/diprioritaskan di atas rute dengan path yang sama dipages/. - Halaman-halaman lama yang belum dimigrasikan tetap berjalan normal menggunakan siklus Pages Router (
_app.tsx,_document.tsx,getServerSideProps). - Pengembang dapat memigrasikan satu rute mandiri (misalnya halaman kebijakan privasi atau blog), mengujinya di staging, dan merilisnya ke produksi secara independen.
Struktur Proyek Hibrida Selama Periode Transisi:
my-enterprise-app/
├── pages/
│ ├── _app.tsx # Tetap melayani rute warisan
│ ├── index.tsx # Rute beranda warisan
│ └── dashboard/
│ └── [id].tsx # Fitur kompleks yang dimigrasi terakhir
├── app/
│ ├── layout.tsx # Root layout global untuk rute App Router
│ ├── blog/
│ │ ├── page.tsx # Halaman blog (sudah migrasi ke RSC)
│ │ └── [slug]/
│ │ └── page.tsx # Detail artikel dengan dynamic metadata
│ └── api/
│ └── sitemap-data/
│ └── route.ts # Route Handler modern
Memetakan Pergeseran Pola Data Fetching #
Perubahan paling fundamental dalam App Router adalah penghapusan fungsi siklus hidup warisan seperti getServerSideProps, getStaticProps, dan getStaticPaths. Seluruh pengambilan data kini disederhanakan menggunakan fungsi native fetch dengan opsi revalidasi bawaan langsung di dalam React Server Components.
| Pola Pages Router (Lama) | Pola App Router (Modern) | Karakteristik Perilaku |
|---|---|---|
getStaticProps |
fetch(url, { cache: 'force-cache' }) |
Static Site Generation (SSG) murni |
getStaticProps + revalidate |
fetch(url, { next: { revalidate: 60 } }) |
Incremental Static Regeneration (ISR) |
getServerSideProps |
fetch(url, { cache: 'no-store' }) |
Dynamic Server-Side Rendering (SSR) |
getStaticPaths |
export async function generateStaticParams() |
Pra-render rute dinamis pada saat build |
Contoh Perbandingan Kode: Halaman Detail Artikel #
Pada Pages Router, kode terpecah antara fungsi server-side dan fungsi komponen klien:
// pages/blog/[slug].tsx (LAMA)
import { GetServerSideProps } from 'next';
export const getServerSideProps: GetServerSideProps = async ({ params }) => {
const res = await fetch(`https://api.perusahaan.com/posts/${params?.slug}`);
const post = await res.json();
return { props: { post } };
};
export default function PostPage({ post }: { post: any }) {
return <article><h1>{post.title}</h1></article>;
}
Pada App Router, komponen adalah async function yang berjalan sepenuhnya di server, mengeliminasi pengiriman JavaScript yang tidak perlu ke browser:
// app/blog/[slug]/page.tsx (MODERN)
import { notFound } from 'next/navigation';
interface Props {
params: { slug: string };
}
// 1. Pembuatan metadata dinamis terisolasi untuk SEO
export async function generateMetadata({ params }: Props) {
const post = await fetchPost(params.slug);
if (!post) return { title: 'Tidak Ditemukan' };
return {
title: post.title,
description: post.excerpt,
};
}
async function fetchPost(slug: string) {
const res = await fetch(`https://api.perusahaan.com/posts/${slug}`, {
next: { revalidate: 3600 }, // ISR per jam
});
if (!res.ok) return null;
return res.json();
}
export default async function PostPage({ params }: Props) {
const post = await fetchPost(params.slug);
if (!post) notFound();
return (
<article className="prose max-w-4xl mx-auto py-12">
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
Menentukan Batas Server Components dan Client Components #
Kesalahan umum bagi pengembang yang baru beradaptasi dengan App Router adalah menambahkan direktif 'use client' di bagian atas setiap file layout atau halaman. Pendekatan ini menghilangkan manfaat arsitektur Server Components dan membuat ukuran bundle aplikasi membengkak kembali.
Aturan praktis menentukan batas (boundary):
- Jadikan Server Component sebagai Default: Semua komponen layout, fetching data, parsing Markdown, dan perakitan layout utama harus tetap menjadi Server Component.
- Dorong Client Component ke Daun Terluar Pohon Komponen (Leaf Nodes): Gunakan
'use client'hanya pada elemen yang membutuhkan interaktivitas pengguna, seperti tombol toggle dark-mode, form dengan state lokaluseState, modal popup, atau event listeneronClick.
┌─────────────────────────────────────────────────────────────┐
│ Page (Server Component) │
│ - Mengambil data post dari database/API │
│ - Merender konten artikel statis │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Client Component ('use client') │ │
│ │ - Form komentar interaktif │ │
│ │ - State 'useState', 'useEffect' │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Strategi Migrasi Step-by-Step Tanpa Risiko Downtime #
Agar migrasi pada aplikasi enterprise berjalan mulus tanpa mengganggu aktivitas bisnis, ikuti lima langkah bertahap:
Langkah 1: Buat Root Layout Bersama #
Buat app/layout.tsx yang memuat font, metadata dasar, dan stylesheet global. Pastikan CSS modular tidak saling menimpa gaya halaman di pages/.
Langkah 2: Migrasikan Halaman Statis dan Publik Terlebih Dahulu #
Pindahkan halaman /about, /faq, /terms, dan /contact. Halaman-halaman ini memiliki ketergantungan minimal terhadap state global yang kompleks, sehingga tim dapat memverifikasi kestabilan pipeline CI/CD lebih dini.
Langkah 3: Migrasikan Struktur Routing Dinamis dan Blog #
Pindahkan rute /blog dan /blog/[slug]. Manfaatkan generateStaticParams() dan generateMetadata() untuk memastikan skor Core Web Vitals dan metadata OpenGraph tetap terindeks sempurna oleh mesin pencari.
Langkah 4: Migrasikan Middleware dan Autentikasi #
Gunakan next/headers untuk membaca cookie sesi di dalam Server Components. Hindari melakukan panggilan API berulang untuk memvalidasi user token dengan memanfaatkan mekanisme React cache() memoization.
Langkah 5: Bersihkan Halaman Terakhir dan Hapus Direktori pages/
#
Setelah seluruh fitur kompleks (seperti dashboard checkout atau portal akun) berhasil dipindahkan dan diuji melalui automated end-to-end testing (Playwright/Cypress), hapus direktori pages/ sepenuhnya untuk memangkas waktu kompilasi Turbopack.
