PHP Performance Optimization: 6x Speedup from 8.2s to 1.4s with 7 Fixes
A legacy PHP 7.2 project with 8.2s average response time was optimized to 1.4s in two weeks through seven fundamental fixes: enabling OPcache, eliminating N+1 queries via eager loading, adding composite database indexes, implementing Redis caching layers, setting HTTP timeouts for external calls, right-sizing PHP-FPM workers, and separating API middleware from web routes.
Last month I took over a legacy PHP 7.2 project where users complained that clicking a button took "the time to drink a cup of coffee." The average API response time was 8.2 seconds. Over two weeks, without rewriting or changing frameworks, I applied seven basic optimizations that brought it down to 1.4 seconds — a nearly 6x improvement.
1. Enable OPcache First
OPcache was disabled ( opcache.enable=0). PHP recompiles every file on every request; with 200 files that's massive overhead. OPcache stores compiled opcode in shared memory.
Configuration used:
opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.revalidate_freq=0Setting validate_timestamps=0 is critical for production — it stops the periodic file-modification check. Restart PHP-FPM on deploy instead. Result: 8.2s → 5.6s (30–50% gain).
2. Kill N+1 Queries with Eager Loading
An order list page triggered 427 SQL queries (Laravel Telescope). The code looped through orders and accessed $order->user->name, $order->items->count(), $order->logistics->status — three extra queries per order. For 100 orders that's 301 queries.
Fixed with with() eager loading:
$orders = Order::with(['user', 'items', 'logistics'])
->where('status', 1)
->get();Queries dropped from 301 to 4. Response: 5.6s → 3.1s. Warning: with() only works on the main query; accessing relations inside a loop after that still triggers lazy loading. Verify with Telescope/Debugbar.
3. Add Missing Database Indexes
Enabled MySQL slow query log ( long_query_time=0.5). A recurring query:
SELECT * FROM orders WHERE status = 1 ORDER BY created_at DESC LIMIT 20;The orders table had 800k rows; status and created_at lacked indexes. Full table scan + filesort took 800ms+. Added composite index:
ALTER TABLE orders ADD INDEX idx_status_created (status, created_at);Time dropped to 12ms. Index column order matters: WHERE columns first, ORDER BY columns last. Reversed order makes the index unusable.
4. Use Redis Properly for Caching
Previous caching was ad-hoc (APCu, file cache) and broke across servers. Standardized on Redis with a layered TTL strategy:
Static hot data (categories, site config, i18n copy) — 24h TTL
Product details, price, stock — 1h TTL, active invalidation on admin update
Implemented a ProductCacheService:
<?php
namespace App\Services;
use Illuminate\Support\Facades\Redis;
class ProductCacheService
{
public static function getProductData(int $id): array
{
$key = "product:info:{$id}";
$cached = Redis::get($key);
if (!empty($cached)) {
return json_decode($cached, true);
}
$product = \App\Models\Product::with(['images', 'category', 'spec'])
->where('status', 1)
->findOrFail($id);
Redis::setex($key, 3600, json_encode($product));
return $product->toArray();
}
public static function clearProductCache(int $id): void
{
Redis::del("product:info:{$id}");
}
}Admin edits call clearProductCache. DB queries fell 85%; response: 3.1s → 2.2s.
5. Add Timeouts to All External Calls
A third-party exchange-rate API used file_get_contents with no timeout (default 60s). When the API hung, PHP-FPM workers were blocked, causing 502s under load.
Fixed with stream context timeout:
$context = stream_context_create([
'http' => ['timeout' => 3]
]);
$rate = file_get_contents('https://api.rate.com/latest', false, $context);For cURL, set both timeouts:
curl_setopt($ch, CURLOPT_TIMEOUT, 3);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 1);Non-critical third-party calls (tracking, logging, notifications) moved to async queues. Peak 502 rate dropped from 15% to near zero.
6. Size PHP-FPM Workers by Memory, Not Guesswork
pm.max_childrenwas 100 on a 4GB server. Each worker ~35MB → 3.5GB, leaving no room for OS/other services; swapping killed performance.
Formula:
max_children = (available_memory - system_reserve) / avg_process_memory. Reserve 1GB, 3GB left / 35MB ≈ 85. Set 60 for safety margin.
Switched listen from TCP to Unix socket: listen = /run/php/php-fpm.sock Saves 1–2ms per request by skipping network stack. Response: 2.2s → 1.8s.
7. Strip Web Middleware from API Routes
API and web routes shared the same middleware stack. Every API request loaded Session, CSRF, Blade view providers — all useless for APIs.
In Laravel, define a lean api middleware group in Kernel.php:
protected $middlewareGroups = [
'api' => [
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];Ensure RouteServiceProvider registers API routes without the web group. For ThinkPHP: php think optimize compiles routes to static arrays, skipping regex matching and reflection per request. Response: 1.8s → 1.4s.
Key Takeaways
None of these are "advanced tricks" — OPcache, eager loading, indexes, Redis, timeouts, FPM sizing, middleware separation are all basics. The project's problem wasn't technical difficulty; it was missing fundamentals.
Recommended order:
Enable OPcache — 5 minutes, biggest immediate win
Enable slow query log + Telescope/Debugbar — measure, don't guess
Kill N+1 with with() — least code, highest ROI
Add indexes — check EXPLAIN for type=ALL Redis cache — start with hottest data, not everything
Timeout all external calls — survival, not just performance
Tune FPM and middleware — final polish
I initially wasted two days refactoring a complex business method based on intuition; performance didn't budge. A Blackfire flame graph revealed the real bottlenecks: DB connections and file loading. Data doesn't lie; intuition does. Run this checklist first; if bottlenecks remain, then dig deeper.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
php Courses
php中文网's platform for the latest courses and technical articles, helping PHP learners advance quickly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
