El portal de reservas llevaba meses en producción. Reservas de actividades al aire libre: rutas, talleres, excursiones. Todo funcionaba. Los formularios respondían, el buscador filtraba bien, las pasarelas de pago procesaban sin errores. No había quejas del equipo ni tickets de usuarios. Y sin embargo, algo estaba fuera de sitio. Las páginas más simples —aviso legal, política de privacidad, una landing informativa sin formularios— tardaban más de lo razonable en pintar. No era un fallo visible. Era un peso que no correspondía.
Lo noté al cargar la página de condiciones de uso desde una conexión media. El navegador tiraba de recursos como si aquella página tuviera la misma complejidad que la home. La barra de progreso se detenía más de lo esperado antes del primer renderizado. Revisé otras páginas estáticas y el patrón se repetía: los widgets registrados en el tema aparecían vinculados a recursos que no tenían sentido en esas URLs. El contenido era texto plano con un par de encabezados, pero la carga decía otra cosa.

Algo estaba inyectando peso donde no hacía falta. Y la primera sospecha apuntaba lejos del lugar real.
Operario: WordPin
Archivo del caso: Los widgets que cargaban JavaScript innecesario
Empresa o negocio: Portal de reservas de actividades
Archivado en: Rendimiento y Carga
Nivel: Intermedio
Lo que los widgets arrastraban en cada petición
Empecé por lo más directo: abrir DevTools y revisar la pestaña Network en una página sin contenido dinámico. La de política de privacidad, por ejemplo. Texto, un par de enlaces internos y nada más. El resultado fue este:
// Red - política de privacidad
Requests: 34
JS files: 14
CSS files: 7
Transferred: 812 KB
DOMContentLoaded: 2.4s
Catorce archivos JavaScript en una página que no necesitaba ni uno para funcionar. Siete hojas de estilo. Más de 800 KB transferidos para pintar un texto legal. Eso no era normal. Comparé con la home, que sí tenía slider, mapa, buscador y calendario de disponibilidad:
// Red - home
Requests: 41
JS files: 18
CSS files: 9
Transferred: 1.1 MB
DOMContentLoaded: 2.9s
La diferencia entre ambas páginas era mínima en número de peticiones. Un portal que muestra calendario, mapa interactivo y carrusel en portada debería pesar bastante más que una página de texto. Pero la brecha era ridícula. Aquí había algo que se estaba cargando en todas partes sin distinción.
Primera pista: el plugin de reservas
El sistema de reservas era el componente más pesado del sitio. Gestionaba disponibilidad, calendarios, pagos y confirmaciones. Era lógico sospechar que sus scripts se cargaban globalmente. Activé Query Monitor y filtré por la pestaña de Scripts & Styles en la página de política de privacidad. Busqué cualquier archivo vinculado al plugin de reservas.
Aparecían dos archivos CSS del plugin, pero ningún JavaScript suyo. Los estilos eran residuales: los cargaba para un bloque de disponibilidad que ni siquiera se renderizaba en esa página. Peso menor, unos 18 KB combinados. No era lo que estaba disparando las 14 peticiones JS. Desactivé el plugin en staging para confirmar y el número de scripts apenas bajó de 14 a 13. La reserva no era la culpable principal.
La caché como segundo sospechoso
La segunda hipótesis fue la capa de caché. El sitio usaba un plugin de caché de página con minificación y combinación de archivos activada. Si la caché estaba sirviendo una versión empaquetada con todos los recursos, podía explicar que páginas simples arrastraran el peso de páginas complejas. Purgué la caché completa, desactivé la minificación y cargué la página de privacidad con bypass de caché añadiendo un parámetro aleatorio en la URL.
https://ejemplo.com/politica-de-privacidad/?nocache=82741El resultado fue prácticamente idéntico: 14 archivos JS, mismo peso, mismo tiempo de carga. La caché no estaba inflando nada. Simplemente conservaba lo que WordPress ya generaba de base. Esto no encajaba. El problema existía antes de que la caché interviniera, así que venía de más atrás en la cadena de encolado.
Un sitio puede responder rápido desde la caché y aun así estar entregando una respuesta innecesariamente pesada.
Con las dos pistas descartadas, volví a Query Monitor. Esta vez no filtré por plugin. Miré la lista completa de scripts encolados en la página de privacidad y empecé a anotar los handles y su origen. Y ahí cambió todo.
El origen estaba en el propio tema
La lista de scripts que Query Monitor mostraba en la página de privacidad incluía archivos que no pertenecían a ningún plugin instalado. Sus handles empezaban con prefijos propios del tema activo: theme-maps, theme-calendar-init, theme-slider, theme-reviews-carousel. Cada uno correspondía a una funcionalidad visual concreta que solo tenía sentido en determinadas secciones del sitio.
Lo que los widgets estaban encolando sin condición
Revisé el archivo functions.php del tema y encontré el bloque que lo explicaba todo. Los widgets del tema —mapa, calendario, carrusel de opiniones, slider de actividades— registraban y encolaban sus scripts y estilos de forma global, sin ninguna condición. El hook wp_enqueue_scripts los cargaba en todas las páginas del sitio, independientemente de si esos widgets estaban activos o visibles en la URL solicitada.
// functions.php del tema (simplificado)
function theme_load_widget_assets() {
wp_enqueue_script('theme-maps', get_template_directory_uri() . '/js/maps-widget.js', array('jquery'), '1.0', true);
wp_enqueue_script('theme-calendar-init', get_template_directory_uri() . '/js/calendar-init.js', array('jquery'), '1.0', true);
wp_enqueue_script('theme-slider', get_template_directory_uri() . '/js/slider.js', array(), '1.0', true);
wp_enqueue_script('theme-reviews-carousel', get_template_directory_uri() . '/js/reviews-carousel.js', array('jquery'), '1.0', true);
wp_enqueue_style('theme-maps-css', get_template_directory_uri() . '/css/maps-widget.css');
wp_enqueue_style('theme-calendar-css', get_template_directory_uri() . '/css/calendar.css');
wp_enqueue_style('theme-slider-css', get_template_directory_uri() . '/css/slider.css');
}
add_action('wp_enqueue_scripts', 'theme_load_widget_assets');
Cuatro scripts y tres hojas de estilo, cargados en cada página sin excepción. Algunos de ellos dependían de jQuery, lo que arrastraba además esa librería incluso cuando no hacía falta. El peso combinado de estos archivos más sus dependencias rondaba los 340 KB en transferencia. En una página de texto plano, eso representaba casi el 42 % del peso total.
El impacto no era solo de peso. Cada script bloqueaba o retrasaba el renderizado. El navegador tenía que descargar, parsear y ejecutar código JavaScript que no iba a usar. El DOMContentLoaded se disparaba medio segundo más tarde de lo necesario y el Largest Contentful Paint se resentía. En términos de experiencia de usuario, un portal de reservas necesita que la navegación sea rápida, especialmente en móvil. Un visitante que llega desde una búsqueda y tarda más de tres segundos en ver contenido útil tiene muchas probabilidades de abandonar.
WordPress no carga lo que el usuario necesita. Carga lo que alguien le dijo que encolara.
Los widgets no tenían ningún fallo interno. Funcionaban perfectamente en las páginas donde se usaban. El problema era que sus recursos viajaban con cada petición, como si el tema asumiera que cualquier página del sitio podía necesitarlos en cualquier momento. Una decisión de desarrollo cómoda, pero costosa.
Condicionar la carga sin desmontar lo que funcionaba
La solución más directa parecía obvia: desenganchar los scripts con wp_dequeue_script en las páginas que no los necesitaban. Pero hacerlo así, a base de exclusiones, era frágil. Cada vez que se añadiera una página nueva o se moviera un widget, habría que actualizar las condiciones manualmente. Además, el tema podía sobrescribir esos cambios en una actualización futura si se tocaba directamente el functions.php.
Lo que no podía limpiarse de golpe
La primera tentación fue crear un plugin auxiliar que desregistrara todos los scripts del tema y los volviera a encolar solo en la home y en las páginas de actividad. Pero eso asumía que los widgets solo se usaban ahí. Revisé todas las áreas de widgets del tema y encontré que el carrusel de opiniones aparecía también en la página de contacto y el mapa se mostraba en tres landings específicas. Limpiar en bloque habría roto esas secciones.
La solución que apliqué fue un mu-plugin ligero con condicionales combinados: primero comprobaba si el widget estaba activo en el sitio y después limitaba la carga a las páginas donde ese componente podía aparecer. No se trataba de borrar recursos a ciegas, sino de devolver cada script a su territorio lógico.
// mu-plugins/theme-conditional-assets.php
add_action('wp_enqueue_scripts', 'conditional_theme_widget_assets', 20);
function conditional_theme_widget_assets() {
// Desencolar los scripts globales del tema
wp_dequeue_script('theme-maps');
wp_dequeue_style('theme-maps-css');
wp_dequeue_script('theme-calendar-init');
wp_dequeue_style('theme-calendar-css');
wp_dequeue_script('theme-slider');
wp_dequeue_style('theme-slider-css');
wp_dequeue_script('theme-reviews-carousel');
// Reencolar solo donde cada widget puede aparecer
if ( is_active_widget(false, false, 'theme_maps_widget', true) && ( is_front_page() || is_page(array('contacto', 'como-llegar', 'actividades-norte')) ) ) {
wp_enqueue_script('theme-maps');
wp_enqueue_style('theme-maps-css');
}
if ( is_active_widget(false, false, 'theme_calendar_widget', true) && ( is_front_page() || is_singular('actividad') ) ) {
wp_enqueue_script('theme-calendar-init');
wp_enqueue_style('theme-calendar-css');
}
if ( is_front_page() ) {
wp_enqueue_script('theme-slider');
wp_enqueue_style('theme-slider-css');
}
if ( is_active_widget(false, false, 'theme_reviews_widget', true) && ( is_front_page() || is_page('contacto') ) ) {
wp_enqueue_script('theme-reviews-carousel');
}
}
Usé prioridad 20 en el hook para asegurar que el desencole se ejecutara después del encolado original del tema. Coloqué el archivo en mu-plugins para que sobreviviera a actualizaciones del tema sin depender de un child theme que no existía en el proyecto.
Después de aplicar el cambio, volví a la página de privacidad con DevTools:
// Red - política de privacidad (tras el cambio)
Requests: 21
JS files: 6
CSS files: 4
Transferred: 438 KB
DOMContentLoaded: 1.5s
De 14 archivos JavaScript a 6. De 812 KB a 438 KB. El DOMContentLoaded bajó casi un segundo. La página no quedó desnuda: seguían cargando estilos base, fuentes, analítica y recursos comunes del tema. Pero la parte absurda —los widgets que no pintaban nada allí— ya había desaparecido.
Comprobé la home y las páginas de actividad: todo seguía funcionando. El calendario cargaba, el mapa pintaba, el carrusel rotaba. Los widgets hacían exactamente lo mismo que antes, pero solo donde debían hacerlo.
Validé el resultado en tres navegadores, en móvil y con caché desactivada. También revisé con Query Monitor que no quedaran handles huérfanos ni dependencias rotas. Todo limpio. El sitio pesaba lo que tenía que pesar en cada URL.
Los widgets no estaban rotos. Solo habían aprendido a viajar demasiado.

