Ir al contenido principal

Manual de Performance y Tuning Avanzado en WordPress

Introducción: Más allá del caché superficial


En el ecosistema WordPress, la mayoría de los artículos sobre rendimiento se limitan a recomendar plugins de caché o CDN. Pero cuando gestionas sitios con tráfico alto, bases de datos complejas o arquitecturas distribuidas, necesitas una estrategia técnica profunda, no soluciones de “clic y olvida”. Este manual, escrito desde la experiencia en seguridad, sockets y PHP de bajo nivel, te guiará a través de los verdaderos puntos de fricción que afectan el rendimiento real de WordPress: la base de datos, el servidor web, las políticas CORS y la gestión eficiente de medios. Además, presentamos un plugin construido desde cero para resolver problemas específicos que ningún otro aborda de forma integrada.

---

## 1. Optimización de la Base de Datos: El Corazón del Rendimiento

WordPress depende de MySQL (o MariaDB) como motor de persistencia. Si la base de datos está mal configurada o fragmentada, ningún caché salvará tu sitio.

### 1.1. Índices inteligentes y consultas lentas

El primer paso es identificar consultas lentas. Activa el *slow query log*:

```ini
# En my.cnf
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1
```

Luego, analiza con `mysqldumpslow` o `pt-query-digest`. En WordPress, las tablas más problemáticas suelen ser:

- `wp_options`: Si contiene cientos de transients sin limpiar.
- `wp_postmeta`: Cuando se abusa de campos personalizados sin índices.
- `wp_comments`: En sitios con foros o blogs muy activos.

**Solución:** Añade índices compuestos donde sea necesario. Por ejemplo, si filtras posts por meta_key y meta_value:

```sql
ALTER TABLE wp_postmeta ADD INDEX idx_meta_key_value (meta_key, meta_value(191));
```

> ⚠️ **Nota de seguridad**: Nunca indexar valores completos de `meta_value` si contienen datos sensibles. Usa prefijos (`(191)`) para evitar exponer información.

### 1.2. Limpieza proactiva

WordPress no elimina transients expirados de forma automática si no se accede a ellos. Usa un cron real (no el pseudo-cron de WordPress) para ejecutar limpiezas:

```bash
# En crontab
0 2 * * * wp --path=/var/www/site transient delete --expired
```

También considera particionar tablas grandes (como `wp_posts`) por año si superas los 100k posts.

### 1.3. Motor de almacenamiento: InnoDB sí, MyISAM no

Asegúrate de que todas tus tablas usen **InnoDB**. MyISAM carece de transacciones, bloqueos de fila y recuperación ante fallos. Convierte con:

```sql
ALTER TABLE wp_options ENGINE=InnoDB;
```

---

## 2. Tuning del Servidor Web: Apache vs Nginx

La elección entre Apache y Nginx no es solo filosófica; tiene implicaciones reales en rendimiento bajo carga.

### 2.1. Apache: MPM y módulos innecesarios

Si usas Apache, evita el MPM `prefork` en entornos modernos. Usa `event`:

```apache
# En apache2.conf
LoadModule mpm_event_module modules/mod_mpm_event.so
```

Desactiva módulos que no uses (`mod_php` si usas PHP-FPM, `mod_autoindex`, `mod_status` en producción). Cada módulo consume memoria y CPU.

Configura adecuadamente:

```apache
<IfModule mpm_event_module>
    StartServers             2
    MinSpareThreads         25
    MaxSpareThreads         75
    ThreadLimit             64
    ThreadsPerChild         25
    MaxRequestWorkers      400
    MaxConnectionsPerChild 10000
</IfModule>
```

### 2.2. Nginx: Buffers, keepalive y gzip

Nginx brilla en servir contenido estático y manejar conexiones concurrentes. Configuración crítica:

```nginx
http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    
    keepalive_timeout 65;
    keepalive_requests 100;
    
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css application/json application/javascript;
    
    # Buffers para evitar escritura lenta al cliente
    client_body_buffer_size 128k;
    client_max_body_size 10M;
    client_header_buffer_size 1k;
    large_client_header_buffers 4 4k;
}
```

