Tienes un menú sólido. Los clientes lo usan. Funciona. Pero cada vez que pruebas tu tienda con PageSpeed Insights, Google señala un archivo específico: tu script de menú. La etiqueta dice “render-blocking JavaScript”, el archivo pesa 120KB, y está agregando 600 milisegundos a tu LCP móvil. Sabes que necesitas arreglarlo, pero la última vez que alguien sugirió usar defer, el menú se rompió. La mitad de los dropdowns dejaron de funcionar, el toggle móvil no hizo nada, y lo revertiste en menos de una hora.
Este es un problema común. Los scripts que bloquean renderización son uno de los cuellos de botella de rendimiento más impactantes en tiendas Shopify, y el menú es un infractor frecuente. Pero diferir JavaScript no es solo agregar un atributo a una etiqueta script. Si lo haces mal, el menú no funcionará. Si lo haces bien, puedes reducir medio segundo de tu LCP sin tocar una sola línea de lógica del menú.
Este artículo explica qué significa realmente render-blocking, por qué async y defer se comportan de manera diferente, y cómo cargar tu script de menú sin romper su funcionalidad.
- Los scripts que bloquean renderización detienen toda la renderización de la página hasta que se descargan, analizan y ejecutan.
deferdescarga scripts en paralelo pero retrasa la ejecución hasta que se completa el análisis HTML.asyncdescarga y ejecuta inmediatamente, lo cual puede romper menús que dependen del orden del DOM.type="module"difiere por defecto y admite características modernas de JavaScript.- La mayoría de roturas de menú después de diferir son causadas por timing de inicialización, no por el atributo defer en sí.
Qué Significa Realmente Render-Blocking
Cuando un navegador encuentra una etiqueta <script> en el HTML sin atributos de carga, se detiene todo. Pausa el análisis HTML, descarga el script, lo analiza, lo ejecuta, y solo entonces continúa construyendo la página. Esto se llama comportamiento sincrónico o que bloquea renderización.
La lógica detrás de este comportamiento es antigua pero razonable: JavaScript puede modificar el DOM usando document.write(), así que el navegador asume que cada script podría cambiar la estructura de la página y por lo tanto debe ejecutarse antes de proceder.
Aquí está el problema. Tu script de menú no usa document.write(). No necesita ejecutarse antes de que el resto de la página se analice. Pero el navegador no lo sabe, así que bloquea de todas formas.
Según la propia documentación de Chrome sobre optimización de Largest Contentful Paint, eliminar JavaScript que bloquea renderización es una de las mejoras de mayor impacto que puedes hacer. Cuando un script en el head del documento bloquea durante 400 milisegundos, cada elemento debajo de él en el DOM — tu imagen hero, tu titular, tu cuadrícula de productos — espera 400 milisegundos más para pintarse.
Las Tres Estrategias de Carga: Default, Defer y Async
Hay tres formas en que un navegador puede cargar un script.
Default (Sincrónico)
<script src="menu.js"></script>
El navegador detiene el análisis HTML, descarga menu.js, lo analiza, lo ejecuta, y luego reanuda. Esto bloquea la renderización durante toda la duración.
Defer
<script src="menu.js" defer></script>
El navegador descarga menu.js en paralelo con el análisis HTML pero no lo ejecuta hasta que se haya analizado todo el documento HTML. Múltiples scripts diferidos se ejecutan en el orden en que aparecen en el HTML.
Async
<script src="menu.js" async></script>
El navegador descarga menu.js en paralelo con el análisis HTML y lo ejecuta tan pronto como se complete la descarga, incluso si el análisis HTML no ha terminado. El orden de ejecución es impredecible si múltiples scripts usan async.
Aquí hay una tabla que resume el comportamiento:
| Atributo | Descarga | Ejecución | Bloquea Análisis HTML | Orden de Ejecución |
|---|---|---|---|---|
| Ninguno (default) | Bloquea análisis | Inmediata | Sí | Secuencial |
defer |
Paralela | Después de análisis HTML | No | Secuencial |
async |
Paralela | Tan pronto como esté listo | Sí (durante ejecución) | Impredecible |
Para la mayoría de menús de navegación, defer es la opción correcta. Mantiene los scripts en orden, lo cual importa cuando un script depende de otro (por ejemplo, un script de menú que depende de una librería de utilidades). Y garantiza que el DOM está listo antes de que se ejecute el script, lo cual es crítico para cualquier código que consulta elementos.
Por Qué Defer Rompe Algunos Menús (Y Cómo Arreglarlo)
Agregas defer a tu etiqueta script de menú, recarga la página, y el menú no funciona. Los dropdowns no se abren. El toggle móvil no hace nada. Removes defer, y todo funciona de nuevo. ¿Qué pasó?
El problema es casi siempre timing de inicialización. Aquí están las tres causas más comunes.
Problema 1: El Script Se Ejecuta Antes de que el JavaScript del Tema Esté Listo
Muchos temas Shopify tienen un objeto JavaScript global o una función de inicialización de la que dependen otros scripts. Si tu script de menú se ejecuta antes de que ese objeto se cree, falla.
Ejemplo:
// theme.js (cargado primero)
window.Theme = { utils: { ... } };
// menu.js (cargado segundo, espera que Theme exista)
Theme.utils.initDropdown();
Si menu.js se difiere pero theme.js no, menu.js podría ejecutarse antes de que theme.js termine de ejecutarse, y Theme será undefined.
Arreglo: Difiere ambos scripts, o asegúrate de que menu.js comprueba las dependencias antes de inicializar.
if (typeof Theme !== 'undefined') {
Theme.utils.initDropdown();
}
Problema 2: Los Scripts Inline Se Ejecutan Antes de los Scripts Diferidos
Los scripts inline (JavaScript escrito directamente en el HTML, no cargado desde un archivo externo) se ejecutan en orden de documento conforme se encuentran. Los scripts diferidos se ejecutan después de que se haya analizado todo el HTML, lo cual significa después de todos los scripts inline.
Si tienes un script inline que intenta inicializar el menú, y la librería de menú está diferida, la inicialización fallará.
Arreglo: Mueve la lógica de inicialización dentro del script diferido, o envuelve la inicialización inline en un listener DOMContentLoaded.
document.addEventListener('DOMContentLoaded', function() {
if (typeof MenuApp !== 'undefined') {
MenuApp.init();
}
});
Problema 3: Condiciones de Carrera con Múltiples Scripts Diferidos
Si tu menú depende de una librería de utilidades, y ambos están diferidos, deberían ejecutarse en el orden en que aparecen en el HTML. Pero si la librería de utilidades se carga asincronamente (usando async en lugar de defer), el orden de ejecución se vuelve impredecible.
Arreglo: Usa defer para todos los scripts interdependientes, no async. Si debes usar async, agrega comprobaciones explícitas de dependencias o usa importaciones dinámicas.
Type=”module”: La Alternativa Moderna
JavaScript moderno admite módulos ES6, que tienen carga diferida incorporada.
<script type="module" src="menu.js"></script>
Los scripts con type="module" se diferencian automáticamente. Se descargan en paralelo y se ejecutan después de que se completa el análisis HTML, tal como defer. También admiten import y export, lo que hace que la gestión de dependencias sea explícita.
Si tu script de menú se escribe como un módulo, puedes importar dependencias directamente:
import { initDropdown } from './dropdown.js';
initDropdown();
Esto elimina los problemas de timing que causan roturas con el defer tradicional, porque el navegador sabe exactamente qué scripts dependen de cuáles.
La desventaja: los navegadores antiguos (Internet Explorer, versiones muy antiguas de Safari) no admiten módulos. Pero según Can I Use, más del 95% del tráfico global de navegadores admite módulos ES6 a partir de 2025. Para la mayoría de tiendas Shopify, esto es seguro.
Consideraciones Específicas de Shopify
Los temas Shopify cargan scripts de diferentes formas dependiendo de la arquitectura del tema. Aquí está cómo funciona diferir en los patrones más comunes.
Etiquetas Script en Theme.liquid
Si tu script de menú se carga en theme.liquid así:
{{ 'menu.js' | asset_url | script_tag }}
El filtro script_tag no agrega defer por defecto. Necesitas reemplazarlo con una etiqueta <script> manual:
<script src="{{ 'menu.js' | asset_url }}" defer></script>
Bloques de Embed de App
Si tu menú se instala como un embed de app (común para apps de menú de terceros), la app controla cómo se carga su script. No puedes agregar defer tú mismo. Necesitas verificar con el desarrollador de la app o cambiar a una app de menú que ya carga scripts de forma asincrónica.
Herramientas como Navi+ cargan su JavaScript con defer por defecto, que es una razón por la que funcionan mejor en pruebas de LCP que las apps de menú heredadas.
Renderización de Secciones y Scripts Diferidos
Los temas Shopify 2.0 cargan secciones dinámicamente. Si una sección incluye un script diferido, y la sección se agrega a la página después de la carga inicial (por ejemplo, vía AJAX para vista rápida o desplazamiento infinito), el script podría no ejecutarse como se espera.
Arreglo: Usa DOMContentLoaded solo para la carga inicial de página. Para secciones agregadas dinámicamente, dispara la inicialización manualmente usando un evento personalizado u observer de mutaciones.
Cómo Probar si Defer Funciona
Antes de desplegar la carga de scripts diferidos a tu tienda en vivo, pruébalo a fondo. Aquí hay una lista de verificación.
- Chrome de escritorio: Abre tu tienda, abre DevTools, ve a la pestaña Network, recarga, y verifica que el script de menú se descarga en paralelo con otros recursos y no bloquea el documento.
- Simulación móvil: En DevTools, cambia a emulación móvil (iPhone o Android), limita la red a “Fast 3G,” y recarga. El menú aún debería inicializarse correctamente.
- Entre navegadores: Prueba en Safari, Firefox, y Edge. Los scripts de módulo se comportan ligeramente diferente en Safari.
- Dispositivo real: Abre la tienda en un teléfono real sobre una conexión móvil real. La emulación es útil, pero la latencia del mundo real expone bugs de timing que las pruebas de escritorio pierden.
- Comprobación de funcionalidad: Abre cada dropdown, prueba el toggle de menú móvil, haz clic en cada enlace, y verifica que nada se rompe.
Antes de que hagas pushEjecuta una prueba de PageSpeed Insights en la misma página antes y después de habilitar defer. Deberías ver una caída medible en LCP y una reducción de recursos que bloquean renderización. Si LCP no mejora, el script podría no haber sido el cuello de botella, o podría haber otros recursos de bloqueo que abordar.
Cuándo Async Tiene Sentido (Y Cuándo No)
async es apropiado para scripts que son completamente autónomos y no dependen del orden del DOM u otros scripts. Ejemplos incluyen rastreadores de analítica, widgets de chat, y algunos scripts de anuncios.
Para menús de navegación, async es arriesgado. El menú necesita que el DOM esté listo, a menudo depende de utilidades del tema, y usualmente necesita ejecutarse antes de que el cliente interactúe con la página. Si el script se ejecuta mientras el HTML aún se está analizando, podría intentar consultar elementos que aún no existen.
Si estás considerando async para un script de menú, el script debe:
- No depender de ningún otro script
- No consultar elementos del DOM (o envolver todas las consultas en una comprobación
DOMContentLoaded) - Ser lo suficientemente pequeño que su tiempo de ejecución no bloquee el análisis
La mayoría de scripts de menú no cumplen con estos criterios. Quédate con defer o type="module" a menos que tengas una razón específica para usar async.
Cómo Se Ve una Buena Carga de Script de Menú
Un script de menú correctamente optimizado en una tienda Shopify se ve así:
<script src="{{ 'menu.js' | asset_url }}" defer></script>
O, si usas módulos:
<script type="module" src="{{ 'menu.js' | asset_url }}"></script>
El script se descarga en paralelo con el resto de la página, se ejecuta después de que el DOM está listo, y no bloquea la imagen hero ni ningún otro elemento LCP para que se pinte.
En la línea de tiempo Performance de Chrome DevTools, deberías ver que la imagen hero se pinta antes de que se ejecute el script de menú. Si el script de menú se ejecuta primero, aún está bloqueando.
La mayoría de tiendas pueden cortar 300 a 600 milisegundos de su LCP móvil simplemente cambiando de carga de scripts sincrónica a diferida. El menú aún funciona. El cliente no nota una diferencia. Pero Google sí, y lo que es más importante, el navegador sí. La página se vuelve visible más rápido, que es el punto completo.
Este artículo es parte de la guía más amplia en Menú y LCP: cómo la navegación bloquea tu largest contentful paint.