Langsung ke konten
KamusNgoding
Mahir Laravel 4 menit baca

Optimasi Performa Laravel: Membasmi N+1 Query dan Implementasi Redis Cache

#laravel #performance #n-plus-1 #eager-loading #redis #cache #query-optimization #advanced
📚

Baca dulu sebelum ini:

Pendahuluan

“API saya cepat di local, tapi lambat banget di production. Padahal VPS-nya sudah cukup kuat.”

Ini keluhan yang sangat sering muncul dari developer Indonesia. Penyebab #1-nya hampir selalu sama: N+1 Query Problem — aplikasi diam-diam mengirim puluhan bahkan ratusan query ke database setiap kali halaman dibuka, tanpa disadari.

Artikel ini bukan teori — ini adalah panduan praktis debugging dan optimasi yang akan mengubah aplikasi Laravel-mu dari lambat menjadi cepat:

Sebelum optimasi: 230ms response time, 47 queries per request
Setelah optimasi:  12ms response time,  3 queries per request

Apa itu N+1 Query Problem?

N+1 adalah kondisi di mana untuk menampilkan N item, aplikasi melakukan N+1 query database — 1 query untuk mengambil semua item, lalu 1 query lagi per item untuk data relasi.

// 🚩 KODE BERMASALAH ini menghasilkan N+1 queries:
$tasks = Task::all(); // Query #1: ambil semua task

foreach ($tasks as $task) {
    echo $task->user->name; // Query #2, #3, #4... (1 query per task!)
}

// Jika ada 50 task → 51 queries! (1 + 50)
// Jika ada 100 task → 101 queries!

Di local dengan 5–10 data, ini tidak terasa. Di production dengan ribuan data, ini yang membuat loading 5 detik.


Langkah 1: Deteksi N+1 dengan Laravel Debugbar

composer require barryvdh/laravel-debugbar --dev

Debugbar muncul otomatis di bawah halaman saat APP_DEBUG=true. Klik tab Queries — kamu akan melihat semua SQL query yang dieksekusi beserta durasinya.

Tanda-tanda N+1:

  • Ada banyak query dengan struktur SELECT * FROM users WHERE id = ?
  • Query yang sama berulang dengan nilai yang berbeda

Deteksi Manual di Kode

// Aktifkan query log
\DB::enableQueryLog();

// Jalankan kode yang ingin diperiksa
$tasks = Task::all();
foreach ($tasks as $task) {
    $task->user->name;
}

// Lihat semua query yang dieksekusi
dd(\DB::getQueryLog());

Langkah 2: Eager Loading — Solusi N+1

Eager loading adalah cara memberitahu Eloquent untuk mengambil data relasi sekaligus dalam satu query tambahan, bukan satu query per item.

// ✅ DENGAN eager loading: hanya 2 queries total
$tasks = Task::with('user')->get();
// Query 1: SELECT * FROM tasks
// Query 2: SELECT * FROM users WHERE id IN (1, 2, 3, ...)

foreach ($tasks as $task) {
    echo $task->user->name; // Tidak ada query tambahan!
}

Variasi Eager Loading

// Eager load beberapa relasi sekaligus
$tasks = Task::with(['user', 'comments', 'tags'])->get();

// Nested eager loading
$tasks = Task::with('comments.user')->get();
// Ambil task + comments + user dari setiap comment

// Eager load dengan kondisi
$tasks = Task::with(['comments' => function($query) {
    $query->where('approved', true)->latest()->limit(3);
}])->get();

// withCount — hanya ambil jumlah, bukan data relasi
$tasks = Task::withCount('comments')->get();
// Menambah kolom 'comments_count' pada setiap task
echo $task->comments_count; // Tidak ada query tambahan

// withSum, withAvg, withMax, withMin
$users = User::withSum('orders', 'total_amount')->get();
echo $user->orders_sum_total_amount;

Eager Loading di Controller

// app/Http/Controllers/TaskController.php

public function index()
{
    $tasks = Task::query()
        ->with(['user:id,name,email', 'comments']) // Select kolom spesifik dari relasi
        ->withCount('comments')
        ->where('user_id', auth()->id())
        ->latest()
        ->paginate(20);

    return view('tasks.index', compact('tasks'));
}

Langkah 3: Mencegah N+1 Secara Otomatis

Tambahkan di AppServiceProvider untuk mendeteksi N+1 saat development:

<?php
// app/Providers/AppServiceProvider.php

use Illuminate\Database\Eloquent\Model;

public function boot(): void
{
    // Hanya aktif saat development — throw exception jika ada lazy loading
    Model::preventLazyLoading(! app()->isProduction());

    // Opsi tambahan: juga cegah akses atribut yang tidak ada
    Model::preventAccessingMissingAttributes(! app()->isProduction());
}

