Leccion 67 de 75 12 min de lectura

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.

ini
; 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
<?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";
}
OPcache en produccion

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
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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);
}

Te está gustando el curso?

Tenemos cursos premium con proyectos reales y soporte personalizado.

Descubrir cursos premium