Rendimiento en PHP
Un codigo rapido mejora la experiencia del usuario y reduce costos de servidor. Conocer las tecnicas basicas de optimizacion te ayudara a escribir aplicaciones PHP mas eficientes.
OPcache: cache de bytecode
PHP compila el codigo fuente a bytecode en cada peticion. OPcache guarda ese bytecode compilado en memoria, evitando recompilar el mismo archivo una y otra vez.
; Configuracion recomendada para produccion (php.ini)
opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=10000
opcache.validate_timestamps=0 ; En produccion, no verificar cambios
opcache.save_comments=1
opcache.enable_file_override=1
<?php
declare(strict_types=1);
// Verificar estado de OPcache
if (function_exists('opcache_get_status')) {
$estado = opcache_get_status();
echo "Memoria usada: " . $estado['memory_usage']['used_memory'] . " bytes\n";
echo "Scripts cacheados: " . $estado['opcache_statistics']['num_cached_scripts'] . "\n";
}
Con validate_timestamps=0, despues de cada deploy debes
limpiar el cache con opcache_reset() o reiniciando PHP-FPM.
Caching de datos
Cachear resultados de operaciones costosas (consultas a BD, APIs externas) reduce drasticamente el tiempo de respuesta.
Cache simple en archivo
<?php
declare(strict_types=1);
class CacheArchivo
{
public function __construct(
private readonly string $directorio = '/tmp/cache'
) {
if (!is_dir($this->directorio)) {
mkdir($this->directorio, 0755, true);
}
}
public function obtener(string $clave): mixed
{
$archivo = $this->rutaArchivo($clave);
if (!file_exists($archivo)) {
return null;
}
$contenido = file_get_contents($archivo);
$datos = unserialize($contenido);
// Verificar expiracion
if ($datos['expira'] !== 0 && $datos['expira'] < time()) {
unlink($archivo);
return null;
}
return $datos['valor'];
}
public function guardar(string $clave, mixed $valor, int $ttl = 3600): void
{
$datos = [
'valor' => $valor,
'expira' => $ttl > 0 ? time() + $ttl : 0,
];
file_put_contents(
$this->rutaArchivo($clave),
serialize($datos)
);
}
private function rutaArchivo(string $clave): string
{
return $this->directorio . '/' . md5($clave) . '.cache';
}
}
<?php
declare(strict_types=1);
$cache = new CacheArchivo();
// Intentar obtener del cache
$productos = $cache->obtener('productos_destacados');
if ($productos === null) {
// No hay cache, consultar BD
$productos = $repositorio->obtenerProductosDestacados();
// Guardar en cache por 1 hora
$cache->guardar('productos_destacados', $productos, 3600);
}
// Usar $productos
Cache en memoria con APCu
<?php
declare(strict_types=1);
// APCu es mas rapido que archivos (en memoria compartida)
// Requiere extension APCu instalada
// Guardar
apcu_store('usuario_123', $usuario, 300); // 5 minutos
// Obtener
$usuario = apcu_fetch('usuario_123', $exito);
if (!$exito) {
$usuario = $repositorio->buscarUsuario(123);
apcu_store('usuario_123', $usuario, 300);
}
// Eliminar
apcu_delete('usuario_123');
// Patron fetch-or-compute
$config = apcu_entry('config_app', function() {
return cargarConfiguracion();
}, 3600);
Optimizacion de consultas
Evitar N+1 queries
<?php
declare(strict_types=1);
// MAL: N+1 queries (1 para posts + N para autores)
$posts = $pdo->query('SELECT * FROM posts LIMIT 10')->fetchAll();
foreach ($posts as $post) {
$autor = $pdo->query("SELECT * FROM autores WHERE id = {$post['autor_id']}")->fetch();
echo "{$post['titulo']} por {$autor['nombre']}\n";
}
// BIEN: 1 query con JOIN
$query = '
SELECT posts.*, autores.nombre AS autor_nombre
FROM posts
JOIN autores ON posts.autor_id = autores.id
LIMIT 10
';
$posts = $pdo->query($query)->fetchAll();
foreach ($posts as $post) {
echo "{$post['titulo']} por {$post['autor_nombre']}\n";
}
Seleccionar solo columnas necesarias
<?php
declare(strict_types=1);
// MAL: traer todas las columnas
$usuarios = $pdo->query('SELECT * FROM usuarios')->fetchAll();
// BIEN: solo las columnas que necesitas
$usuarios = $pdo->query('SELECT id, nombre, email FROM usuarios')->fetchAll();
Buenas prácticas de código
Evitar operaciones en bucles
<?php
declare(strict_types=1);
$items = [...]; // miles de items
// MAL: count() se ejecuta en cada iteracion
for ($i = 0; $i < count($items); $i++) {
// ...
}
// BIEN: calcular una vez
$total = count($items);
for ($i = 0; $i < $total; $i++) {
// ...
}
// MEJOR: usar foreach cuando sea posible
foreach ($items as $item) {
// ...
}
Usar funciones nativas
<?php
declare(strict_types=1);
$numeros = range(1, 1000);
// MAL: implementacion manual
$suma = 0;
foreach ($numeros as $n) {
$suma += $n;
}
// BIEN: funcion nativa (escrita en C, mas rapida)
$suma = array_sum($numeros);
// MAL: busqueda manual
$encontrado = false;
foreach ($usuarios as $u) {
if ($u['email'] === $email) {
$encontrado = true;
break;
}
}
// BIEN: funcion nativa
$encontrado = in_array($email, array_column($usuarios, 'email'), true);
Concatenacion eficiente de strings
<?php
declare(strict_types=1);
$items = ['a', 'b', 'c', 'd', 'e'];
// MAL: concatenacion repetida (crea strings intermedios)
$resultado = '';
foreach ($items as $item) {
$resultado .= $item . ',';
}
// BIEN: usar implode
$resultado = implode(',', $items);
// Para HTML, usar array y join
$html = [];
foreach ($productos as $p) {
$html[] = "<li>{$p['nombre']}</li>";
}
echo '<ul>' . implode('', $html) . '</ul>';
Generadores para grandes conjuntos
Los generadores procesan datos uno a uno sin cargar todo en memoria. Ideales para archivos grandes o resultados de BD voluminosos.
<?php
declare(strict_types=1);
// MAL: cargar todo en memoria
function leerCSV(string $archivo): array
{
$lineas = [];
$handle = fopen($archivo, 'r');
while (($fila = fgetcsv($handle)) !== false) {
$lineas[] = $fila; // Usa MUCHA memoria si el archivo es grande
}
fclose($handle);
return $lineas;
}
// BIEN: generador (usa memoria constante)
function leerCSV(string $archivo): Generator
{
$handle = fopen($archivo, 'r');
while (($fila = fgetcsv($handle)) !== false) {
yield $fila; // Procesa una fila a la vez
}
fclose($handle);
}
// Uso: procesar archivo de millones de filas sin problemas de memoria
foreach (leerCSV('datos.csv') as $fila) {
procesarFila($fila);
}
Ejercicios
Ejercicio 1: Implementar cache
Crea una funcion que cachee el resultado de una consulta costosa:
<?php
declare(strict_types=1);
// Esta funcion tarda 2 segundos en ejecutarse
function obtenerEstadisticas(): array
{
sleep(2); // Simula consulta lenta
return ['usuarios' => 1000, 'ventas' => 50000];
}
// Implementa una version cacheada
Ver solucion
<?php
declare(strict_types=1);
function obtenerEstadisticasCacheadas(): array
{
$archivocache = '/tmp/estadisticas.cache';
$ttl = 300; // 5 minutos
// Verificar si existe cache valido
if (file_exists($archivocache)) {
$datos = unserialize(file_get_contents($archivocache));
if ($datos['expira'] > time()) {
return $datos['valor'];
}
}
// Ejecutar consulta costosa
$resultado = obtenerEstadisticas();
// Guardar en cache
$datos = [
'valor' => $resultado,
'expira' => time() + $ttl,
];
file_put_contents($archivocache, serialize($datos));
return $resultado;
}
Ejercicio 2: Optimizar bucle
Optimiza este código:
<?php
$productos = [...]; // array grande
$html = '';
for ($i = 0; $i < count($productos); $i++) {
$html = $html . '<div class="producto">';
$html = $html . '<h3>' . $productos[$i]['nombre'] . '</h3>';
$html = $html . '<p>' . $productos[$i]['precio'] . '</p>';
$html = $html . '</div>';
}
echo $html;
Ver solucion
<?php
declare(strict_types=1);
$productos = [...];
$partes = [];
foreach ($productos as $producto) {
$partes[] = sprintf(
'<div class="producto"><h3>%s</h3><p>%s</p></div>',
htmlspecialchars($producto['nombre'], ENT_QUOTES, 'UTF-8'),
htmlspecialchars((string) $producto['precio'], ENT_QUOTES, 'UTF-8')
);
}
echo implode('', $partes);
Mejoras aplicadas:
- Usar foreach en lugar de for con count()
- Acumular en array y usar implode (evita crear strings intermedios)
- Escapar HTML para seguridad
Ejercicio 3: Generador para archivo grande
Convierte esta función para usar un generador:
<?php
function obtenerUsuarios(PDO $pdo): array
{
$stmt = $pdo->query('SELECT * FROM usuarios');
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// Problema: si hay millones de usuarios, agota la memoria
Ver solucion
<?php
declare(strict_types=1);
function obtenerUsuarios(PDO $pdo): Generator
{
$stmt = $pdo->query('SELECT * FROM usuarios');
while ($usuario = $stmt->fetch(PDO::FETCH_ASSOC)) {
yield $usuario;
}
}
// Uso: procesa un usuario a la vez, memoria constante
foreach (obtenerUsuarios($pdo) as $usuario) {
procesarUsuario($usuario);
}
Has encontrado un error o tienes una sugerencia para mejorar esta leccion?
EscribenosTe está gustando el curso?
Tenemos cursos premium con proyectos reales y soporte personalizado.
Descubrir cursos premium