Skip to content
ExponaBLOG
PHP & LARAVEL DEVELOPMENT · DAY 057 / 100

PHP performance tuning: a practical guide for developers

Make PHP apps fast in production.

Expona Admin6 min read

Most PHP applications run slower than they need to, not because of bad code, but because fundamental performance optimizations are missing or misconfigured. PHP performance tuning: a practical guide for developers starts with three pillars: enabling the right caching layers, optimizing your framework configuration, and fixing database bottlenecks before you spend hours chasing microseconds in application code.

We've seen teams reduce response times by 60–70% in a single afternoon by addressing these areas systematically. This guide walks through what actually moves the needle in production.

OPcache — Enable and size it correctly — the biggest single win

OPcache is PHP's built-in opcode cache, and it's the single most impactful performance optimization you can enable. Without it, PHP re-parses and recompiles every line of your application on every request. Enabling OPcache alone typically cuts response times by 40–50%.

Check if OPcache is installed and running:

php
php -i | grep opcache

If it's not loaded, add it to your php.ini:

php
zend_extension=opcache.so

; Critical settings for production
opcache.enable=1
opcache.enable_cli=0
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=32768
opcache.validate_timestamps=0
opcache.revalidate_freq=0

The key knob here is memory_consumption. Allocate enough memory to hold your entire codebase without eviction. Too small, and OPcache will discard frequently-used files; too large, and you're wasting memory. For a typical Laravel application with 5,000–10,000 files, 256 MB is a safe starting point.

Set validate_timestamps=0 in production. This prevents PHP from checking disk timestamps on every request, which adds I/O overhead. You'll deploy code changes through container restarts or process reloads anyway, not hot-swapping files.

For development, keep validate_timestamps=1 so code changes take effect immediately without restarting:

php
; php-dev.ini (development)
opcache.validate_timestamps=1
opcache.revalidate_freq=0

Monitor OPcache hit rate in production. Aim for 99%+ cache hits. If your hit rate is below 95%, increase memory_consumption or max_accelerated_files.

Optimize Laravel — config:cache, route:cache, view:cache, event:cache

Laravel's configuration, routes, views, and event listeners are parsed and registered on every request by default. Caching them is non-negotiable in production.

Run these commands as part of your deployment pipeline:

php
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache

Each command generates a single PHP file that replaces dynamic discovery. config:cache merges all config files into bootstrap/cache/config.php, eliminating dozens of file reads. route:cache compiles all routes into an optimized lookup table. view:cache pre-compiles Blade templates to PHP. event:cache caches the event listener manifest.

The impact is measurable. We've seen Laravel applications with dozens of routes and hundreds of configuration values improve baseline request time by 15–25% from caching alone.

Important: after running cache commands, code that dynamically registers routes or listeners won't work. Do this work in your service providers' register() method, not boot(), and ensure dynamic registration happens during cache generation:

php
// In AppServiceProvider.php
public function register(): void
{
    // This runs during cache generation and production requests.
    // Safe to dynamically register routes or listeners here.
}

public function boot(): void
{
    // This doesn't run during cache generation in production.
    // Don't dynamically register things here.
}

If you're using route:cache, define all routes in routes/web.php or routes/api.php, not inside service providers. Clear and regenerate caches during every deployment:

php
php artisan cache:clear
php artisan config:cache
php artisan route:cache

Database first — Fix slow queries and add indexes before micro-tuning

Slow database queries are often the real bottleneck, but they're frequently overlooked while teams optimize PHP code that runs 1 ms. Find and fix database problems first.

Enable the query log in your local or staging environment to see what queries are running:

php
// In config/database.php or your DB connection
'log_queries' => env('DB_LOG_QUERIES', false),
'logging' => [
    'single' => [
        'driver' => 'single',
        'path' => storage_path('logs/queries.log'),
    ],
],

Then monitor slow queries in your database:

php
-- MySQL
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 0.5;

Look for queries without indexes or queries that retrieve columns you don't need. Common culprits:

  • SELECT * instead of specific columns. Larvel's eager loading solves this; use it.
  • Missing indexes on foreign keys and WHERE clauses. Index anything you filter or join on.
  • N+1 queries. Load related data with with(), not inside loops.

Example: a naive query:

php
// Bad: N+1 queries
$orders = Order::all();
foreach ($orders as $order) {
    echo $order->user->name; // One query per order
}

Fixed:

php
// Good: eager loading
$orders = Order::with('user')->get();
foreach ($orders as $order) {
    echo $order->user->name; // Single query
}

Add indexes strategically:

php
Schema::table('orders', function (Blueprint $table) {
    $table->index('user_id');      // Foreign key
    $table->index('status');       // Filtered in WHERE
    $table->index(['created_at', 'status']); // Composite index
});

A single missing index can cause a query to scan 1 million rows instead of 10. Fix that before worrying about PHP micro-optimizations.

Profile — Use Xdebug profiler, Blackfire or Telescope to find hot paths

Profiling pinpoints where your application actually spends time, replacing guesswork with data.

Xdebug profiler (free, built-in):

php
; php.ini
xdebug.profiler_enable=0
xdebug.profiler_enable_trigger=1
xdebug.profiler_output_dir=/var/tmp/xdebug
xdebug.profiler_output_name=cachegrind.out.%t.%R

Trigger profiling on demand by adding XDEBUG_PROFILE=1 to a request header or query string, then analyze the output with KCachegrind or Qcachegrind. You'll see wall-clock time, CPU time, and call counts for every function.

Laravel Telescope (built-in to Laravel):

php
php artisan telescope:install
// Visit /telescope in your app

Telescope shows queries, requests, exceptions, logs, and cache hits in a dashboard. It's lighter than Xdebug and perfect for spotting queries and slow endpoints in development and staging.

Blackfire (commercial, production-grade):

Blackfire profiles your application with minimal overhead, works in production, and integrates with CI/CD. It's worth the cost if you're optimizing for high traffic.

Profile a real-world user flow: add a product to cart, view the cart, complete checkout. Don't optimize random code paths; optimize paths users actually take.

Common mistakes

  • Disabling validation timestamps but caching the wrong thing. If you're caching routes but adding routes dynamically, they won't appear. Test cache generation as part of deployment.
  • Assuming OPcache is always on. Many shared hosting providers or local Docker images don't include it. Verify it's enabled in every environment.
  • Caching expensive queries without invalidation. If you cache user permissions or settings, they'll be stale until cache expires. Use targeted cache invalidation.
  • Optimizing code that runs 100 times per second while ignoring a query that blocks 5 seconds. Profile first. Database wins beat application code wins almost always.
  • Forgetting to clear caches during deployment. Stale cached configs or routes can cause mysterious bugs. Make cache-clearing part of your CI/CD pipeline.

Key takeaways

  • Enable OPcache with appropriate memory sizing (256 MB for typical Laravel apps) and disable timestamp validation in production. This is a 40–50% improvement for free.
  • Cache Laravel configuration, routes, views, and events as part of deployment. These caches eliminate dynamic file discovery and merge operations.
  • Find and fix slow queries and missing indexes before optimizing PHP code. A single database fix often beats a dozen micro-optimizations.
  • Profile your application with Xdebug, Telescope, or Blackfire to identify actual bottlenecks. Measure, don't guess.
  • Clear all caches during deployment to prevent stale data from blocking new features or bug fixes.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles