All articles
PerformanceOptimizationPHPBackendMySQL

Why Your App Feels Slow (And How I Actually Fix It)

PKParth Kanasagra
9 min read

My performance process always starts the same way: I stop guessing and measure first. The number of times I have seen someone add Redis caching to a route that was slow because of a missing index is painful. The caching helped slightly, but the index would have solved it completely.

Step 1: Measure Before You Touch Anything

You need numbers before you touch any code. I use three tools:

  • Laravel Telescope or Laravel Debugbar locally — shows every query, cache hit/miss, mail, and job triggered per request
  • slow_query_log in MySQL — catches the worst offenders at the database level
  • Blackfire.io for deep profiling — flame graphs, call stack analysis, memory allocation per function

Most of the time the first two are enough. Telescope will show you a route making 120 queries, and that is your problem right there.

Database Queries: The #1 Culprit

In my experience, 70% of slow app complaints trace back to the database. The most common issues:

  • N+1 queries — fixed with eager loading
  • Missing indexes — run EXPLAIN on slow queries and look for type: ALL
  • ORDER BY without an index — sorting 2 million rows with no index on that column is catastrophic
  • SELECT * on fat tables — selecting 40 columns when you need 4 wastes I/O
-- Run this on any slow query
EXPLAIN SELECT * FROM orders WHERE user_id = 1 ORDER BY created_at DESC LIMIT 10;

-- If you see type=ALL in the result, add:
ALTER TABLE orders ADD INDEX idx_user_created (user_id, created_at DESC);

PHP-FPM: Are You Starving Your Workers?

A misconfigured PHP-FPM pool is a sneaky performance killer. If your pool has too few workers, concurrent requests queue up and wait. Check and tune:

# /etc/php/8.4/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 40    # (available_RAM - OS_overhead) / avg_PHP_memory
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 500   # Restart worker after N requests — prevents memory leaks

OPcache Is Non-Negotiable

PHP parses and compiles your source files on every request without OPcache. Never run without it in production:

; /etc/php/8.4/fpm/conf.d/10-opcache.ini
opcache.enable = 1
opcache.memory_consumption = 256
opcache.max_accelerated_files = 20000
opcache.revalidate_freq = 0
opcache.validate_timestamps = 0
opcache.jit = tracing
opcache.jit_buffer_size = 128M

Application-Level Caching

// Cache expensive aggregations, not individual lookups
Cache::remember('dashboard:stats', 600, function () {
    return [
        'total_users'    => User::count(),
        'revenue_today'  => Order::today()->sum('amount'),
        'active_sessions'=> Session::active()->count(),
    ];
});

HTTP-Level Quick Wins

Enable gzip in Nginx — reduces JSON response sizes by 60-80%:

gzip on;
gzip_types application/json text/html text/css application/javascript;
gzip_min_length 1000;
gzip_comp_level 6;

The Priority Order

  1. Fix N+1 queries and add missing database indexes (highest impact)
  2. Enable and tune OPcache
  3. Add application-level caching for expensive queries
  4. Tune PHP-FPM worker count
  5. Enable HTTP response compression
  6. CDN for static assets
  7. Read replicas when you are genuinely I/O bound

Most apps only need steps 1 through 4. Steps 6 and 7 are for when you have genuinely exhausted the earlier ones.

Related Articles

PK

Parth Kanasagra

Certified Senior Laravel & PHP Developer · 9+ years experience · Available for hire