High-Performance PHP: Optimisation Strategies
Proven techniques for optimising PHP applications to handle high-turnover, high-complexity scenarios.
PHP has a reputation for being slow, but that's largely outdated. Modern PHP 8.3+ with proper optimisation can handle thousands of requests per second. The key is knowing where to optimise and how to measure the impact of your changes.
These techniques come from optimising PHP applications under real production load. Here are the ones that deliver measurable performance gains.
Performance Measurement Foundation
Profiling Tools
You can't optimise what you don't measure. Install Xdebug for profiling:
# Install Xdebug for profiling
pecl install xdebug
# php.ini configuration
zend_extension=xdebug.so
xdebug.mode=profile
xdebug.start_with_request=trigger
xdebug.output_dir="/tmp/xdebug"
xdebug.profiler_output_name="cachegrind.out.%p"
Use with tools like KCacheGrind or Webgrind to visualise performance bottlenecks.
Application Performance Monitoring
A small timer registry, keyed by metric name, keeps instrumentation code out of the way of the logic it's measuring:
<?php
declare(strict_types=1);
namespace App\Monitoring;
use App\Exceptions\TimerNotFoundException;
use App\ValueObjects\{Duration, MetricName};
use Psr\Log\LoggerInterface;
final class PerformanceMonitor
{
/** @var array<string, float> */
private array $timers = [];
public function __construct(
private readonly LoggerInterface $logger,
private readonly MetricsCollector $metricsCollector,
) {
}
public function start(MetricName $name): void
{
$this->timers[$name->value] = hrtime(true);
}
public function end(MetricName $name): Duration
{
$timerKey = $name->value;
if (!isset($this->timers[$timerKey])) {
throw new TimerNotFoundException("Timer '{$timerKey}' not found");
}
$elapsed = Duration::fromNanoseconds(
hrtime(true) - $this->timers[$timerKey]
);
unset($this->timers[$timerKey]);
$this->metricsCollector->timing($name, $elapsed);
$this->logger->debug('Performance metric recorded', [
'metric' => $name->value,
'duration_ms' => $elapsed->toMilliseconds(),
]);
return $elapsed;
}
}
OPcache Optimisation
OPcache is the most important PHP optimisation. It caches compiled bytecode, eliminating the need to parse and compile PHP files on every request.
Production OPcache Configuration
# php.ini
opcache.enable=1
opcache.enable_cli=1
opcache.memory_consumption=512
opcache.interned_strings_buffer=64
opcache.max_accelerated_files=32531
opcache.validate_timestamps=0
opcache.revalidate_freq=0
opcache.fast_shutdown=1
opcache.enable_file_override=1
opcache.optimization_level=0x7FFEBFFF
opcache.preload=/var/www/html/preload.php
opcache.preload_user=www-data
OPcache Monitoring
<?php
declare(strict_types=1);
namespace App\Monitoring\OPcache;
use App\Exceptions\{OPcacheInvalidationFailedException, OPcacheNotAvailableException, OPcacheResetFailedException};
use App\ValueObjects\{HitRate, MemoryUsage};
final readonly class OPcacheMonitor
{
public function __construct(
private OPcacheStatusReader $statusReader,
private OPcacheConfigReader $configReader,
) {
}
public function getStats(): OPcacheStats
{
if (!extension_loaded('opcache')) {
throw new OPcacheNotAvailableException('OPcache extension not loaded');
}
$status = $this->statusReader->read();
$config = $this->configReader->read();
return new OPcacheStats(
enabled: $status['opcache_enabled'],
hitRate: HitRate::fromFloat($status['opcache_statistics']['opcache_hit_rate']),
memoryUsage: MemoryUsage::fromArray($status['memory_usage']),
cachedScripts: $status['opcache_statistics']['num_cached_scripts'],
maxCachedKeys: $config['directives']['opcache.max_accelerated_files'],
jitEnabled: $config['directives']['opcache.jit_buffer_size'] > 0,
jitBufferSize: $config['directives']['opcache.jit_buffer_size'],
);
}
public function reset(): void
{
if (!opcache_reset()) {
throw new OPcacheResetFailedException('Failed to reset OPcache');
}
}
public function invalidateFile(string $filePath): void
{
if (!opcache_invalidate($filePath, true)) {
throw new OPcacheInvalidationFailedException(
"Failed to invalidate file: {$filePath}"
);
}
}
}
Database Optimisation
Connection Pooling
Database connections are expensive to establish, so reusing them matters. The acquire/release pattern below only helps within a single long-running PHP process - a queue worker, a CLI daemon, a Swoole/RoadRunner-style long-lived worker - because it holds state across calls in the same process. It is not how you pool connections for typical PHP-FPM web traffic, where each request gets a fresh process: there, the real answer is PDO::ATTR_PERSISTENT or an external pooler such as ProxySQL or PgBouncer.
<?php
declare(strict_types=1);
namespace App\Database\Connection;
use App\Exceptions\{ConnectionCreationFailedException, ConnectionPoolExhaustedException};
use App\ValueObjects\{ConnectionId, ConnectionString};
use PDO;
use PDOException;
final class DatabaseConnectionPool
{
/** @var array<string, PDO> */
private array $connections = [];
/** @var array<string, ConnectionId> */
private array $available = [];
/** @var array<string, ConnectionId> */
private array $checkedOut = [];
public function __construct(
private readonly ConnectionString $dsn,
private readonly DatabaseCredentials $credentials,
private readonly int $maxConnections = 20,
private readonly ConnectionOptions $options = new ConnectionOptions(),
) {
}
public function getConnection(): PDO
{
$connectionId = $this->takeAvailableConnection()
?? $this->createNewConnection();
return $this->connections[$connectionId->value];
}
public function release(ConnectionId $connectionId): void
{
unset($this->checkedOut[$connectionId->value]);
$this->available[$connectionId->value] = $connectionId;
}
private function takeAvailableConnection(): ?ConnectionId
{
if ($this->available === []) {
return null;
}
$connectionId = array_shift($this->available);
$this->checkedOut[$connectionId->value] = $connectionId;
return $connectionId;
}
private function createNewConnection(): ConnectionId
{
if (count($this->connections) >= $this->maxConnections) {
throw new ConnectionPoolExhaustedException(
"Maximum connections ({$this->maxConnections}) reached"
);
}
$connectionId = ConnectionId::generate();
try {
$pdo = new PDO(
$this->dsn->value,
$this->credentials->username,
$this->credentials->password,
$this->options->toPdoOptions(),
);
$this->connections[$connectionId->value] = $pdo;
$this->checkedOut[$connectionId->value] = $connectionId;
return $connectionId;
} catch (PDOException $e) {
throw new ConnectionCreationFailedException(
"Failed to create database connection: {$e->getMessage()}",
previous: $e
);
}
}
}
Query Optimisation
Logging slow queries as they happen gives you a rolling record of what to fix without adding overhead to the fast path:
<?php
declare(strict_types=1);
namespace App\Database\Performance;
use App\ValueObjects\{QueryDuration, QueryMetrics};
use DateTimeImmutable;
use PDO;
use Psr\Log\LoggerInterface;
final class QueryOptimizer
{
/** @var array<int, QueryMetrics> */
private array $queryLog = [];
public function __construct(
private PDO $pdo,
private LoggerInterface $logger,
private float $slowQueryThreshold = 0.1,
private int $maxSlowQueries = 10,
) {
}
public function executeQuery(string $sql, array $params = []): array
{
$startTime = hrtime(true);
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
$result = $stmt->fetchAll();
$duration = QueryDuration::fromNanoseconds(hrtime(true) - $startTime);
if ($duration->exceeds($this->slowQueryThreshold)) {
$this->logSlowQuery($sql, $params, $duration);
}
return $result;
}
private function logSlowQuery(string $sql, array $params, QueryDuration $duration): void
{
$metrics = new QueryMetrics(
sql: $sql,
parameters: $params,
duration: $duration,
executedAt: new DateTimeImmutable()
);
$this->queryLog[] = $metrics;
$this->logger->warning('Slow query detected', [
'sql' => $sql,
'duration_ms' => $duration->toMilliseconds(),
'params' => $params,
]);
if (count($this->queryLog) >= $this->maxSlowQueries) {
$this->logger->alert('Slow query threshold exceeded', [
'count' => count($this->queryLog),
'threshold' => $this->maxSlowQueries,
]);
}
}
public function getSlowQueries(): array
{
return $this->queryLog;
}
}
Caching Strategies
Multi-Level Caching
A two-tier cache checks an in-process store first (an array for the lifetime of the request, or APCu across requests on the same worker), and only falls back to a shared store like Redis on a miss. A hit at the second tier backfills the first tier, so the next lookup for the same key on that process is free:
<?php
declare(strict_types=1);
namespace App\Caching;
use JsonException;
use Redis;
final class MultiLevelCacheManager
{
/** @var array<string, mixed> */
private array $l1Cache = [];
public function __construct(
private readonly Redis $l2Cache,
private readonly int $l1MaxEntries = 500,
private readonly int $l2TtlSeconds = 3600,
) {
}
public function get(string $key, mixed $default = null): mixed
{
if (array_key_exists($key, $this->l1Cache)) {
return $this->l1Cache[$key];
}
$encoded = $this->l2Cache->get($key);
if ($encoded === false) {
return $default;
}
try {
$value = json_decode($encoded, associative: true, flags: JSON_THROW_ON_ERROR);
} catch (JsonException) {
return $default;
}
$this->backfillL1($key, $value);
return $value;
}
public function set(string $key, mixed $value): void
{
$this->backfillL1($key, $value);
$this->l2Cache->set($key, json_encode($value, JSON_THROW_ON_ERROR), $this->l2TtlSeconds);
}
public function delete(string $key): void
{
unset($this->l1Cache[$key]);
$this->l2Cache->del($key);
}
private function backfillL1(string $key, mixed $value): void
{
if (count($this->l1Cache) >= $this->l1MaxEntries) {
array_shift($this->l1Cache);
}
$this->l1Cache[$key] = $value;
}
}
Smart Cache Invalidation
Tagging cache entries lets you invalidate a whole group of related keys at once, without tracking every individual key that needs clearing when the underlying data changes:
<?php
declare(strict_types=1);
namespace App\Caching;
use Redis;
final class TaggedCacheInvalidator
{
public function __construct(
private readonly Redis $redis,
private readonly string $tagKeyPrefix = 'cache:tag:',
) {
}
/**
* @param array<int, string> $tags
*/
public function set(string $key, string $value, array $tags, int $ttlSeconds = 3600): void
{
$this->redis->set($key, $value, $ttlSeconds);
foreach ($tags as $tag) {
$this->redis->sAdd($this->tagKey($tag), $key);
}
}
public function invalidateTag(string $tag): int
{
$tagKey = $this->tagKey($tag);
$keys = $this->redis->sMembers($tagKey);
if ($keys === [] || $keys === false) {
return 0;
}
$removed = $this->redis->del($keys);
$this->redis->del($tagKey);
return $removed;
}
private function tagKey(string $tag): string
{
return $this->tagKeyPrefix . $tag;
}
}
Memory Management
Object Pooling
Some objects are expensive enough to construct that reusing them is worthwhile - a database result buffer, a compiled template, a large value object. An object pool hands out instances via acquire() and only accepts them back via an explicit release(), so it can enforce a maximum outstanding count instead of constructing without limit:
<?php
declare(strict_types=1);
namespace App\Performance;
use App\Exceptions\PoolExhaustedException;
use Closure;
use SplObjectStorage;
/**
* @template T of object
*/
final class ObjectPool
{
/** @var array<int, object> */
private array $available = [];
private SplObjectStorage $checkedOut;
/**
* @param Closure(): T $factory
*/
public function __construct(
private readonly Closure $factory,
private readonly int $maxSize = 20,
) {
$this->checkedOut = new SplObjectStorage();
}
/**
* @return T
*/
public function acquire(): object
{
$object = array_pop($this->available) ?? $this->createIfPermitted();
$this->checkedOut->attach($object);
return $object;
}
public function release(object $object): void
{
if (!$this->checkedOut->contains($object)) {
throw new PoolExhaustedException('Cannot release an object this pool did not hand out');
}
$this->checkedOut->detach($object);
$this->available[] = $object;
}
private function createIfPermitted(): object
{
$inUse = count($this->checkedOut);
if ($inUse >= $this->maxSize) {
throw new PoolExhaustedException("Pool exhausted: {$inUse} objects checked out");
}
return ($this->factory)();
}
}
HTTP Client Reuse
The same pooling idea applies to outbound HTTP clients: constructing a fresh client per request discards the underlying TCP connection along with it. A factory that reuses a client per target host keeps the connection alive between calls instead of paying a new TCP and TLS handshake every time:
<?php
declare(strict_types=1);
namespace App\Http;
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
final class HttpClientFactory
{
/** @var array<string, Client> */
private array $clients = [];
public function forHost(string $baseUri): Client
{
return $this->clients[$baseUri] ??= $this->buildClient($baseUri);
}
private function buildClient(string $baseUri): Client
{
return new Client([
'base_uri' => $baseUri,
'handler' => HandlerStack::create(),
'headers' => [
'Connection' => 'keep-alive',
],
'curl' => [
CURLOPT_TCP_KEEPALIVE => 1,
CURLOPT_TCP_KEEPIDLE => 60,
CURLOPT_TCP_KEEPINTVL => 15,
CURLOPT_FORBID_REUSE => false,
CURLOPT_FRESH_CONNECT => false,
],
'timeout' => 5.0,
]);
}
}
Memory Leak Detection
In a long-running worker, a slow memory leak only shows up as a trend across many requests. Snapshotting memory_get_usage() at named checkpoints and diffing between them makes that trend visible:
<?php
declare(strict_types=1);
namespace App\Performance;
use InvalidArgumentException;
final class MemoryProfiler
{
/** @var array<string, array{usage: int, peak: int}> */
private array $checkpoints = [];
public function checkpoint(string $label): void
{
$this->checkpoints[$label] = [
'usage' => memory_get_usage(real_usage: true),
'peak' => memory_get_peak_usage(real_usage: true),
];
}
/**
* @return array{from: string, to: string, deltaBytes: int, peakBytes: int}
*/
public function diff(string $fromLabel, string $toLabel): array
{
if (!isset($this->checkpoints[$fromLabel], $this->checkpoints[$toLabel])) {
throw new InvalidArgumentException('Both checkpoints must be recorded before diffing');
}
$from = $this->checkpoints[$fromLabel];
$to = $this->checkpoints[$toLabel];
return [
'from' => $fromLabel,
'to' => $toLabel,
'deltaBytes' => $to['usage'] - $from['usage'],
'peakBytes' => $to['peak'],
];
}
/**
* @return array<string, array{usage: int, peak: int}>
*/
public function getCheckpoints(): array
{
return $this->checkpoints;
}
}
Asynchronous Processing
Job Queue Implementation
Moving slow work off the request/response cycle means pushing a serialised job onto a queue and processing it separately. The queue below pushes and pops jobs from a Redis list, keyed by job name against a small class map so it knows how to reconstruct each one. Jobs implement a shared abstract Job base (handle(), getName(), getPayload(), and a retry limit) so the queue can serialise and reconstruct any job the same way:
<?php
declare(strict_types=1);
namespace App\Jobs;
use JsonException;
use Redis;
use RuntimeException;
final class RedisJobQueue
{
/**
* @param array<string, class-string<Job>> $jobClassMap keyed by Job::getName()
*/
public function __construct(
private readonly Redis $redis,
private readonly array $jobClassMap,
private readonly string $queueKey = 'jobs:default',
) {
}
public function push(Job $job): void
{
$encoded = json_encode([
'name' => $job->getName(),
'payload' => $job->getPayload(),
], JSON_THROW_ON_ERROR);
$this->redis->lPush($this->queueKey, $encoded);
}
public function pop(int $timeoutSeconds = 5): ?Job
{
$result = $this->redis->brPop([$this->queueKey], $timeoutSeconds);
if ($result === [] || $result === false) {
return null;
}
[, $encoded] = $result;
try {
$decoded = json_decode($encoded, associative: true, flags: JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
throw new RuntimeException("Failed to decode job payload: {$e->getMessage()}", previous: $e);
}
$jobClass = $this->jobClassMap[$decoded['name']]
?? throw new RuntimeException("No job class registered for '{$decoded['name']}'");
return $jobClass::fromPayload($decoded['payload']);
}
}
<?php
declare(strict_types=1);
namespace App\Jobs;
abstract class Job
{
abstract public function handle(): void;
abstract public function getName(): string;
/** @return array<string, mixed> */
abstract public function getPayload(): array;
/**
* @param array<string, mixed> $payload
*/
abstract public static function fromPayload(array $payload): self;
public function getMaxRetries(): int
{
return 3;
}
}
HTTP Performance Optimisation
Response Streaming
Large responses don't need to sit fully in memory before the first byte goes out. Fetching rows with an unbuffered query and flushing output as each one is encoded keeps memory use flat regardless of result size:
<?php
declare(strict_types=1);
namespace App\Http;
use Generator;
use PDO;
final class StreamingResponse
{
public function __construct(
private readonly PDO $pdo,
) {
}
/**
* Streams rows as newline-delimited JSON without buffering the full
* result set in memory, using an unbuffered cursor.
*/
public function streamQueryAsNdjson(string $sql): void
{
header('Content-Type: application/x-ndjson');
header('X-Accel-Buffering: no');
foreach ($this->fetchRows($sql) as $row) {
echo json_encode($row, JSON_THROW_ON_ERROR), "\n";
if (ob_get_level() > 0) {
ob_flush();
}
flush();
}
}
/**
* Uses an unbuffered query so PDO fetches rows from the server as they
* are consumed, rather than pulling the whole result set into memory
* before the first row is available.
*
* @return Generator<int, array<string, mixed>>
*/
private function fetchRows(string $sql): Generator
{
$wasBuffered = $this->pdo->getAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY);
$this->pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
$stmt = $this->pdo->query($sql);
while (($row = $stmt->fetch(PDO::FETCH_ASSOC)) !== false) {
yield $row;
}
$this->pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, $wasBuffered);
}
}
Response Compression
Compressing the response body when the client supports it cuts transfer size at the cost of some CPU time. This middleware only compresses above a minimum size, since gzip has fixed overhead that makes it counterproductive on tiny payloads:
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
final class CompressionMiddleware implements MiddlewareInterface
{
public function __construct(
private readonly StreamFactoryInterface $streamFactory,
private readonly int $minimumBytes = 860,
private readonly int $gzipLevel = 6,
) {
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$response = $handler->handle($request);
$encoding = $this->negotiateEncoding($request->getHeaderLine('Accept-Encoding'));
$body = (string) $response->getBody();
if ($encoding === null || strlen($body) < $this->minimumBytes) {
return $response;
}
$compressed = $encoding === 'gzip'
? gzencode($body, $this->gzipLevel)
: gzdeflate($body, $this->gzipLevel);
if ($compressed === false) {
return $response;
}
return $response
->withBody($this->streamFactory->createStream($compressed))
->withHeader('Content-Encoding', $encoding)
->withHeader('Content-Length', (string) strlen($compressed))
->withHeader('Vary', 'Accept-Encoding');
}
private function negotiateEncoding(string $acceptEncoding): ?string
{
if (str_contains($acceptEncoding, 'gzip')) {
return 'gzip';
}
if (str_contains($acceptEncoding, 'deflate')) {
return 'deflate';
}
return null;
}
}
Code-Level Optimisations
Efficient Array Operations
A handful of small habits add up across a codebase that runs the same code path millions of times: choosing the right existence check, preferring the built-in array functions where they read more clearly, and avoiding unnecessary array copies.
<?php
declare(strict_types=1);
namespace App\Performance\Examples;
final class ArrayOptimizer
{
/**
* isset() short-circuits on the array's internal hash lookup and
* tolerates null values being present; in_array() must scan (and,
* without strict: true, apply loose type comparisons).
*/
public function hasKeyEfficiently(array $lookup, string $key): bool
{
return isset($lookup[$key]);
}
/**
* array_key_exists() is the right call when a key might legitimately
* hold null and isset() would give a false negative.
*/
public function hasKeyEvenIfNull(array $lookup, string $key): bool
{
return array_key_exists($key, $lookup);
}
/**
* array_map()/array_filter() avoid the manual accumulator array and
* read closer to the transformation being described, at the cost of
* an extra function call per element versus a hand-written loop.
*/
public function activeEmailAddresses(array $users): array
{
return array_map(
static fn (array $user): string => $user['email'],
array_filter($users, static fn (array $user): bool => $user['active']),
);
}
/**
* PHP arrays are copy-on-write, so passing one by value only costs a
* refcount bump - the actual copy happens the moment the callee writes
* to it. Taking $dataset by reference here means the normalisation
* happens in place, so the caller never pays for a duplicated array
* being returned and reassigned.
*/
public function normaliseInPlace(array &$dataset): void
{
foreach ($dataset as $key => $value) {
$dataset[$key] = is_string($value) ? trim($value) : $value;
}
}
}
String Optimisation
The same applies to strings - using the function that says what it checks, and avoiding repeated concatenation in a loop in favour of building an array and joining it once.
<?php
declare(strict_types=1);
namespace App\Performance\Examples;
final class StringOptimizer
{
/**
* str_contains()/str_starts_with()/str_ends_with() say directly what
* they check. The strpos() equivalents need an explicit !== false
* comparison, since strpos() returns 0 (falsy) for a match at
* position zero.
*/
public function pathLooksAbsolute(string $path): bool
{
return str_starts_with($path, '/');
}
public function pathContainsTraversal(string $path): bool
{
return str_contains($path, '../');
}
/**
* sprintf() keeps a template with several substitutions readable in
* one place; string concatenation of the same template becomes harder
* to scan once there are more than two or three placeholders.
*/
public function formatLogLine(string $level, string $message, float $durationMs): string
{
return sprintf('[%s] %s (%.2fms)', $level, $message, $durationMs);
}
/**
* Repeated string concatenation inside a loop reallocates the growing
* string on every iteration. Collecting the pieces in an array and
* joining once with implode() avoids the repeated reallocation.
*/
public function buildCsvLine(array $columns): string
{
$escaped = [];
foreach ($columns as $column) {
$escaped[] = str_contains($column, ',')
? '"' . str_replace('"', '""', $column) . '"'
: $column;
}
return implode(',', $escaped);
}
}
Load Testing and Benchmarking
Simple Benchmarking
Before trusting an optimisation, measure it. A small harness that times a callable across many iterations and reports the spread is enough to tell whether a change actually helped:
<?php
declare(strict_types=1);
namespace App\Performance;
final class Benchmark
{
/**
* @return array{iterations: int, minMs: float, maxMs: float, meanMs: float}
*/
public function run(callable $subject, int $iterations = 1000): array
{
$durationsMs = [];
for ($i = 0; $i < $iterations; $i++) {
$start = hrtime(true);
$subject();
$durationsMs[] = (hrtime(true) - $start) / 1_000_000;
}
return [
'iterations' => $iterations,
'minMs' => min($durationsMs),
'maxMs' => max($durationsMs),
'meanMs' => array_sum($durationsMs) / count($durationsMs),
];
}
}
Production Monitoring
Real-time Performance Dashboard
Pulling the metrics gathered by the monitors above into a single structured snapshot is what turns them into something a dashboard endpoint can actually return:
<?php
declare(strict_types=1);
namespace App\Monitoring;
use App\Database\Performance\QueryOptimizer;
use App\Monitoring\OPcache\OPcacheMonitor;
use App\Performance\MemoryProfiler;
use DateTimeImmutable;
final readonly class PerformanceDashboard
{
public function __construct(
private OPcacheMonitor $opcacheMonitor,
private QueryOptimizer $queryOptimizer,
private MemoryProfiler $memoryProfiler,
) {
}
/**
* @return array<string, mixed>
*/
public function getSnapshot(): array
{
$opcacheStats = $this->opcacheMonitor->getStats();
$slowQueries = $this->queryOptimizer->getSlowQueries();
return [
'opcache' => [
'enabled' => $opcacheStats->enabled,
'hitRate' => $opcacheStats->hitRate,
'cachedScripts' => $opcacheStats->cachedScripts,
'jitEnabled' => $opcacheStats->jitEnabled,
],
'database' => [
'slowQueryCount' => count($slowQueries),
],
'memory' => $this->memoryProfiler->getCheckpoints(),
'generatedAt' => (new DateTimeImmutable())->format(DATE_ATOM),
];
}
}
Common Pitfalls
- Premature optimisation: Profile before optimising
- Over-caching: Cache invalidation complexity
- Ignoring memory limits: Monitor memory usage
- Database over-optimisation: Sometimes simple queries are better
- Micro-optimisations: Focus on significant bottlenecks
Got a PHP system that needs this kind of attention?
Get in touch