Aplikasi Laravel yang terasa cepat saat pengembangan bisa melambat drastis di produksi. Penyebabnya hampir selalu sama: terlalu banyak query atau query yang berat. Artikel ini fokus pada teknik di sisi Laravel/Eloquent. Untuk sisi database (index, EXPLAIN), baca juga Tips Optimasi Query Database MySQL.
Langkah pertama: lihat query yang berjalan
Kamu tidak bisa memperbaiki apa yang tidak terlihat. Alat bantu:
- Laravel Debugbar (
barryvdh/laravel-debugbar): menampilkan jumlah dan waktu query pada setiap halaman. - Laravel Telescope: memantau query, request, job, dan exception.
DB::listenuntuk mencatat query sendiri:
DB::listen(function ($query) {
if ($query->time > 100) { // lebih dari 100 ms
Log::warning('Query lambat', ['sql' => $query->sql, 'ms' => $query->time]);
}
});
->toSql()dan->explain()pada query builder untuk melihat SQL dan rencana eksekusinya.
Masalah #1: N+1 query
Ini penyebab kelambatan paling umum di Laravel. Contoh:
$posts = Post::all(); // 1 query
foreach ($posts as $post) {
echo $post->author->name; // +1 query PER post
}
Untuk 100 artikel, terjadi 101 query. Penyebabnya: relasi author di-lazy load satu per satu saat diakses.
Solusi: eager loading
$posts = Post::with('author')->get(); // hanya 2 query
Laravel mengambil semua penulis dengan satu query WHERE id IN (...) lalu mencocokkannya di memori. Beberapa variasi:
Post::with(['author', 'categories'])->get(); // beberapa relasi
Post::with('comments.user')->get(); // relasi bersarang
Post::with(['author:id,name'])->get(); // hanya kolom tertentu
Post::with(['comments' => fn ($q) => $q->latest()->limit(5)])->get();
$posts->load('author'); // setelah data terlanjur diambil
Catatan: kolom kunci (id, foreign key) harus ikut dipilih saat memakaiauthor:id,name.
Mencegah N+1 secara otomatis
Di AppServiceProvider::boot():
Model::preventLazyLoading(! app()->isProduction());
Di lingkungan lokal, Laravel akan melempar exception setiap kali terjadi lazy loading yang tidak disengaja, sehingga masalah ketahuan sebelum rilis. (Pada produksi, sebaiknya hanya dicatat ke log.)
Relasi dijelaskan lebih lanjut di Mengenal Eloquent Relationship.
Masalah #2: Mengambil data yang tidak dibutuhkan
// Boros: mengambil semua kolom termasuk teks panjang
$users = User::all();
// Hemat
$users = User::select('id', 'name', 'email')->get();
// Hanya satu kolom
$emails = User::pluck('email');
Dan jangan memuat seluruh data hanya untuk menghitung atau mengecek:
User::count(); // bukan User::all()->count()
Post::where('user_id', 5)->exists(); // bukan ->get()->isNotEmpty()
Menghitung relasi tanpa memuatnya
$posts = Post::withCount('comments')->get();
// akses: $post->comments_count
$users = User::withSum('orders', 'total')->get(); // $user->orders_sum_total
Jauh lebih efisien daripada memanggil $post->comments->count() untuk setiap baris.
Masalah #3: Memproses data besar sekaligus
Memuat jutaan baris ke memori akan menghabiskan RAM. Pecah:
// chunk: ambil per 1000 baris
Pesanan::where('status', 'baru')->chunkById(1000, function ($pesanan) {
foreach ($pesanan as $p) { /* proses */ }
});
// cursor: satu model dalam memori pada satu waktu (memakai generator)
foreach (Pesanan::where('status', 'baru')->cursor() as $p) { /* proses */ }
// lazy: seperti chunk tetapi API-nya aliran (LazyCollection)
Pesanan::lazyById(1000)->each(fn ($p) => ...);
Gunakan chunkById, bukan chunk, jika kamu mengubah kolom yang dipakai untuk penyaringan selama iterasi, agar tidak ada baris yang terlewat.
Untuk perubahan massal, lakukan di database, bukan di PHP:
Pesanan::where('status', 'baru')->update(['status' => 'diproses']); // 1 query
bukan perulangan yang memanggil save() untuk tiap baris.
Masalah #4: Pagination yang berat
paginate()menjalankan queryCOUNT(*)tambahan. Jika kamu tidak butuh total halaman, gunakansimplePaginate()ataucursorPaginate()(paling efisien untuk tabel besar).- Untuk tabel besar dengan banyak filter, pertimbangkan menghitung total secara terpisah atau memakai cache.
Masalah #5: Kurangnya index
Eloquent hanya membuat SQL; kecepatannya tetap ditentukan index di database. Pastikan kolom di where, orderBy, dan foreign key punya index ($table->index([...]) di migration). Lihat Index Database dan Pengaruhnya terhadap Performa.
Masalah #6: Menghitung hal yang sama berulang kali
Caching menyimpan hasil agar tidak dihitung ulang:
$kategori = Cache::remember('kategori.semua', now()->addHour(), function () {
return Kategori::orderBy('nama')->get();
});
Panduan:
- Cache data yang sering dibaca, jarang berubah (menu, kategori, pengaturan, statistik).
- Hapus atau perbarui cache saat data berubah (
Cache::forget('kategori.semua')), atau gunakan cache tags pada driver yang mendukung (Redis). - Gunakan driver cepat seperti Redis di produksi, bukan
file. - Aktifkan cache konfigurasi dan route:
php artisan config:cachedanroute:cache.
Masalah #7: Pekerjaan berat di dalam request
Kirim email, olah gambar, atau buat laporan PDF di dalam request membuat pengguna menunggu. Pindahkan ke queue:
ProsesLaporan::dispatch($laporan); // dikerjakan worker di latar belakang
Contoh sebelum-sesudah
Sebelum (halaman daftar pesanan, 1 + 2N query, memuat semua kolom):
$pesanan = Pesanan::all();
foreach ($pesanan as $p) {
echo $p->pelanggan->nama . ' - ' . $p->items->count();
}
Sesudah (3 query, hanya kolom perlu, terpaginasi):
$pesanan = Pesanan::select('id', 'pelanggan_id', 'total', 'created_at')
->with('pelanggan:id,nama')
->withCount('items')
->latest()
->cursorPaginate(25);
Alur kerja optimasi yang disarankan
- Ukur: aktifkan Debugbar/Telescope, catat jumlah query dan waktu halaman bermasalah.
- Cari pola: apakah N+1? kolom berlebih? query tanpa index?
- Perbaiki satu hal pada satu waktu.
- Ukur lagi untuk memastikan ada peningkatan nyata.
- Cegah regresi: aktifkan
preventLazyLoading, tulis test untuk halaman kritis (misalnya assert jumlah query).
Kesimpulan
Mayoritas masalah performa Laravel berasal dari N+1, pengambilan data berlebih, dan ketiadaan index. Gunakan eager loading, select/withCount, chunk/cursor untuk data besar, cache untuk data berulang, dan queue untuk pekerjaan berat, dan selalu ukur sebelum dan sesudah mengubah.
Komentar (0)
Masuk / Daftar