**Importante**: Usa `fastcgi_cache` en lugar de plugins de caché cuando sea posible. Es más rápido y consume menos memoria.

---

## 3. CORS: No es solo un error de consola

Las políticas CORS mal configuradas no solo rompen APIs o embeds; también generan solicitudes OPTIONS innecesarias que consumen recursos del servidor.

### 3.1. Configuración mínima y segura

En **Nginx**:

```nginx
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
    add_header 'Access-Control-Allow-Origin' 'https://tudominio.com';
    add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS';
    add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
    add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
}
```

En **Apache**:

```apache
<FilesMatch "\.(js|css|png|jpg|jpeg|gif|ico|svg)$">
    Header set Access-Control-Allow-Origin "https://tudominio.com"
    Header set Access-Control-Allow-Methods "GET, OPTIONS"
</FilesMatch>
```

> 🔒 **Práctica segura**: Nunca uses `Access-Control-Allow-Origin: *` en contenido que incluya cookies o autenticación. Siempre especifica dominios explícitos.

---

## 4. Gestión de Imágenes en la Media Library: El peso invisible

La biblioteca de medios es el mayor consumidor de ancho de banda y disco en WordPress. Pero el problema no es solo el tamaño; es la *multiplicidad* de formatos generados.

### 4.1. Limitar tamaños de imagen innecesarios

Desactiva tamaños que no usas en tu tema:

```php
function custom_image_sizes($sizes) {
    unset($sizes['medium_large']); // 768px, rara vez usado
    unset($sizes['1536x1536']);   // Full HD+ (solo si no lo usas)
    unset($sizes['2048x2048']);
    return $sizes;
}
add_filter('intermediate_image_sizes_advanced', 'custom_image_sizes');
```

### 4.2. WebP + Lazy Load nativo

Desde PHP 7.4+, puedes generar WebP directamente con GD o Imagick. Pero WordPress no lo hace por defecto. Solución: intercepta el proceso de subida.

Además, usa el atributo `loading="lazy"` en imágenes:

```php
function add_lazy_loading($html, $id) {
    return str_replace('<img ', '<img loading="lazy" ', $html);
}
add_filter('get_image_tag', 'add_lazy_loading', 10, 2);
```

### 4.3. Servir imágenes desde un subdominio sin cookies

Crea un subdominio estático (ej. `static.tudominio.com`) y configúralo en Nginx/Apache para **no enviar cookies ni sesiones**. Esto reduce el overhead en cada solicitud de imagen.

---

## 5. El Plugin Construido: **WP-PerfCore**

Después de años de depurar sitios lentos, decidí construir un plugin que aborde estos problemas de forma coherente, sin sobrecargar el sistema. **WP-PerfCore** no es otro caché; es un *tuner de bajo nivel*.

### 5.1. Arquitectura

- **Sin interfaz de usuario**: Se configura vía constantes en `wp-config.php`.
- **Zero overhead en frontend**: Solo actúa durante subidas, cron real o inicialización de admin.
- **PHP 8.0+ nativo**: Usa typed properties, JIT-friendly code y evita globals.

### 5.2. Funcionalidades clave

#### a) **Smart Transient Cleaner**
Ejecuta limpieza de transients usando un socket Unix para evitar HTTP overhead. Se integra con systemd timers.

#### b) **Image Optimizer Pipeline**
Al subir una imagen:
1. Genera solo los tamaños definidos por el tema.
2. Convierte a WebP usando Imagick (si está disponible).
3. Añade metadatos EXIF mínimos (mejora SEO y privacidad).
4. Registra hashes para evitar duplicados.

#### c) **DB Index Advisor**
Analiza las consultas más frecuentes (usando `performance_schema` si está activo) y sugiere índices vía WP-CLI:

```bash
wp perfcore db suggest-indexes
```

#### d) **CORS Policy Enforcer**
Aplica reglas CORS basadas en dominios permitidos definidos en `WP_PERFCORE_CORS_DOMAINS`.

### 5.3. Código de ejemplo: Conversión WebP

