Anjrahweb.com – Pernahkah kamu membuka sebuah website, tunggu beberapa detik, lalu langsung tutup karena kelamaan? Kamu tidak sendirian. Riset menunjukkan bahwa pengunjung modern punya kesabaran sangat tipis. Kalau halaman tidak muncul dalam 3 detik, lebih dari setengah pengunjung akan pergi. Google juga lebih suka website yang cepat, karena kecepatan jadi salah satu faktor penentu di algoritma peringkat pencarian.
Dan yang paling menyakitkan, konversi penjualan turun drastis kalau website lambat. Pengunjung yang sudah hampir checkout bisa batal hanya karena tombol loading terlalu lama muncul.
Bayangkan website itu seperti restoran. Pelanggan datang, duduk, lalu pesan makanan. Kalau makanan lama keluar dari dapur, pelanggan akan kesal dan pergi ke restoran sebelah. Kecepatan website itu sama dengan kecepatan dapur menyajikan makanan. Semakin cepat dapur bekerja, semakin puas pelanggan, semakin banyak mereka kembali lagi. Dapur yang lambat bukan hanya membuat pelanggan pergi, tapi juga bikin reputasi restoran turun di mata orang lain.
Banyak vibecoder pemula yang baru mulai pakai Laravel fokusnya hanya ke fitur. Mau tambah halaman ini, fitur itu, modul baru ini. Speed website sering dilupakan sampai traffic mulai tumbuh dan pengunjung mengeluh. Padahal optimasi speed itu seperti membayar utang teknis yang wajar dibayar saat website mulai ramai. Utang teknis ini kalau dibiarkan akan menumpuk dan makin sulit dibayar.
Lebih baik mulai perbaiki sedikit demi sedikit sekarang, daripada panik nanti saat website sudah ramai pengunjung dan server mulai merintih.
Daftar Isi
1. Aktifkan SPA Mode di Filament
2. Pakai wire:navigate di Link Navigasi
3. Turunkan Interval Polling Notifikasi
4. Eager Loading dengan with()
5. Memoize Method Getter per-Request
6. Cache Data Semi-Statis
7. Pakai Redis untuk Cache, Session, dan Queue
8. Aktifkan HTTP/2 di nginx
9. Kompresi Brotli dan gzip
10. Cache Header untuk Static Asset
11. Vite Asset Bundling dan Code Splitting
12. OPcache Config Sesuai Jumlah File PHP
13. PHP-FPM max_children Sesuai RAM Server
14. Lazy Loading dan Responsive Images
15. Database Indexing untuk Kolom yang Sering Di-Query
Kiat 1: Aktifkan SPA Mode di Filament
Kalau kamu pakai Filament untuk panel admin, ada satu setting kecil yang efeknya besar. Secara default, Filament melakukan full page reload setiap kali kamu klik navigasi. Artinya setiap klik, browser memuat ulang seluruh halaman dari nol, termasuk CSS, JavaScript, gambar, dan semua resource. Ini terasa lambat, apalagi kalau koneksi internet pengguna tidak terlalu cepat.
Filament punya fitur SPA (Single Page Application) mode yang bisa diaktifkan dengan mudah. Dengan menambahkan ->spa(true, true), hanya bagian body halaman yang di-swap, sementara script dan style tetap di memori browser. Parameter kedua true juga mengaktifkan prefetch saat hover, artinya saat kamu mengarahkan kursor ke sebuah menu, Filament sudah mulai memuat halaman tujuan di latar belakang sebelum kamu klik. Saat klik terjadi, halaman sudah siap dan muncul hampir instan.
Analoginya seperti ini. Bayangkan restoran yang tiap pesanan baru bikin dari nol, mulai dari menyalakan kompor, ambil bahan dari gudang, cuci piring, baru masak. Itu seperti full page reload. Sekarang bayangkan restoran yang sudah menyiapkan bahan sebelumnya, bumbu sudah diukur, dan kompor sudah panas. Saat pesanan masuk, tinggal masak langsung jadi. Itu seperti SPA mode dengan prefetch. Hasilnya, pelanggan tidak perlu nunggu lama.
Berikut cara mengaktifkannya di file app/Providers/Filament/AdminPanelProvider.php.
public function panel(Panel $panel): Panel
{
return $panel
->default()
->id('admin')
->path('admin')
->login()
->spa(true, true) // Aktifkan SPA mode + prefetch saat hover
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets');
}
Cukup tambahkan ->spa(true, true) di dalam method panel. Simpan file, refresh halaman admin, lalu coba klik menu navigasi. Kamu akan merasakan perbedaan kecepatannya secara langsung. Navigasi terasa mulus dan cepat, tidak ada kedipan putih seperti saat full reload.
Kiat 2: Pakai wire:navigate di Link Navigasi
Kiat ini cocok untuk area member atau halaman non-Filament yang masih pakai Livewire. Secara default, setiap link <a href="..."> akan memicu browser memuat ulang seluruh halaman. Ini sama seperti full page reload di Filament, semua resource dimuat dari awal lagi. Untuk halaman yang sering dikunjungi seperti dashboard member, halaman course, atau halaman profil, ini bikin pengalaman pengguna terasa berat.
Livewire v3 punya solusi sederhana yaitu atribut wire:navigate. Kamu cukup menambahkan atribut ini di tag <a>, dan Livewire akan mengambil halaman tujuan via AJAX, lalu menukar body tanpa memuat ulang seluruh halaman. Script dan style tetap di memori browser, jadi transisi terasa mulus seperti aplikasi native. Livewire v3 juga otomatis inject script yang dibutuhkan, jadi kamu tidak perlu setting tambahan apa pun.
Analoginya seperti naik tangga vs naik lift. Full reload itu seperti kamu turun dari lantai 5 ke lantai dasar, keluar gedung, masuk lagi lewat pintu depan, lalu naik tangga ke lantai tujuan. Capek dan lama banget. Sementara wire:navigate itu seperti naik lift, kamu langsung lompat ke lantai tujuan tanpa perlu keluar gedung. Cepat, hemat tenaga, dan mulus.
Berikut contoh penggunaannya di template Blade.
<!-- Sebelum (full reload, lambat) -->
<a href="{{ route('member.dashboard') }}">Dashboard</a>
<a href="{{ route('member.courses') }}">Kursus Saya</a>
<a href="{{ route('member.profile') }}">Profil</a>
<!-- Sesudah (wire:navigate, cepat) -->
<a href="{{ route('member.dashboard') }}" wire:navigate>Dashboard</a>
<a href="{{ route('member.courses') }}" wire:navigate>Kursus Saya</a>
<a href="{{ route('member.profile') }}" wire:navigate>Profil</a>
Cukup tambahkan wire:navigate di setiap link navigasi internal. Hindari memakainya untuk link eksternal yang menuju domain lain, karena wire:navigate hanya untuk navigasi internal di aplikasi Laravel kamu. Untuk link eksternal, biarkan sebagai link biasa.
Kiat 3: Turunkan Interval Polling Notifikasi
Filament punya fitur database notifications yang menampilkan lonceng notifikasi di pojok kanan atas panel admin. Fitur ini berguna, tapi ada jebakan yang sering tidak disadari pemula. Secara default, interval polling notifikasi bisa di-set ke 10 detik. Artinya setiap 10 detik, setiap halaman admin yang terbuka akan mengirim request AJAX ke server untuk mengecek apakah ada notifikasi baru.
Bayangkan kalau ada 5 admin yang sedang login dan masing-masing membuka 2 tab. Itu berarti 10 koneksi yang tiap 10 detik kirim request ke server. Dalam 1 menit, server menerima 60 request hanya untuk mengecek notifikasi. Padahal notifikasi penting jarang sekali muncul tiap 10 detik. Ini pemborosan resource server yang tidak perlu, dan bikin server sibuk melayani polling daripada melayani pengunjung asli.
Solusinya, naikkan interval polling ke 30 atau 60 detik. Notifikasi penting tetap akan ketahuan, hanya saja ceknya tidak terlalu sering. Analoginya seperti mengecek kotak surat. Kalau kamu tiap 10 detik buka kotak surat, kamu capek dan boros tenaga, padahal surat penting jarang datang tiap 10 detik. Kalau dicek tiap 30 detik saja, itu sudah cukup. Surat penting tetap ketahuan, tapi kamu tidak kelelahan bolak-balik ke kotak surat.
Berikut cara mengaturnya di PanelProvider.
public function panel(Panel $panel): Panel
{
return $panel
->default()
->id('admin')
->path('admin')
->databaseNotifications()
->databaseNotificationsPolling('30s') // Naikkan dari default ke 30 detik
->spa(true, true)
// ... konfigurasi lainnya
}
Ganti '30s' dengan '60s' kalau notifikasi di aplikasi kamu tidak terlalu urgent. Dengan perubahan kecil ini, beban server berkurang signifikan, terutama saat banyak admin aktif bersamaan. Server jadi punya waktu untuk melayani request yang benar-benar penting.
Kiat 4: Eager Loading dengan with()
Masalah N+1 query adalah penyebab lambat yang paling sering menimpa pemula Laravel, tapi jarang disadari karena tidak muncul sebagai error. N+1 query terjadi saat kamu loop sebuah data dan di dalam loop itu kamu panggil relasi yang memicu query baru ke database.
Contoh klasik, kamu ambil 10 blog post lalu di view kamu tampilkan $post->category->name. Saat loop berjalan, Laravel mengambil category satu per satu untuk tiap post. Total query jadi 1 (untuk ambil 10 post) ditambah 10 (untuk ambil category tiap post) sama dengan 11 query. Padahal sebenarnya bisa hanya 2 query saja.
Solusinya adalah eager loading dengan method with(). Kamu memberi tahu Laravel sejak awal, ambil post sekaligus dengan relasi category-nya dalam satu waktu. Laravel akan jalankan 2 query, query pertama ambil 10 post, query kedua ambil semua category yang dibutuhkan sekaligus. Hasilnya sama, tapi jumlah query turun dari 11 menjadi 2. Untuk data yang lebih besar, penghematannya makin terasa.
Analoginya seperti belanja bahan masak. N+1 query itu seperti kamu bolak-balik ke warung 11 kali untuk membeli 11 bahan, tiap bahan satu perjalanan. Capek, boros waktu, dan boros bensin. Eager loading itu seperti kamu sekali jalan bawa keranjang besar, semua 11 bahan diambil sekaligus dalam satu kunjungan. Jauh lebih efisien.
Berikut perbandingan kode yang salah (N+1) dan kode yang benar (eager loading).
// SALAH: N+1 query, 11 query untuk 10 post
$posts = BlogPost::latestFirst()->paginate(9);
foreach ($posts as $post) {
echo $post->category->name; // Query baru tiap loop!
}
// BENAR: Eager loading, hanya 2 query
$posts = BlogPost::with(['category', 'featuredMedia'])
->latestFirst()
->paginate(9);
foreach ($posts as $post) {
echo $post->category->name; // Sudah di-load, tidak ada query baru
}
Perhatikan bahwa with(['category', 'featuredMedia']) memuat relasi category dan featuredMedia sekaligus. Kalau di view kamu juga menampilkan gambar featured, relasi itu juga perlu di-eager load. Kuncinya, periksa view kamu dan cari semua relasi yang dipanggil di dalam loop, lalu masukkan semuanya ke dalam with(). Untuk memastikan tidak ada N+1 query yang lolos, kamu bisa pakai Laravel Debugbar atau Laravel Telescope saat development, kedua tool ini akan menampilkan jumlah query yang dijalankan tiap halaman.
Kiat 5: Memoize Method Getter per-Request
Kiat ini agak lebih teknis tapi sangat berguna. Kadang kamu punya method getter yang dipanggil berulang kali dalam satu request.
Misalnya, method getUserCourse() dipanggil 5 kali di view untuk mengecek apakah user sudah ikut course tertentu. Tanpa memoize, setiap pemanggilan method ini akan menjalankan query ke database. Jadi 5 kali panggil berarti 5 kali query, padahal hasilnya sama semua karena dalam satu request data user tidak berubah.
Memoize adalah teknik menyimpan hasil method di memori saat pertama kali dipanggil, lalu pemanggilan berikutnya cukup baca dari memori tanpa query ulang. Dengan begini, query database hanya jalan sekali, yaitu saat pemanggilan pertama. Empat pemanggilan berikutnya langsung dapat hasil dari cache di memori.
Analoginya seperti menanyakan jam ke orang yang sama 5 kali berturut-turut. Orang itu akan menjawab 5 kali, padahal jawabannya sama semua dan dia capek. Memoize itu seperti kamu mencatat jamnya sekali di kertas, lalu membaca kertas itu 5 kali. Orangnya tidak diganggu lagi, dan kamu tetap dapat jawaban yang sama.
Berikut implementasinya dengan operator ??= di PHP.
class User extends Model
{
protected array $userCourseMemo = [];
public function getUserCourse(Course $course): ?UserCourse
{
return $this->userCourseMemo[$course->id] ??= $this->userCourses()
->where('course_id', $course->id)
->first();
}
}
Operator ??= artinya “kalau belum ada, isi dengan hasil query. Kalau sudah ada, pakai yang sudah ada.” Jadi saat method ini dipanggil pertama kali, $this->userCourseMemo[$course->id] masih kosong, sehingga query dijalankan dan hasilnya disimpan di array $userCourseMemo. Saat dipanggil kedua kali dengan course yang sama, nilai sudah ada di array, jadi query tidak dijalankan lagi. Hasilnya langsung dikembalikan dari memori.
Catatan penting, property
$userCourseMemohanya hidup selama satu request. Saat request selesai, object User di-destroy dan memori dibersihkan. Jadi ini aman, tidak akan menyebabkan data basi atau memory leak. Memoize per-request ini cocok untuk method getter yang dipanggil berulang di satu halaman, terutama di view yang punya loop atau komponen yang render berkali-kali.
Kiat 6: Cache Data Semi-Statis
Bayangkan kamu punya warung makan yang menempel daftar menu di dinding. Daftar menu itu tidak berubah setiap hari, kan?
Mau pelanggan pertama atau pelanggan ke-50, menu yang dibaca tetap sama. Jadi kenapa harus mencetak ulang daftar menu setiap kali ada pelanggan masuk? Cukup cetak sekali di pagi hari, lalu tempel di dinding. Kalau ada menu baru atau harga berubah, baru cetak ulang.
Prinsip yang sama berlaku untuk data semi-statis di aplikasi Laravel kamu. Data seperti statistik dashboard, opsi dropdown, daftar kategori, atau daftar blog post yang sudah dipublish itu jarang berubah. Tapi kalau kamu query database setiap kali halaman dibuka, server harus bekerja ulang dari nol. Padahal hasilnya kemungkinan besar sama dengan load sebelumnya. Solusinya adalah menyimpan hasil query ke cache dengan TTL atau time-to-live, yaitu masa berlaku cache.
Contoh paling umum adalah halaman index blog. Data postingan yang sudah dipublish tidak akan berubah setiap detik. Jadi kita bisa cache hasil query selama satu jam. Dalam satu jam itu, berapa pun pengunjung yang buka halaman, database tidak disentuh sama sekali. Semua dilayani dari cache.
$data = Cache::tags(['blog'])->remember('blog.index', 3600, function () {
return BlogPost::published()->with(['category', 'featuredMedia'])->paginate(9);
});
Mari kita bedah kode di atas. Method remember artinya Laravel akan cek dulu apakah cache dengan key blog.index sudah ada. Kalau ada, langsung pakai. Kalau tidak ada, jalankan closure di dalamnya, simpan hasilnya ke cache, lalu kembalikan. Angka 3600 adalah TTL dalam detik, yang berarti 3600 detik sama dengan satu jam. Setelah satu jam, cache otomatis kedaluwarsa dan query dijalankan ulang.
Lalu apa itu Cache::tags(['blog'])? Tags adalah cara Laravel untuk mengelompokkan cache yang saling terkait. Bayangkan tags seperti label folder. Semua cache yang berkaitan dengan blog diberi label blog. Nanti kalau ada perubahan data blog, kamu bisa hapus semua cache bertag blog sekaligus dengan satu perintah flush(). Tidak perlu hapus satu per satu.
Tapi bagaimana supaya cache otomatis dihapus saat ada postingan baru atau postingan diedit? Di sinilah Model Observer berperan. Observer akan memantau setiap kali model BlogPost disimpan atau dihapus, lalu otomatis membersihkan cache.
class BlogPostObserver
{
public function saved(BlogPost $post): void
{
Cache::tags(['blog'])->flush();
}
public function deleted(BlogPost $post): void
{
Cache::tags(['blog'])->flush();
}
}
Dengan observer ini, setiap kali kamu menambah, mengedit, atau menghapus blog post, cache bertag blog langsung dibersihkan. Load berikutnya akan membuat cache baru dengan data terbaru. Kamu tidak perlu khawatir pengunjung melihat data lama.
Satu hal penting yang perlu diingat. Untuk data yang bersifat per-user, seperti dashboard member atau riwayat pesanan pribadi, key cache harus unik per user. Jangan pakai key dashboard untuk semua user, karena nanti user A melihat data user B. Pakai key seperti dashboard.user.{$userId} supaya cache tiap user terpisah.
Kiat 7: Pakai Redis untuk Cache, Session, dan Queue
Bayangkan kamu punya toko kelontong. Gudang tempat menyimpan semua barang ada di belakang toko, jauh dari kasir. Setiap ada pelanggan beli sesuatu, kamu harus jalan ke gudang, cari barang, bawa ke depan, baru kasih ke pelanggan. Capek dan lama, kan? Itu ibarat database yang simpan data di disk atau hard disk. Aksesnya lambat karena harus baca dari hardware fisik.
Sekarang bayangkan kamu punya rak display kecil di depan kasir. Barang yang sering dibeli pelanggan kamu taruh di rak itu. Kalau pelanggan minta barang yang ada di rak, kamu tinggal ambil, langsung selesai. Cepat sekali. Itulah Redis. Redis menyimpan data di RAM atau memory, bukan di disk. Karena RAM jauh lebih cepat dari hard disk, akses ke Redis bisa 1000 kali lebih cepat dari akses ke database.
Redis bisa dipakai untuk tiga hal sekaligus di Laravel, yaitu cache, session, dan queue. Cache untuk menyimpan hasil query seperti di Kiat 6. Session untuk menyimpan data login pengguna. Queue untuk antrian job background seperti kirim email. Kalau ketiganya pakai Redis, aplikasi kamu akan terasa jauh lebih ringan dan responsif.
Cara mengaktifkannya sangat mudah. Cukup ubah konfigurasi di file .env Laravel kamu.
CACHE_STORE=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
Setelah itu, pastikan Redis server sudah terinstall dan berjalan di server kamu. Kalau pakai Laravel Sail atau Docker, Redis biasanya sudah include. Kalau di VPS, install dengan sudo apt-get install redis-server.
Salah satu fitur Redis yang sangat berguna adalah maxmemory policy. Karena RAM itu terbatas dan relatif mahal, kamu tidak bisa menyimpan semua data di Redis. Kalau memory penuh, Redis perlu tahu apa yang harus dibuang. Dengan policy allkeys-lru, Redis akan otomatis membuang cache yang paling lama tidak diakses atau least recently used. Jadi cache yang sering dipakai tetap aman, yang jarang dipakai otomatis dibuang. Kamu bisa set ini di file redis.conf.
maxmemory 256mb
maxmemory-policy allkeys-lru
Dengan konfigurasi ini, Redis akan bekerja seperti rak display yang otomatis merapikan diri. Barang yang tidak laku lama-lama dibuang, barang yang laku ditaruh depan. Kamu tidak perlu mikir soal memory management, Redis yang urus.
Kiat 8: Aktifkan HTTP/2 di nginx
Bayangkan kamu sedang mengantar paket lewat jalan. HTTP/1.1 itu seperti jalan satu lajur. Mobil harus antri, satu per satu. Walaupun jalan lurus dan mulus, tetap saja pelan karena tidak bisa saling mendahului. Browser yang pakai HTTP/1.1 maksimal hanya bisa buka 6 koneksi paralel per domain. Artinya kalau halaman kamu punya 30 file CSS dan JS, browser harus antri untuk download semua file itu.
HTTP/2 itu seperti jalan tol banyak lajur. Semua mobil bisa jalan bersamaan dalam satu gerbang tol. HTTP/2 punya fitur multiplexing, yaitu semua file CSS, JS, dan gambar bisa diunduh paralel dalam satu koneksi TCP. Tidak perlu antri, tidak perlu buka koneksi baru untuk setiap file. Hasilnya, halaman web terasa jauh lebih cepat dimuat.
Ini sangat terasa kalau kamu pakai Filament atau Laravel Nova yang load 20 sampai 30 file per halaman. Dengan HTTP/1.1, browser harus antri download semua file itu. Dengan HTTP/2, semua diunduh bersamaan. Bedanya bukan cuma beberapa milidetik, bisa sampai beberapa detik.
Cara mengaktifkan HTTP/2 di nginx sangat mudah. Cukup tambahkan kata http2 di direktif listen. File konfigurasi nginx biasanya ada di /etc/nginx/sites-available/namasitus.
# SEBELUM (HTTP/1.1)
listen 443 ssl;
# SESUDAH (HTTP/2)
listen 443 ssl http2;
Setelah mengubah konfigurasi, jangan lupa test dulu lalu reload nginx.
sudo nginx -t
sudo systemctl reload nginx
Pastikan juga kamu sudah pakai HTTPS atau SSL, karena mayoritas browser hanya mendukung HTTP/2 lewat koneksi HTTPS. Kalau kamu belum pakai SSL, aktifkan dulu dengan Let’s Encrypt yang gratis.
Kiat 9: Kompresi Brotli dan gzip
Bayangkan kamu mau kirim paket berisi baju ke teman. Tanpa kompresi, kamu masukkan baju apa adanya ke kotak besar. Banyak ruang kosong, kotak jadi besar, ongkos kirim mahal. Itu ibarat server mengirim file CSS dan JS tanpa kompresi, ukuran penuh, banyak karakter kosong dan spasi yang sebenarnya tidak perlu.
gzip itu seperti kamu lipat baju dulu sebelum masuk kotak. Kotak jadi lebih kecil, ongkos kirim lebih murah. gzip bisa kompres file teks sekitar 60 sampai 70 persen. Lalu brotli itu seperti kamu pakai mesin vakum untuk menyedot udara dari kantong baju. Hasilnya lebih kecil lagi, bisa kompres 70 sampai 80 persen. Brotli memang dirancang khusus untuk file teks seperti HTML, CSS, dan JS, jadi lebih efisien dari gzip.
Untuk mengaktifkan gzip di nginx, tambahkan baris berikut di file nginx.conf atau di blok server kamu.
gzip on;
gzip_vary on;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript
text/xml application/xml image/svg+xml;
Untuk brotli, kamu perlu install modul tambahan dulu karena brotli tidak default di nginx.
sudo apt-get install -y libnginx-mod-http-brotli-filter libnginx-mod-http-brotli-static
Setelah terinstall, tambahkan konfigurasi brotli di nginx.
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/json application/javascript
text/xml application/xml image/svg+xml;
gzip_comp_level 6 dan brotli_comp_level 6 adalah tingkat kompresi. Angka 6 adalah titik manis atau sweet spot, balance antara ukuran file kecil dan beban CPU server. Kalau terlalu tinggi, misalnya 9, file memang lebih kecil tapi CPU kerja lebih berat. Kalau terlalu rendah, kompresi tidak optimal. Angka 6 sudah cukup untuk mayoritas kasus.
Setelah selesai, test dan reload nginx seperti biasa.
sudo nginx -t
sudo systemctl reload nginx
Untuk mengecek apakah kompresi aktif, buka situs kamu di browser, klik kanan, pilih Inspect, masuk ke tab Network, lalu refresh halaman. Klik salah satu file CSS atau JS, lihat header Content-Encoding. Kalau isinya gzip atau br, berarti kompresi sudah aktif.
Kiat 10: Cache Header untuk Static Asset
Bayangkan kamu punya buku catatan yang versinya selalu dicetak di cover. Kalau cover tulisannya “Edisi 2024”, kamu tahu isinya pasti sama selama cover tidak berubah. Kamu tidak perlu buka ulang bukunya untuk cek isinya. Tapi kalau cover berubah jadi “Edisi 2025”, baru kamu tahu isinya sudah berbeda dan perlu baca ulang.
Prinsip inilah yang dipakai Laravel untuk static asset seperti CSS, JS, gambar, dan font. Saat kamu jalankan npm run build, Vite akan memberi nama file dengan hash unik, misalnya app-BfpX1doZ.js. Hash itu dihasilkan dari isi file. Kalau isi file berubah, hash juga berubah, jadi nama file ikut berubah. Kalau isi tidak berubah, nama file tetap sama.
Karena nama file sudah mencerminkan isi, kita bisa aman menyuruh browser cache file itu selama satu tahun penuh. Browser tidak perlu re-fetch atau minta ulang ke server setiap kali buka halaman. File langsung diambil dari cache lokal di komputer pengguna. Ini sangat menghemat bandwidth dan mempercepat load page untuk pengunjung yang sudah pernah buka situs kamu sebelumnya.
Konfigurasi nginx untuk cache static asset cukup sederhana.
location ~* \.(js|css|woff2?|ttf|eot|png|jpe?g|gif|webp|svg|ico|avif)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
Mari kita bedah baris per baris. expires 1y artinya browser akan menyimpan file selama satu tahun. Cache-Control "public, immutable" memberi tahu browser dan CDN bahwa file ini boleh di-cache oleh siapa saja, dan immutable adalah kata kunci penting di sini.
immutable artinya “tidak berubah”. Dengan flag ini, browser tidak akan mengecek ETag atau Last-Modified ke server. Browser langsung pakai versi yang ada di cache tanpa pertanyaan. Ini sangat efisien karena menghilangkan request validasi yang sebenarnya tidak perlu, karena nama file sudah berisi hash yang berubah kalau konten berubah.
access_log off berguna untuk mengurangi ukuran log nginx. Karena request asset statis ini sangat sering dan selalu sama, tidak ada gunanya mencatatnya di access log. Log jadi lebih kecil, disk lebih hemat.
Yang perlu diingat, strategi cache ini hanya aman untuk file yang sudah di-hash. Jangan pakai immutable untuk file yang namanya tetap seperti index.html atau favicon.ico tanpa hash, karena kamu tidak bisa memaksa browser memuat versi baru kalau isi berubah. Untuk file tanpa hash, gunakan Cache-Control "public, max-age=3600" dengan TTL yang lebih pendek, atau pakai must-revalidate supaya browser tetap cek ke server.
Catatan untuk pemula. Kombinasi Kiat 8, 9, dan 10 ini adalah perbaikan di sisi server web atau nginx, bukan di kode Laravel. Tapi dampaknya ke aplikasi Laravel kamu sangat besar, terutama kalau pakai Filament atau panel admin yang load banyak file. Tiga kiat ini “gratis” dalam artian kamu tidak perlu ubah kode sama sekali, cukup konfigurasi nginx. Hasilnya bisa mempercepat load halaman sampai 50 persen.
Kiat 11: Vite Asset Bundling dan Code Splitting
Laravel sekarang pakai Vite sebagai default bundler untuk JavaScript dan CSS. Tugas Vite menyatukan semua file asset kamu jadi satu paket kecil yang siap dikirim ke browser. Tapi banyak pemula yang salah kaprah, mereka tetap pakai mode development saat website sudah live di server production.
Mode development itu npm run dev. Mode ini tidak mengoptimasi apa pun, malah sering memuat ulang seluruh modul setiap kali ada perubahan. Cocok untuk kerja di laptop sendiri, tapi bencana kalau dipakai di server yang diakses pengunjung. Yang harus kamu pakai di server production adalah npm run build. Perintah ini mengompres, memangkas kode tidak terpakai, dan menghasilkan file siap pakai yang ukurannya jauh lebih kecil.
Bayangkan kamu sedang packing koper untuk traveling. Mode development itu seperti bawa semua pakaian untuk semua musim, jaket tebal, baju pantai, selimut, payung, padahal kamu cuma pergi ke Bali tiga hari. Berat banget kan? Mode production build itu kamu cuma bawa yang benar-benar perlu untuk trip ini, dua kaos, satu celana pendek, satu baju renang. Ringan dan cukup.
Nah, code splitting itu satu level lebih pintar lagi. Kamu pisah koper per aktivitas. Koper pantai hanya dibuka kalau kamu ke pantai. Koper dinner baru dibuka kalau mau makan malam. Di Laravel dengan Vite, ini berarti JavaScript untuk halaman dashboard tidak ikut dimuat saat pengunjung membuka halaman blog. Browser hanya mengunduh script yang dibutuhkan halaman itu saja.
File konfigurasi Vite ada di vite.config.js di root project kamu. Pastikan file ini ter-setup dengan benar dan jalankan build production sebelum deploy.
# Jalankan di server production, BUKAN npm run dev
npm run build
Setelah build selesai, Vite menghasilkan file manifest di folder public/build. File manifest ini penting karena Laravel pakai itu untuk tahu file mana yang harus diload di setiap halaman. Jadi jangan hapus folder build ini setelah proses build selesai.
Kiat 12: OPcache Config Sesuai Jumlah File PHP
Setiap kali ada request masuk ke server, PHP harus membaca file .php kamu, mengompilasi menjadi bytecode, baru kemudian mengeksekusi. Proses compile ini dilakukan berulang-ulang untuk setiap request, padahal kode PHP kamu tidak berubah dari menit ke menit. OPcache itu fitur PHP yang menyimpan hasil compile di memory, jadi request berikutnya langsung pakai yang sudah jadi, tidak perlu compile ulang.
Bayangkan ada koki yang tiap menerima pesanan harus membuka buku resep, baca satu per satu, baru masak. Itu koki tanpa OPcache. Koki dengan OPcache sudah hafal resep di luar kepala, jadi begitu pesanan masuk langsung masak. Jauh lebih cepat kan?
Tapi ada jebakannya. OPcache punya kapasitas hafalan terbatas, diatur lewat max_accelerated_files. Default Debian cuma 10.000. Padahal project Laravel biasa saja bisa 8.000 file, kalau kamu pakai Filament atau Livewire, total bisa tembus 13.000 file lebih. Artinya 3.000 file tidak muat di hafalan OPcache, dan untuk file-file itu koki tetap harus baca buku resep. Optimasi tidak maksimal.
Cara cek jumlah file PHP di project kamu adalah dengan jalankan perintah berikut di root project.
find app vendor -name "*.php" | wc -l
Angka yang muncul itulah jumlah file PHP kamu. Setelah itu, edit file konfigurasi OPcache. Biasanya ada di /etc/php/8.2/mods-available/opcache.ini.
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.enable_file_override=On
Penjelasan tiap baris. opcache.memory_consumption=256 memberi OPcache memory 256MB, cukup untuk project menengah. opcache.interned_strings_buffer=16 penting banget kalau kamu pakai Filament atau Livewire karena komponen mereka banyak string literal berulang, dan buffer ini menyimpan string-string tersebut supaya tidak diduplikasi di memory. opcache.max_accelerated_files=20000 harus lebih besar dari jumlah file PHP project kamu, jadi kalau hasil perintah tadi 13.000, set 20.000 supaya ada ruang untuk tumbuh. opcache.enable_file_override=On supaya OPcache langsung dipakai untuk file yang sudah di-cache, tanpa PHP perlu cek stat() di filesystem setiap request, yang memakan waktu.
Setelah edit, restart PHP-FPM supaya perubahan aktif.
Kiat 13: PHP-FPM max_children Sesuai RAM Server
PHP-FPM itu process manager yang menangani request PHP. Setiap request yang masuk ditangani oleh satu worker, yaitu satu proses PHP yang berjalan. Jumlah worker maksimal yang bisa jalan bersamaan diatur lewat pm.max_children. Default Debian cuma 5. Artinya kalau ada 5 request sedang diproses, request ke-6 harus antri sampai salah satu worker selesai.
Bayangkan warung dengan 5 kasir. Kalau ada 5 pelanggan, semua dilayani bersamaan. Pelanggan ke-6 harus antri. Kalau antrian terlalu panjang, pelanggan kesal dan pergi ke warung sebelah. Di dunia website, pengunjung yang pergi itu artinya kamu kehilangan traffic dan potensi pendapatan. Solusinya tambah kasir, tapi jumlah kasir tergantung seberapa luas ruang tokomu. Ruang toko itu RAM server kamu.
Cara hitungnya gampang. Setiap worker PHP untuk project Laravel dengan Filament biasanya makan sekitar 80 sampai 120MB RAM. Anggap rata-rata 100MB per worker. Maka rumusnya adalah RAM tersedia dibagi 100MB sama dengan max_children.
Contoh, kalau server kamu punya 4GB RAM khusus untuk PHP-FPM, maka 4GB dibagi 100MB hasilnya sekitar 30 worker. Kalau 8GB, sekitar 60 worker. Tapi ingat, RAM tidak hanya untuk PHP, ada juga web server, database, dan sistem operasi. Sisakan ruang.
Edit file pool configuration di /etc/php/8.2/fpm/pool.d/www.conf.
pm = dynamic
pm.max_children = 20
pm.start_servers = 6
pm.min_spare_servers = 4
pm.max_spare_servers = 12
pm.max_requests = 1000
Penjelasan singkat. pm = dynamic berarti jumlah worker naik turun sesuai beban. pm.max_children = 20 adalah jumlah maksimal worker, sesuaikan dengan RAM kamu. pm.start_servers = 6 jumlah worker yang langsung jalan saat FPM start. pm.min_spare_servers = 4 dan pm.max_spare_servers = 12 mengatur jumlah worker cadangan yang siap sedia.
Yang paling penting adalah pm.max_requests = 1000, ini membuat setiap worker di-restart setelah menangani 1000 request. Tujuannya untuk mencegah memory leak, karena kadang ada kode atau library yang bocor memory perlahan-lahan, dan recycle worker ini menjaga server tetap sehat dalam jangka panjang.
Kiat 14: Lazy Loading dan Responsive Images
Gambar biasanya jadi komponen terbesar di sebuah halaman web. Kalau kamu load semua gambar sekaligus saat halaman dibuka, pengunjung harus menunggu lama padahal sebagian gambar itu berada di bawah layar dan belum terlihat. Solusinya adalah bedakan cara load gambar di atas fold dan di bawah fold.
Above fold adalah bagian halaman yang langsung terlihat saat halaman pertama kali dibuka, seperti logo, gambar hero, atau judul utama. Gambar-gambar ini harus load eager, artinya langsung dimuat secepat mungkin. Below fold adalah bagian halaman yang baru terlihat setelah pengunjung scroll ke bawah, seperti kartu blog, gambar artikel, atau footer. Gambar-gambar ini boleh load lazy, artinya baru dimuat saat pengunjung mendekat.
Bayangkan toko buku. Buku di etalase depan langsung dipajang rapi karena itu yang pertama dilihat pelanggan. Tapi buku di rak belakang tidak perlu dipajang semua, diambil saja kalau pelanggan mendekat ke rak itu. Tidak masalah kan kalau rak belakang masih kosong saat pelanggan baru masuk? Toh mereka belum sampai situ.
Untuk gambar above fold, pakai loading="eager" dan fetchpriority="high" supaya browser tahu ini prioritas utama.
<!-- Above the fold (eager) -->
<img src="logo.png" loading="eager" fetchpriority="high" decoding="async" width="180" height="50">
<!-- Below the fold (lazy) -->
<img src="card.jpg" loading="lazy" decoding="async" sizes="(max-width: 768px) 100vw, 33vw"
srcset="card-320.webp 320w, card-640.webp 640w, card-1024.webp 1024w">
Perhatikan juga atribut srcset di contoh gambar lazy. Ini adalah responsive images. Browser akan otomatis pilih ukuran gambar yang sesuai dengan layar pengunjung. Kalau pengunjung buka dari HP, browser ambil versi 320px yang kecil. Kalau dari laptop, ambil versi 640px atau 1024px. Tidak ada alasan kirim gambar 1024px ke HP yang layarnya cuma 360px.
Soal format, gunakan WebP kalau bisa. WebP ukurannya 25 sampai 35 persen lebih kecil dari JPG dengan kualitas visual yang sama, dan lebih kecil dari PNG untuk gambar transparan. Laravel bisa generate variant WebP otomatis lewat package seperti Intervention Image atau Spatie Media Library, jadi kamu tidak perlu konversi manual satu per satu.
Kiat 15: Database Indexing untuk Kolom yang Sering Di-Query
Database indexing adalah optimasi yang efeknya paling terasa saat data kamu sudah ribuan baris. Tanpa index, setiap kali kamu query dengan WHERE, ORDER BY, atau JOIN, database harus melakukan full table scan, yaitu membaca seluruh baris di tabel dari atas sampai bawah untuk menemukan yang cocok. Dengan index, database langsung lompat ke baris yang dicari, seperti loncat ke halaman tertentu di buku.
Bayangkan kamu cari kata “optimasi” di kamus tebal 1.000 halaman. Tanpa index, kamu buka halaman 1, baca, tidak ketemu, buka halaman 2, baca, tidak ketemu, dan seterusnya. Bisa seharian. Dengan index, kamu buka daftar isi di belakang, cari kata “optimasi”, lihat halaman 347, lompat langsung ke sana. Selesai dalam hitungan detik.
Di Laravel, kamu tambah index lewat migration. Berikut contoh migration untuk menambah index di tabel blog.
Schema::table('blog_posts', function (Blueprint $table) {
$table->index('slug');
$table->index('blog_category_id');
$table->index(['status', 'published_at']);
});
Aturan mainnya, index kolom yang sering kamu pakai di kondisi WHERE. Kolom yang umum di-index antara lain slug, status, user_id, dan category_id. Kalau kamu sering query dengan kombinasi dua kolom atau lebih, misalnya WHERE status = 'published' AND published_at > '2024-01-01', pakai composite index seperti ['status', 'published_at'] di contoh di atas. Composite index jauh lebih efisien daripada dua index terpisah untuk query kombinasi.
Tapi ingat, jangan over-index. Setiap index yang kamu tambah memperlambat operasi INSERT, UPDATE, dan DELETE karena database harus update index juga setiap kali data berubah. Jadi index hanya kolom yang benar-benar sering di-query, bukan semua kolom. Kalau kamu index 20 kolom di tabel yang jarang dibaca tapi sering ditulis, performa tulis akan anjlok.
Penutup
Lima belas kiat di atas bukan fitur baru yang bikin website kamu tiba-tiba jadi super canggih. Ini adalah utang teknis yang wajar dibayar saat traffic mulai tumbuh. Mulai dari yang paling gampang dulu, seperti aktifkan SPA mode di Livewire, pakai wire:navigate, dan nyalakan gzip di web server. Setelah itu baru lanjut ke yang butuh akses server seperti konfigurasi OPcache dan tuning PHP-FPM. Tidak perlu sekaligus, tapi jangan ditunda terus.
Buat kamu vibecoder pemula yang baru mulai pakai Laravel, pesan saya sederhana. Jangan tunggu traffic ribuan baru mau optimasi. Setup dasar sejak awal, walau cuma gzip, lazy loading, dan database index. Jadi saat traffic datang, website kamu sudah siap menerima, bukan malah tumbang di hadapan pengunjung pertama.
Speed bukan kemewahan, tapi standar. User tidak pernah bilang “wow website ini cepat”, tapi mereka pasti pergi kalau lambat.





No comment yet, add your voice below!