PHP Performance
OPcache
Production Configuration
; /etc/php/8.x/fpm/conf.d/10-opcache.ini opcache.enable=1 opcache.memory_consumption=256 ; MB — increase for large codebases opcache.interned_strings_buffer=16 ; MB — reduces string duplication opcache.max_accelerated_files=20000 ; rounded to next prime internally opcache.validate_timestamps=0 ; DISABLE in production — requires manual reset on deploy opcache.save_comments=1 ; required by Doctrine, PHPUnit, many frameworks opcache.optimization_level=0x7FFEBFFF ; all safe optimizations (default)
Development Configuration
opcache.enable=1 opcache.validate_timestamps=1 ; auto-detect file changes opcache.revalidate_freq=0 ; check on every request
Key Directives
Directive Default Production Notes
opcache.enable
1 1 Always on
opcache.enable_cli
0 0 (or 1 for workers) Enable for long-running CLI
opcache.memory_consumption
128 256-512 MB of shared memory
opcache.interned_strings_buffer
8 16-32 MB for deduplicated strings
opcache.max_accelerated_files
10000 20000-50000 Max cached scripts
opcache.validate_timestamps
1 0 Disable in prod — reset on deploy
opcache.revalidate_freq
2 N/A (timestamps off) Seconds between checks
opcache.file_update_protection
2 2 Skip files modified <N seconds ago
opcache.max_wasted_percentage
5 5-10 Trigger restart at this waste %
Preloading (PHP 7.4+)
opcache.preload=/var/www/app/preload.php opcache.preload_user=www-data
// preload.php — loaded once at server startup $directory = new RecursiveDirectoryIterator(DIR . '/src'); $files = new RecursiveIteratorIterator($directory); $php = new RegexIterator($files, '/.php$/'); foreach ($php as $file) { opcache_compile_file($file->getRealPath()); // compiles without executing }
Rule Detail
opcache_compile_file()
Compiles without executing — safe for any order
require_once alternative Executes code — order matters (base classes first)
Constants NOT preloaded Only functions, classes, traits, interfaces
Requires server restart Can't update preloaded code at runtime
FPM/mod_php only Not useful for one-off CLI scripts
Check opcache.preload_user
Must have read access to files
JIT (PHP 8.0+)
opcache.jit=tracing ; recommended mode (or "function" for simpler JIT) opcache.jit_buffer_size=128M ; 0 = JIT disabled
Mode Value Best for
disable
Permanent off Cannot re-enable at runtime
off
Toggleable off Can enable later via ini_set
tracing
1254 Recommended — traces hot loops/paths
function
1205 Simpler — JIT per function, less overhead
Rule Detail
CPU-bound workloads benefit Math, data processing, tight loops
I/O-bound code: minimal gain DB queries, HTTP calls dominate runtime
opcache.jit_hot_loop = 64 Iterations before loop gets JIT-compiled
opcache.jit_hot_func = 127 Calls before function gets JIT-compiled
Monitor with opcache_get_status()
Check jit.buffer_free for buffer usage
Monitoring & Management
// Check cache health $status = opcache_get_status(); $hitRate = $status['opcache_statistics']['opcache_hit_rate']; // target: >98% $wasted = $status['memory_usage']['current_wasted_percentage']; $full = $status['cache_full'];
// Reset cache (deploy hook) opcache_reset(); // clears entire in-memory cache
// Invalidate single file opcache_invalidate('/path/to/file.php', true); // force=true ignores mtime
Indicator Healthy Action if unhealthy
Hit rate
98% Increase max_accelerated_files or memory_consumption
Wasted memory <5% Restart PHP-FPM or increase max_wasted_percentage
cache_full
false Increase memory_consumption
oom_restarts
0 Increase memory — OOM caused forced restarts
File Cache (Shared Hosting / CLI)
opcache.file_cache=/var/cache/opcache ; persistent across restarts opcache.file_cache_only=0 ; 1 = skip shared memory entirely opcache.file_cache_consistency_checks=1 ; validate checksums
Database (PDO)
// Recommended PDO configuration $pdo = new PDO('mysql:host=localhost;dbname=app;charset=utf8mb4', $user, $pass, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // throw exceptions PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // assoc arrays PDO::ATTR_EMULATE_PREPARES => false, // real prepared statements ]);
// Named parameters — clearer for multiple params $stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email AND active = :active'); $stmt->execute(['email' => $email, 'active' => 1]); $user = $stmt->fetch();
// Transactions $pdo->beginTransaction(); try { $pdo->prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?')->execute([$amount, $from]); $pdo->prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?')->execute([$amount, $to]); $pdo->commit(); } catch (\Throwable $e) { $pdo->rollBack(); throw $e; }
Rule Detail
ERRMODE_EXCEPTION
Always — silent failures hide bugs
EMULATE_PREPARES = false
Real server-side prepared statements — actual SQL injection protection
FETCH_ASSOC default Less memory than FETCH_BOTH (default)
charset=utf8mb4 in DSN MySQL: full Unicode including emoji
Named params :name
Clearer than positional ? for 3+ parameters
Transactions for multi-statement Atomicity — all succeed or all roll back
fetchAll() caution Loads entire result into memory — use fetch() in loop for large results
Caching (Redis / Memcached)
// Redis — cache-aside pattern $redis = new Redis(); $redis->connect('127.0.0.1', 6379);
$key = 'user:' . $userId; $cached = $redis->get($key); if ($cached !== false) { return json_decode($cached, true); } $data = $db->fetchUser($userId); $redis->setex($key, 3600, json_encode($data)); // TTL 1 hour return $data;
// Memcached $mc = new Memcached(); $mc->addServer('127.0.0.1', 11211); $mc->set('key', $value, 3600); $value = $mc->get('key');
Layer Tool When
Bytecode OPcache Always — eliminates recompilation
Application data Redis / Memcached DB query results, API responses, computed values
HTTP Reverse proxy (Varnish, Nginx) Full page / fragment caching
Preloading (7.4+) opcache.preload
Framework classes loaded at startup
Pattern Detail
Cache-aside Check cache → miss → fetch → store → return
Invalidate on write Clear/update cache when data changes
TTL expiration Set appropriate time-to-live per data type
Cache stampede Use locking or probabilistic early refresh
fetchAll()
- cache Load once, cache result, avoid repeated queries
SPL Data Structures
Class Use for vs Array
SplFixedArray
Large fixed-size numeric arrays ~50% less memory
SplStack
LIFO stack (push/pop) Enforces stack semantics
SplQueue
FIFO queue (enqueue/dequeue) Enforces queue semantics
SplPriorityQueue
Priority-based processing Built-in priority ordering
SplHeap / SplMinHeap / SplMaxHeap
Heap operations O(log n) insert/extract
SplDoublyLinkedList
Efficient insert/remove at ends Better for frequent head/tail ops
SplObjectStorage
Map objects to data Object identity as key
Micro-Optimizations
Function Choices — Prefer Faster Alternatives
Slower Faster Why
array_key_exists($k, $a)
isset($a[$k])
Language construct, no function call (but returns false for null values)
in_array($v, $arr) (repeated) isset($flipped[$v])
O(1) hash lookup vs O(n) linear scan — flip once with array_flip()
count($arr) in loop condition $len = count($arr) before loop Avoid recalculating each iteration
strpos($h, $n) !== false
str_contains($h, $n)
Cleaner, same speed (PHP 8.0+)
substr($s, 0, 3) === 'foo'
str_starts_with($s, 'foo')
Purpose-built, clearer (PHP 8.0+)
intval($v) / settype()
(int) $v
Direct cast — no function call overhead
floatval($v)
(float) $v
Direct cast
strval($v)
(string) $v
Direct cast
sprintf() for simple join . concatenation or implode()
sprintf() parses format string
array_push($a, $v)
$a[] = $v
Language construct, no function call
== comparison
comparison No type juggling overhead
array_merge() in loop $result[] = $items
- array_merge(...$result)
Single merge at end
Strings & Arrays
Pattern Detail
String building in loops Collect in array, implode() at end — not .=
array_column($rows, 'id')
Extract column from 2D array — faster than foreach
array_map() for transforms Clean for simple callbacks; foreach for complex logic
array_filter() preserves keys Use array_values() to reindex if needed
Generators for large data yield instead of building arrays in memory
SplFixedArray
Pre-sized array for large fixed-size datasets
in_array() strict mode Always in_array($v, $a, true) — avoids type juggling
Memory Management
// Monitor memory usage echo memory_get_usage(true); // real OS allocation echo memory_get_peak_usage(true); // max during script
// Free large variables explicitly unset($largeArray);
// Force garbage collection in long-running processes gc_collect_cycles();
Rule Detail
unset() large variables Frees memory when no other references exist
gc_collect_cycles()
Manual GC for long-running CLI scripts
Generators over arrays Process items one-by-one instead of loading all into memory
Streaming file reads fgets() / SplFileObject — never file_get_contents() on large files
Typed properties Enable engine optimizations and reduce memory
memory_get_peak_usage(true)
Track high-water mark during profiling
Loops
// Cache count outside loop $len = count($items); for ($i = 0; $i < $len; $i++) { }
// foreach is idiomatic for arrays — use it foreach ($items as $key => $value) { }
// Unset reference after foreach by-reference foreach ($items as &$item) { $item = transform($item); } unset($item); // CRITICAL — prevents bugs from lingering reference
// array_map for simple transforms $names = array_map(fn($u) => $u->name, $users);
Pattern Best for
foreach
Default for iterating arrays — idiomatic, fast
for with cached count Index-based access, early break by position
array_map()
Simple 1:1 transforms with clean callback
array_filter()
Filtering with predicate
array_walk()
Modify in-place by reference (less common)
while
- fgets()
Line-by-line file processing
Generator + foreach
Lazy iteration over large datasets
Profiling
Tool Use for
opcache_get_status()
OPcache hit rates, memory, JIT stats
memory_get_peak_usage()
Memory high-water mark
Xdebug profiler Function-level timing and call graphs
Blackfire Production-safe profiling with recommendations
Tideways APM with low overhead
Rule Always profile before optimizing — don't guess