```php
class WebP_Converter {
    public static function maybe_convert( $metadata, $attachment_id ) {
        if ( ! self::should_convert( $metadata ) ) return $metadata;

        $file = get_attached_file( $attachment_id );
        if ( ! $file || ! file_exists( $file ) ) return $metadata;

        $webp_path = trailingslashit( dirname( $file ) ) . pathinfo( $file, PATHINFO_FILENAME ) . '.webp';

        if ( extension_loaded( 'imagick' ) ) {
            $image = new Imagick( $file );
            $image->setImageFormat( 'webp' );
            $image->setImageCompressionQuality( 82 );
            $image->writeImage( $webp_path );
            $image->destroy();
        } elseif ( function_exists( 'imagewebp' ) ) {
            // Fallback a GD
            $source = self::get_gd_resource( $file );
            imagewebp( $source, $webp_path, 82 );
            imagedestroy( $source );
        }

        if ( file_exists( $webp_path ) ) {
            $metadata['sizes']['webp'] = [
                'file' => basename( $webp_path ),
                'width' => $metadata['width'],
                'height' => $metadata['height'],
            ];
        }

        return $metadata;
    }
}
add_filter( 'wp_generate_attachment_metadata', [ WebP_Converter::class, 'maybe_convert' ], 10, 2 );
```

> ✅ **Ventaja**: No requiere procesos externos ni llamadas a APIs. Todo en PHP puro, con fallbacks seguros.

---

## Conclusión: Rendimiento es disciplina, no magia

Mejorar el rendimiento de WordPress no se trata de instalar un plugin mágico. Se trata de entender cómo interactúan la base de datos, el servidor web, las políticas de red y el manejo de recursos estáticos. Con una configuración sólida, limpieza proactiva y herramientas construidas con precisión (como **WP-PerfCore**), puedes lograr tiempos de carga consistentes incluso bajo picos de tráfico.

En *Hooked*, creemos que el rendimiento es una extensión de la seguridad: un sitio lento es un sitio vulnerable. Porque cuando el sistema está al límite, los errores se multiplican, los logs se saturan y los atacantes encuentran grietas.

Optimiza con inteligencia. Mide con rigor. Y nunca confíes en soluciones que no entiendes.


Comentarios

Publicar un comentario

Entradas más populares de este blog

WPDB Table Renderer

WPDB Table Renderer septiembre 28, 2025 La tabla que WordPress olvidó Por HOOKED / Investigación Técnica En el ecosistema WordPress, donde los plugins visuales dominan y los dashboards se multiplican, hay una necesidad que rara vez se aborda con rigor: mostrar datos de forma profesional . No hablamos de gráficos decorativos ni de interfaces sobrecargadas. Hablamos de algo más básico, más estructural: tablas que funcionen . Ahí entra en escena WPDB Table Renderer , una clase PHP que transforma cualquier resultado de $wpdb en una tabla interactiva, ordenable, filtrable y exportable. Sin dependencias externas. Sin frameworks. Sin adornos. Solo código limpio, funcional y 100 % nativo. El problema: datos sin forma WordPress ofrece acceso directo a su base de datos mediante $wpdb , pero lo que devuelve es crudo: arrays sin formato, sin interacción, sin contexto visual. Los desarrolladores deben construir desde cero interfaces para mostrar esos datos… o recurrir a soluciones...

SDL Bridge SQL seguro, trazable y libre para WordPress

  El problema estructural en WordPress WordPress ha revolucionado la creación de sitios web, pero su relación con las bases de datos sigue siendo precaria. Aunque ofrece una API llamada $wpdb para ejecutar consultas SQL, esta interfaz está lejos de ser suficiente para entornos profesionales. El acceso a datos en WordPress suele estar acoplado directamente al código PHP, lo que genera una serie de problemas estructurales que se agravan a medida que el proyecto crece. El primer problema es el acoplamiento excesivo entre lógica de negocio y presentación. Las consultas SQL se escriben directamente en funciones PHP, sin separación clara de responsabilidades. Esto dificulta la reutilización, la auditoría y la evolución del sistema. El segundo problema es la falta de validación formal : los parámetros se interpolan sin control tipado, lo que abre la puerta a errores silenciosos y vulnerabilidades como inyecciones SQL. El tercero es la ausencia de trazabilidad : no hay for...