💡 Qué he aprendido
Este caso me recordó algo que debería tener siempre presente: la cola de scripts de WordPress no discrimina por sí sola. Si un tema o un plugin encola un recurso sin condición, ese recurso viaja con cada página del sitio, sin importar si alguien lo va a usar o no. Los widgets funcionaban bien, no daban errores, no rompían nada. Pero su huella se extendía mucho más allá de donde tenía sentido.
Aprendí a no dar por hecho que el peso de una página viene solo de su contenido visible. A veces viene de componentes que están registrados en otro lugar y que nadie se molestó en condicionar. Revisar la pestaña de scripts encolados debería ser una de las primeras paradas cuando los tiempos de carga no cuadran con la complejidad real de la página.
También confirmé que un mu-plugin con lógica condicional puede ser más robusto que editar el tema directamente. Es una intervención limpia, portable y resistente a actualizaciones. A veces la mejor solución no toca el código original: lo intercepta justo después.
Cuaderno de WordPin
Forense WordPress
🧩 Notas técnicas
⬋⬋
Portal de reservas de actividades con tiempos de carga desproporcionados en páginas estáticas. La pestaña Network mostraba 14 archivos JavaScript en una página de texto plano, con más de 800 KB transferidos para contenido que no necesitaba scripts.
La causa estaba en el functions.php del tema, que encolaba los recursos de todos los widgets de forma global. La corrección se aplicó mediante un mu-plugin con condicionales por widget y por página, reduciendo las peticiones JS a 6 y el peso total a 438 KB en esas URLs.
🔎 Pistas detectadas
⬋⬋
- Página de política de privacidad con 14 archivos JS y 812 KB transferidos sin contenido dinámico.
- Diferencia mínima de peticiones entre la home completa y páginas de texto plano.
- Handles de scripts en Query Monitor con prefijo del tema activo presentes en todas las URLs.
- DOMContentLoaded casi un segundo más alto de lo justificable por el contenido real de la página.
- Bypass de caché sin cambios en el número de scripts cargados, descartando la capa de caché como origen.
🚫 Errores en WordPress detectados
⬋⬋
- Scripts de widgets encolados globalmente sin condición: cada página del sitio cargaba recursos de mapa, calendario, slider y carrusel aunque no los usara.
- Dependencia de jQuery arrastrada innecesariamente: tres de los cuatro scripts declaraban jQuery como dependencia, forzando su carga en páginas sin interactividad.
- Ausencia de condicionales en wp_enqueue_scripts: el tema no usaba is_active_widget, is_page ni is_front_page para discriminar la carga.
- Peso de renderizado innecesario: 340 KB de JavaScript parseado y ejecutado sin efecto visible, penalizando LCP y tiempo de interacción.
- Ausencia de capa de abstracción sobre el tema: cualquier personalización de scripts requería editar directamente el functions.php.
📘 Glosario de WordPress
⬋⬋
- wp_enqueue_scripts: hook de acción donde temas y plugins registran y encolan archivos CSS y JavaScript para el frontend.
- wp_dequeue_script: función que retira un script previamente encolado, impidiendo que se cargue en la página actual.
- is_active_widget: función condicional que comprueba si un widget específico está asignado a alguna zona activa del sitio.
- mu-plugins: directorio de plugins obligatorios que WordPress carga automáticamente sin necesidad de activación desde el panel.
- DOMContentLoaded: evento del navegador que se dispara cuando el HTML ha sido completamente parseado, sin esperar a imágenes o subrecursos.
📏 Mini guía práctica
⬋⬋
- Abre Query Monitor en una página simple y revisa la pestaña Scripts para identificar handles que no deberían estar ahí.
- Compara el número de peticiones JS entre la home y una página estática con DevTools Network para detectar carga innecesaria.
- Localiza en functions.php del tema o del plugin los bloques wp_enqueue_script que no usen condicionales de página o widget.
- Crea un archivo en mu-plugins que desencole los scripts globales y los reencole solo donde sus widgets puedan aparecer.
- Valida tras el cambio que las páginas con widgets siguen funcionando y que las páginas sin ellos han reducido peticiones y peso.
Deja una respuesta
El plugin de caché y el HTML incompleto · WordPin 002
El .htaccess en WordPress que redirigía todo a la home · WordPin 007
Expedientes y Archivos