Sekarang jika ada kode yang melakukan lazy loading (N+1), Laravel akan melempar exception dengan pesan jelas:

Illuminate\Database\LazyLoadingViolationException:
Attempted to lazy load [user] on model [App\Models\Task]
but lazy loading is disabled.

Ini memaksamu memperbaiki N+1 sebelum sampai ke production!


Langkah 4: Query Optimization Lanjutan

Select Hanya Kolom yang Dibutuhkan

// ❌ Ambil semua kolom (termasuk kolom besar yang tidak dipakai)
$tasks = Task::all();

// ✅ Ambil hanya yang dibutuhkan
$tasks = Task::select('id', 'title', 'status', 'created_at')->get();

// Untuk relasi:
$tasks = Task::with('user:id,name')->select('id', 'title', 'user_id')->get();
// Pastikan foreign key (user_id) selalu ada dalam select!

Chunk untuk Data Besar

// ❌ Memuat semua data ke memory sekaligus — bisa OOM!
$tasks = Task::all(); // Jika 100.000 records → habis RAM

// ✅ Proses 500 data per batch
Task::chunk(500, function ($tasks) {
    foreach ($tasks as $task) {
        // Proses setiap task
    }
});

// ✅ lazy() — bahkan lebih efisien (generator)
foreach (Task::lazy() as $task) {
    // Proses satu per satu tanpa load semua ke memory
}

Index Database untuk Query Cepat

// database/migrations/xxxx_add_index_to_tasks_table.php

public function up(): void
{
    Schema::table('tasks', function (Blueprint $table) {
        // Index untuk kolom yang sering di-filter
        $table->index('user_id');
        $table->index('status');
        $table->index('created_at');

        // Composite index untuk query yang sering filter + sort
        $table->index(['user_id', 'status', 'created_at']);
    });
}

Langkah 5: Redis Cache — Simpan Hasil Query

Setelah query dioptimalkan, langkah berikutnya adalah cache hasil agar query tidak perlu dieksekusi berulang.

Setup Redis

# Install Redis
sudo apt install redis-server

# Install package
composer require predis/predis
# .env
CACHE_DRIVER=redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

Pola Cache Dasar

use Illuminate\Support\Facades\Cache;

// Cache::remember — ambil dari cache, jika tidak ada, jalankan callback dan cache hasilnya
$tasks = Cache::remember('user:' . auth()->id() . ':tasks', now()->addMinutes(10), function () {
    return Task::with('user')
        ->where('user_id', auth()->id())
        ->latest()
        ->get();
});

// Cache forever (sampai manual di-clear)
$config = Cache::rememberForever('app:config', function () {
    return Setting::all()->pluck('value', 'key');
});

// Hapus cache secara manual
Cache::forget('user:' . $userId . ':tasks');

// Hapus banyak cache sekaligus dengan tags
Cache::tags(['user:' . $userId])->flush();

Auto-Invalidate Cache dengan Observer

<?php
// app/Observers/TaskObserver.php

namespace App\Observers;

use App\Models\Task;
use Illuminate\Support\Facades\Cache;

class TaskObserver
{
    // Bersihkan cache saat task dibuat/diupdate/dihapus
    public function created(Task $task): void
    {
        $this->clearCache($task->user_id);
    }

    public function updated(Task $task): void
    {
        $this->clearCache($task->user_id);
    }

    public function deleted(Task $task): void
    {
        $this->clearCache($task->user_id);
    }

    private function clearCache(int $userId): void
    {
        Cache::forget("user:{$userId}:tasks");
    }
}
// app/Providers/AppServiceProvider.php

use App\Models\Task;
use App\Observers\TaskObserver;

public function boot(): void
{
    Task::observe(TaskObserver::class);
}

Sekarang cache otomatis dibersihkan setiap kali ada perubahan data — tidak perlu ingat-ingat manual!


Langkah 6: Cache Config, Route, dan View

# Cache semua konfigurasi (config/*.php) — sangat disarankan di production
php artisan config:cache

# Cache route definitions
php artisan route:cache

# Pre-compile semua Blade views
php artisan view:cache

# Cache event listeners
php artisan event:cache

# Jalankan semua sekaligus
php artisan optimize

# Hapus semua cache (saat development)
php artisan optimize:clear

Kapan jalankan php artisan optimize?

Penting: Setelah config:cache, perubahan di .env tidak langsung terbaca. Harus jalankan php artisan config:clear dulu, edit .env, lalu php artisan config:cache lagi.


Contoh Lengkap: API Endpoint Optimization

Sebelum Optimasi

// ❌ Endpoint yang lambat
public function index()
{
    $tasks = Task::all(); // SELECT * FROM tasks (semua kolom!)

    return response()->json(
        $tasks->map(fn($task) => [
            'id'     => $task->id,
            'title'  => $task->title,
            'author' => $task->user->name, // N+1 query!
        ])
    );
}
// Hasil: 230ms, 47 queries

