PHP es un lenguaje interpretado: por defecto, cada vez que llega una petición, el servidor lee el fichero .php, lo convierte a opcodes (bytecode) y los ejecuta. OPcache interrumpe ese ciclo guardando los opcodes compilados en memoria compartida. El resultado es que las peticiones siguientes saltan directamente a la ejecución sin volver a parsear ni compilar el código fuente.
Cómo funciona OPcache
Sin OPcache, el ciclo de vida de cada petición es: leer el .php del disco ? parsear el código ? compilar a opcodes ? ejecutar. Con OPcache habilitado, la segunda petición en adelante solo ejecuta, porque los opcodes ya están en memoria. El ahorro de tiempo suele estar entre el 30% y el 80% del tiempo de proceso, dependiendo de la complejidad del código.
Habilitar y configurar OPcache
En PHP moderno viene incluido pero puede estar desactivado. Compruébalo con php -m | grep OPcache o revisando phpinfo():
; php.ini configuración recomendada para producción
opcache.enable=1
opcache.enable_cli=0 ; normalmente no hace falta en CLI
opcache.memory_consumption=128 ; MB de RAM para opcodes (256 en apps grandes)
opcache.interned_strings_buffer=16 ; MB para strings internos (identificadores, etc.)
opcache.max_accelerated_files=10000 ; número máximo de ficheros cacheados
opcache.validate_timestamps=0 ; en producción desactívalo (mejora rendimiento)
opcache.revalidate_freq=0 ; solo importa si validate_timestamps=1
opcache.fast_shutdown=1 ; liberación más rápida de memoria al final de petición
opcache.save_comments=0 ; elimina comentarios de opcodes (cuidado con Doctrine/anotaciones)
La directiva más importante para producción es validate_timestamps=0: hace que OPcache no compruebe si el fichero ha cambiado en disco, eliminando accesos al sistema de ficheros. El precio es que tienes que limpiar la caché manualmente al deployar.
Limpiar la caché al deployar
<?php
// Invalidar un fichero específico
opcache_invalidate('/var/www/app/src/Controllers/HomeController.php', true);
// Invalidar todos los ficheros cacheados
opcache_reset();
// Invalidar desde CLI (via script de deploy)
// Cuidado: opcache_reset() en CLI afecta al proceso CLI, no a PHP-FPM
// Debes lanzarlo via HTTP (petición al servidor web) o usar un signal
// Script de deploy: reset_cache.php (protegido con token)
<?php
$token = $_GET['token'] ?? '';
if (!hash_equals($_ENV['DEPLOY_TOKEN'], $token)) {
http_response_code(403);
exit;
}
opcache_reset();
echo json_encode(['status' => 'ok', 'time' => date('c')]);
# En el script de deploy (shell):
# Después de copiar los ficheros nuevos:
curl -s "https://miapp.com/reset_cache.php?token=$DEPLOY_TOKEN"
Inspeccionar el estado de OPcache
<?php
$info = opcache_get_status();
if ($info === false) {
echo "OPcache no está habilitado";
} else {
$mem = $info['memory_usage'];
$stats = $info['opcache_statistics'];
echo "Habilitado: " . ($info['opcache_enabled'] ? 'sí' : 'no') . "n";
echo "Ficheros: " . $stats['num_cached_scripts'] . "n";
echo "Hits: " . $stats['hits'] . "n";
echo "Misses: " . $stats['misses'] . "n";
echo "Hit rate: " . round($stats['opcache_hit_rate'], 2) . "%n";
echo "Memoria usada: " . round($mem['used_memory'] / 1024 / 1024, 2) . " MBn";
echo "Memoria libre: " . round($mem['free_memory'] / 1024 / 1024, 2) . " MBn";
// Lista de ficheros cacheados
foreach ($info['scripts'] as $ruta => $datos) {
echo $ruta . " hits: " . $datos['opcache_hits'] . "n";
}
}
// Configuración activa
$config = opcache_get_configuration();
echo "max_accelerated_files: " . $config['directives']['opcache.max_accelerated_files'];
El JIT de PHP 8
PHP 8.0 introdujo el compilador JIT (Just-In-Time). Donde OPcache evita recompilar a opcodes, el JIT va un paso más allá y compila partes del código a código máquina nativo durante la ejecución. En código PHP típico de aplicaciones web el beneficio del JIT es modesto (5-10%); donde destaca es en código computacionalmente intensivo (procesamiento de imágenes, cálculos numéricos, compresión).
; php.ini activar JIT (requiere OPcache habilitado)
opcache.jit_buffer_size=64M ; memoria para el código compilado por JIT
opcache.jit=tracing ; modos: disable, function, tracing (recomendado), on
; Modos JIT:
; function (1201): compila funciones completas
; tracing (1254): compila trazas de ejecución calientes mejor para la mayoría
; on (1255): alias de tracing
<?php
// Verificar si JIT está activo
$config = opcache_get_configuration();
echo "JIT activo: " . ($config['jit']['enabled'] ? 'sí' : 'no');
echo "JIT buffer: " . round($config['jit']['buffer_size'] / 1024 / 1024) . " MB";
$status = opcache_get_status();
if (isset($status['jit'])) {
echo "JIT hits: " . $status['jit']['jit_hot_funcs'];
echo "JIT buffer usado: " . round($status['jit']['buffer_size'] / 1024 / 1024, 2) . " MB";
}
Impacto real: benchmark simplificado
<?php
// Medir el tiempo con y sin OPcache
function benchmark(int $iteraciones, callable $fn): float {
$inicio = hrtime(true);
for ($i = 0; $i < $iteraciones; $i++) {
$fn();
}
return (hrtime(true) - $inicio) / 1e6; // milisegundos
}
// Simular trabajo: incluir ficheros PHP (el beneficio real de OPcache)
$ms = benchmark(1000, function() {
// include 'config.php'; // con OPcache: solo ejecuta opcodes cacheados
json_encode(range(1, 100)); // operación representativa
});
echo "1000 iteraciones: {$ms} ms";
Preloading: cargar código al arrancar PHP-FPM
Disponible desde PHP 7.4: ejecuta un script de preloading al arrancar PHP-FPM y carga las clases más usadas en memoria compartida. Todas las peticiones subsiguientes acceden a esas clases sin buscarlas en disco:
; php.ini
opcache.preload=/var/www/app/preload.php
opcache.preload_user=www-data ; usuario que ejecuta PHP-FPM
<?php
// preload.php se ejecuta UNA vez al arrancar PHP-FPM
$clases = [
'/var/www/app/src/Core/Router.php',
'/var/www/app/src/Core/Request.php',
'/var/www/app/src/Core/Response.php',
'/var/www/app/src/Middleware/Auth.php',
];
foreach ($clases as $ruta) {
opcache_compile_file($ruta);
}
// Con Composer:
require '/var/www/app/vendor/autoload.php';
// Cargar las clases más usadas de las dependencias
$classLoader = require '/var/www/app/vendor/autoload.php';
foreach ($classLoader->getClassMap() as $clase => $fichero) {
if (str_starts_with($clase, 'Symfony\Component\HttpFoundation')) {
require_once $fichero;
}
}
Errores frecuentes
- validate_timestamps=0 en desarrollo: si desactivas la validación de timestamps en tu entorno local, OPcache no detectará que cambiaste el código. Mantén
validate_timestamps=1en desarrollo. - opcache_reset() desde CLI: no afecta al proceso PHP-FPM. Debes hacer una petición HTTP o enviar SIGUSR2 al proceso master de PHP-FPM:
kill -SIGUSR2 $(cat /var/run/php-fpm.pid). - memory_consumption demasiado pequeño: si OPcache se llena, empieza a desalojar ficheros y el hit rate baja. Monitoriza
opcache_get_status()['opcache_statistics']['oom_restarts']; si sube, aumenta la memoria. - save_comments=0 con anotaciones: Doctrine, PHPUnit y otros frameworks usan comentarios PHPDoc como metadatos. Desactivar save_comments los elimina y causa errores.
