Selama lebih dari satu dekade, single-page application (SPA) menjadi jawaban default untuk hampir semua produk web — padahal tidak semua produk membutuhkannya. Arah angin kini berbalik: kerangka HTMX dan Hotwire membuktikan bahwa server yang mengirim HTML penuh, ditambah interaktivitas berbasis fragmen, cukup untuk mayoritas aplikasi — dengan jauh lebih sedikit JavaScript dan kompleksitas. Kabar baiknya, PHP sejak lahir memang bekerja dengan cara itu. Section ini membahas arsitektur web modern dari perspektif PHP, dan chapter pertamanya mengembalikan kita ke model kerja paling alami bahasa ini: server merender HTML lengkap di setiap request.
Kebangkitan Server-Rendered #
SPA menjanjikan pengalaman aplikasi desktop di browser: transisi halus tanpa muat ulang, state yang bertahan, dan antarmuka yang responsif. Janji tersebut nyata bagi aplikasi yang sangat interaktif, tetapi strategi “SPA untuk semuanya” menagih tagihan yang berat: build pipeline yang harus dirawat, bundel JavaScript yang membengkak, state yang harus disinkronkan dua arah antara klien dan server, dan API yang harus dibangun khusus untuk frontend milik kita sendiri. Untuk situs konten, toko, dan aplikasi formulir, sebagian besar kompleksitas tersebut dibayar tanpa hasil yang terasa.
Server-rendered applications menawarkan tawaran berlawanan. Server merakit HTML lengkap untuk setiap request, browser tinggal menampilkan hasilnya — first paint lebih cepat karena tidak menunggu JavaScript mengunduh dan mengeksekusi sebelum konten terlihat, dan model mentalnya satu arah: satu request menghasilkan satu halaman. Perkembangan komunitas web beberapa tahun terakhir — HTMX di sisi ekosistem umum, Hotwire di sisi Rails, bahkan React Server Components di sisi frontend — semuanya mengakui intuisi yang sama: HTML dari server tetap cara termurah mengirim konten ke browser.
PHP dan Rendering yang Alami #
PHP tidak perlu “kembali” ke server-side rendering karena PHP tidak pernah pergi. Siklus request PHP yang kita bedah di Request Lifecycle
adalah model server-rendered yang murni: setiap permintaan menjalankan skrip, skrip merender HTML — dulu dengan echo, kini dengan engine template seperti Twig — lalu memori dibersihkan sepenuhnya. Tidak ada proses yang harus dihidupkan, tidak ada state antar request yang harus dikelola, tidak ada build step sama sekali: berkas .php yang Anda tulis adalah berkas yang berjalan.
Kerangka kerja template yang kita bangun di Template Rendering memperlihatkan betapa dekatnya PHP dengan pekerjaan ini: satu fungsi yang membaca berkas template, mengganti placeholder dengan data, dan mengembalikan HTML. Seluruh framework besar PHP — Laravel dengan Blade, Symfony dengan Twig — bekerja dengan ide inti yang sama, hanya dengan tambahan escaping otomatis, inheritance template, dan komponen reusable. Aplikasi server-rendered PHP bukan teknologi lama yang bertahan; aplikasi tersebut adalah penerapan langsung dari model eksekusi bahasanya.
Interaktivitas via Fragmen HTML #
Titik lemah klasik server-rendered adalah interaktivitas kecil: memperbarui satu bagian halaman — jumlah notifikasi, daftar komentar — tanpa memuat ulang seluruhnya. Dulu kebutuhan inilah yang mendorong semuanya ke arah SPA. Pendekatan modern menjawabnya dengan tukar-menukar fragmen HTML: pustaka JavaScript kecil di browser meminta potongan HTML ke server, lalu mengganti isi elemen tertentu dengan respons tersebut. Kerangka HTMX menyatakan kontrak ini langsung sebagai atribut pada HTML:
<!-- Tombol HTMX: minta fragmen lalu tukar isi elemen -->
<button hx-get="/fragmen/jumlah-notifikasi" hx-target="#jumlah-notifikasi" hx-swap="outerHTML">
Segarkan notifikasi
</button>
<span id="jumlah-notifikasi">5</span>Atribut hx-get menetapkan URL fragmen, hx-target menunjuk elemen yang akan diganti, dan hx-swap menentukan cara penggantiannya — dalam kasus ini elemen target diganti seluruhnya oleh respons. Ketika tombol ditekan, HTMX mengirim permintaan GET biasa ke server, dan server tinggal mengembalikan potongan HTML. Sisi server dalam PHP murni akan tampak seperti fungsi berikut:
<?php
/**
* Handler fragmen HTML: menerima nama fragmen dan data,
* lalu mengembalikan potongan HTML — bukan halaman penuh.
*/
function renderFragmen(string $namaFragmen, array $data): string
{
return match ($namaFragmen) {
'jumlah-notifikasi' => sprintf(
'<span id="jumlah-notifikasi">%d</span>',
$data['jumlah'],
),
'daftar-komentar' => '<ul id="daftar-komentar">' . implode('', array_map(
static fn (array $komentar): string => sprintf(
'<li><strong>%s</strong>: %s</li>',
htmlspecialchars($komentar['penulis'], ENT_QUOTES),
htmlspecialchars($komentar['isi'], ENT_QUOTES),
),
$data['komentar'],
)) . '</ul>',
default => throw new InvalidArgumentException("Fragmen tidak dikenal: {$namaFragmen}"),
};
}
echo renderFragmen('jumlah-notifikasi', ['jumlah' => 3]) . "\n";
echo renderFragmen('daftar-komentar', ['komentar' => [
['penulis' => 'Budi', 'isi' => 'Materi ini membantu sekali!'],
['penulis' => 'Siti', 'isi' => 'Menunggu <b>lanjutan</b>-nya.'],
]]) . "\n";Output yang Dihasilkan:
<span id="jumlah-notifikasi">3</span>
<ul id="daftar-komentar"><li><strong>Budi</strong>: Materi ini membantu sekali!</li><li><strong>Siti</strong>: Menunggu <b>lanjutan</b>-nya.</li></ul>Ekspresi match memilih bentuk fragmen berdasarkan nama, dan setiap cabang merakit HTML dari data. Perhatikan dua detail penting pada cabang daftar-komentar. Pertama, fungsi array_map dengan arrow function memetakan setiap komentar menjadi elemen <li> — pola yang sama dengan koleksi komentar nyata dari database. Kedua, setiap nilai yang berasal dari pengguna melewati htmlspecialchars() dengan ENT_QUOTES; fragmen tetap HTML, sehingga aturan Output Escaping
berlaku sama kerasnya seperti pada halaman penuh — komentar Siti yang mengandung tag <b> tampil sebagai teks, bukan markup yang dieksekusi browser. Bagi endpoint /fragmen/jumlah-notifikasi, handler inilah seluruh “API”-nya: respons adalah HTML siap tampil, dan browser menukarnya tanpa ada JSON yang perlu diparsing dan dirender ulang.
Menghindari Kompleksitas SPA #
Untuk situs konten dan aplikasi berbasis formulir, daftar infrastruktur yang tidak dibutuhkan adalah argumen terkuat server-rendered. Tidak ada bundler yang harus dikonfigurasi dan diperbarui; tidak ada state management klien yang harus disinkronkan dengan state server; tidak ada API JSON yang dibangun khusus agar frontend kita sendiri bisa membaca data aplikasi kita sendiri. Setiap fitur baru hidup di satu tempat — handler server dan template-nya — alih-alih tersebar pada endpoint, model klien, dan komponen tampilan.
Model mental satu arah ini juga menyederhanakan penalaran tentang kebenaran. Pada SPA, satu tampilan bisa menjadi benar di server dan salah di klien karena state yang tidak sinkron; pada aplikasi server-rendered, HTML yang dikirim adalah kebenaran tunggal pada saat request. Kompleksitas yang hilang bukan sihir — kompleksitas tersebut pindah ke server, tempat PHP, database, dan cache sudah bekerja sama dengan baik. Interaktivitas yang memang dibutuhkan tetap didapat melalui fragmen, satu interaksi demi satu interaksi, bukan dengan memindahkan seluruh aplikasi ke browser.
Dasar SEO untuk Konten #
Mesin pencari membaca HTML. Konten yang dirender penuh di server tiba di crawler lengkap sejak respons pertama — judul, teks, tautan, dan struktur heading sudah ada di dalam payload. Untuk situs yang hidup dari pencarian organik, kepemilikan tersebut fundamental: setiap halaman produk, setiap artikel, setiap profil pengguna adalah dokumen HTML unik dengan URL sendiri yang bisa diindeks, ditautkan, dan dibagikan. Metadata — <title> yang unik, meta description, canonical URL — adalah data template biasa yang diisi dari sisi server, persis pola output yang sudah kita kuasai.
SPA berhadapan dengan masalah yang sama dari arah berlawanan: crawler harus mengeksekusi JavaScript sebelum konten terlihat, sehingga muncul kerangka tambahan — server-side rendering untuk SPA, prerendering, atau hydration penuh — demi mendapatkan apa yang aplikasi PHP terima secara gratis. Ironi sejarahnya cukup jelas: sebagian besar investasi rekayasa pada kerangka JavaScript modern bertujuan mengembalikan perilaku yang dimiliki PHP sejak awal.
Progressive Enhancement #
Server-rendered yang sehat dibangun dari prinsip progressive enhancement: fungsionalitas inti bekerja dengan HTML seminimal mungkin, lalu JavaScript menambah kenyamanan secara berlapis. Formulir adalah contoh terbaiknya — sebuah <form> dengan method="post" yang valid sudah berfungsi penuh tanpa satu baris JavaScript; lapisan berikutnya mencegat pengiriman dengan JavaScript untuk menampilkan pesan kesalahan lebih halus atau menyimpan draf; lapisan teratas menambah autocomplete dan validasi instan.
Pendekatan berlapis ini membayar dividen pada kondisi nyata: JavaScript gagal dimuat karena jaringan lambat, ekstensi browser memblokir skrip, atau pengguna memakai perangkat tua. Pada arsitektur yang menghormati enhancement, hal tersebut menurunkan kenyamanan — bukan mematikan aplikasi. Aksesibilitas juga ikut menang, karena elemen HTML native membawa perilaku keyboard dan pembaca layar secara bawaan, alih-alih bergantung pada JavaScript untuk menyediakannya.
Kapan SPA Tetap Menang #
| Kebutuhan | Mengapa SPA unggul |
|---|---|
| Dashboard sangat interaktif | Ratusan elemen berubah serentak menuntut manajemen state klien |
| Kolaborasi real-time | Sinkronisasi lintas pengguna memang hidup di klien |
| Aplikasi offline-first | Logika harus berjalan tanpa koneksi ke server |
| Berbagi logika dengan aplikasi mobile | Satu kode TypeScript bisa dipakai lintas platform |
| Transisi kaya antar tampilan | Animasi antarhalaman menuntut kontrol penuh atas DOM |
Tabel tersebut bukan daftar alasan menghindari server-rendered — tabel tersebut adalah batas kejujuran. Di luar lima baris tersebut, mayoritas produk web adalah konten, formulir, dan interaksi fragmen: wilayah tempat server-rendered PHP menghasilkan lebih cepat dengan kompleksitas lebih rendah. Keputusan arsitektur selanjutnya naik tingkat dari “bagaimana halaman dirender” ke “bentuk apa yang diberikan pada aplikasi secara keseluruhan” — dan itulah pembahasan berikutnya: monolith versus microservices.