Setelah Optimasi

// ✅ Endpoint yang cepat
public function index()
{
    $tasks = Cache::remember('tasks:all', now()->addMinutes(5), function () {
        return Task::select('id', 'title', 'user_id')   // Hanya kolom yang diperlukan
            ->with('user:id,name')                        // Eager loading spesifik
            ->whereNull('deleted_at')
            ->latest('created_at')
            ->get();
    });

    return response()->json(
        $tasks->map(fn($task) => [
            'id'     => $task->id,
            'title'  => $task->title,
            'author' => $task->user->name,
        ])
    );
}
// Hasil: 12ms, 3 queries (2 DB + 1 cache)

Troubleshooting: Error yang Sering Muncul

Cache tidak ter-update setelah data berubah

Penyebab: Lupa menghapus cache saat data berubah, atau Observer tidak terdaftar.

Solusi:

// Cek apakah Observer terdaftar di AppServiceProvider
Task::observe(TaskObserver::class); // ← Harus ada ini

// Clear cache manual untuk debugging
php artisan cache:clear

// Atau hanya clear key tertentu
Cache::forget('user:1:tasks');

Redis: NOAUTH Authentication required

Penyebab: Redis dikonfigurasi dengan password tapi di .env tidak diset.

Solusi:

# .env
REDIS_PASSWORD=your_redis_password

# Atau jika Redis tidak punya password
REDIS_PASSWORD=null
# Cek konfigurasi Redis
redis-cli CONFIG GET requirepass

php artisan config:cache setelah itu .env tidak terbaca

Penyebab: Setelah config:cache, Laravel membaca dari file cache, bukan .env langsung.

Solusi:

# JANGAN gunakan env() langsung di kode setelah config:cache
# ❌ env('QUEUE_CONNECTION')    ← tidak akan berkerja!
# ✅ config('queue.default')   ← gunakan ini

# Cara reset saat edit .env:
php artisan config:clear
# Edit .env
php artisan config:cache

Memory exhausted saat proses data besar

Penyebab: Menggunakan Task::all() atau ->get() pada jutaan record.

Solusi:

// Gunakan chunk()
Task::chunk(1000, function ($tasks) {
    foreach ($tasks as $task) {
        // proses...
    }
});

// Atau cursor() untuk operasi yang tidak butuh seluruh batch
foreach (Task::cursor() as $task) {
    // proses satu per satu, sangat hemat memory
}

Pertanyaan yang Sering Diajukan

Kapan harus menggunakan Cache dan kapan tidak perlu?

Gunakan cache untuk data yang jarang berubah tapi sering dibaca: daftar kategori, konfigurasi aplikasi, leaderboard, data statistik. Jangan cache data yang harus selalu real-time (notifikasi baru, saldo terkini) atau data yang berbeda per user (kecuali per-user cache key).

Redis vs Memcached, mana yang lebih cocok untuk Laravel?

Redis lebih direkomendasikan karena mendukung data structures (list, set, sorted set) yang dimanfaatkan oleh Laravel Queues, Broadcasting, dan Rate Limiting. Jika sudah pakai Redis untuk Queue, gunakan Redis juga untuk Cache — satu service, dua fungsi.

Bagaimana cara benchmark performa Laravel?

Gunakan Laravel Telescope untuk monitoring per-request, Debugbar untuk development, atau tools seperti ab (Apache Bench) untuk load testing:

# Simulate 100 concurrent users, 1000 total requests
ab -n 1000 -c 100 https://domain-kamu.com/api/tasks

Apakah eager loading selalu lebih baik dari lazy loading?

Tidak selalu. Jika kamu hanya butuh relasi untuk sebagian kecil item dalam koleksi, eager loading justru memuat data yang tidak terpakai. Contoh: menampilkan 100 task tapi hanya 5 yang akan diklik detail — tidak perlu eager load semua detail. Gunakan lazy loading selektif atau conditional eager loading berdasarkan kebutuhan.


Kesimpulan

Optimasi performa bukan sihir — ini systematic debugging dengan tools yang tepat:

  1. Deteksi dengan Debugbar atau preventLazyLoading()
  2. Perbaiki N+1 dengan with() eager loading
  3. Cache hasil query yang sering dipanggil
  4. Optimalkan struktur query (select spesifik, index database)

Dari 230ms menjadi 12ms — perbedaan ini nyata dirasakan user dan berpengaruh langsung pada SEO (Core Web Vitals) serta konversi.

Selanjutnya, pelajari Testing di Laravel untuk memastikan optimasi yang kamu lakukan tidak merusak fungsionalitas yang sudah ada — testing adalah jaring pengaman setiap perubahan!

Kamu sudah berpikir seperti senior Laravel developer. Terus pertajam kemampuanmu! 🚀

Artikel Terkait