
El patrón Module/Nomodule es una técnica de entrega condicional de JavaScript que permite servir código moderno a navegadores compatibles y versiones transpiladas a aquellos que no lo son, mejorando rendimiento y compatibilidad sin romper funcionalidades. Esta aproximación se apoya en la capacidad de los navegadores modernos para interpretar módulos ES y en el atributo nomodule para bloquear la carga de scripts modernos en motores antiguos, lo que facilita el despliegue progresivo de nuevas características. En entornos de producción, combinar este patrón con herramientas de construcción y análisis de compatibilidad ayuda a reducir el tamaño de los paquetes y a mantener una base de código única que atienda diferentes generaciones de navegadores.
Introducción al patrón Module/Nomodule
El patrón Module/Nomodule separa dos variantes del mismo recurso JavaScript: una versión moderna que usa módulos ES (import/export) y otra versión transpilada o polifillada para navegadores antiguos, y se basa en atributos del elemento script para control de carga. Implementarlo correctamente implica generar dos builds durante la fase de construcción y usar el atributo type="module" para la versión moderna y nomodule para la alternativa, lo que permite una degradación elegante sin necesidad de detección por user-agent; la especificación y explicaciones prácticas están en la documentación de MDN. Esta estrategia es especialmente útil para aplicaciones que desean aprovechar características como carga diferida de módulos y scope de módulos, manteniendo soporte para navegadores que no entienden la sintaxis moderna según el estándar HTML.
La adopción de este patrón reduce la necesidad de polifills innecesarios para navegadores modernos, lo que mejora tiempos de carga y uso de CPU en dispositivos actuales; además permite implementar optimizaciones como HTTP/2 push o carga condicional basada en módulos sin lógica adicional en el cliente. Para equipos de desarrollo, el patrón clarifica la estrategia de builds y el ciclo de pruebas: una rama o pipeline produce el artefacto moderno y otra produce el artefacto legacy, lo que facilita la monitorización de errores específicos por variante y mejora la telemetría de experiencia de usuario.
Cómo funciona la carga condicional de JS
La carga condicional usando type="module" y nomodule se apoya en un principio simple: los navegadores que entienden módulos procesan y ejecutan únicamente los scripts con type="module", mientras que los navegadores antiguos solo cargarán aquellos con el atributo nomodule. Esto elimina la necesidad de detección por user-agent y reduce falsos positivos, pero requiere atención en la concatenación y en cómo se referencian los módulos para evitar cargas duplicadas o dependencias rotas; para una guía práctica sobre servir código moderno se puede consultar web.dev. Además, los módulos se ejecutan en modo estricto por defecto y poseen su propio scope, lo que cambia detalles de comportamiento como variables globales y hoisting, por lo que la equivalencia funcional entre builds debe verificarse.
En la práctica, el navegador evaluará primero si soporta type="module" y, si es así, cargará esos recursos y ignorará scripts marcados con nomodule; los navegadores que no reconozcan type="module" simplemente lo tratarán como script normal y ejecutarán los archivos con nomodule. Esta lógica proporciona una ruta de actualización segura para los desarrolladores: pueden introducir nuevas APIs y sintaxis en la build moderna sin romper la versión legacy, mientras que la telemetría y las pruebas A/B permiten medir el impacto en rendimiento y errores por segmento de navegador.
Sintaxis: script type=module y nomodule
La sintaxis básica consiste en incluir dos etiquetas script en el HTML: una con type="module" apuntando al bundle moderno y otra con nomodule apuntando al bundle transpilado o legacy, habitualmente con atributos adicionales como defer para control de ejecución. Un ejemplo típico es junto a , y la documentación de atributos de script en MDN explica las implicaciones de cada atributo. Es importante también considerar el uso de atributos de integridad y CORS en escenarios de CDN y cache, y validar que los header de servidor y las rutas no impidan la correcta entrega de cada variante.
Al usar módulos ES se habilitan importaciones asincrónicas y código dividido mediante import(), lo que facilita la carga lazy de secciones de la aplicación sólo cuando se necesitan, potenciando aún más la eficiencia del bundle moderno. En paralelo, el bundle legacy debe mantener la compatibilidad semántica y de API con la versión moderna, por lo que las transformaciones que aplican los transpiladores deben ser predictivas y testeadas, y la especificación HTML sirve como referencia para casos límite en la interpretación del navegador.
Estrategias para transpilar y entregar código
Existen varias estrategias para generar las dos variantes del código: pipelines separadas que producen un bundle moderno con target ES2017+ y otro transpileado a ES5, o una sola pipeline que realiza multiple-target builds usando herramientas como Babel o bundlers modernos como esbuild para velocidad. La elección entre generar builds completos o aplicar técnicas de differential serving depende del tamaño del proyecto, del coste de mantenimiento y del porcentaje de usuarios en navegadores legacy; herramientas de análisis automáticas y reports de compatibilidad en tiempo de build ayudan a tomar decisiones informadas. Además, el uso de source maps y etiquetado claro de artefactos facilita el diagnóstico de errores y la correlación entre versiones en producción.
Para la entrega, lo ideal es servir los archivos con nombres distintos y políticas de cache apropiadas desde un CDN, y en entornos avanzados emplear HTTP headers o service workers para optimizar la sustitución de bundles cuando sea necesario, aunque el patrón Module/Nomodule por sí solo ya reduce dependencias de runtime. También es recomendable automatizar la generación de builds mediante CI/CD y validar la integridad de los bundles a través de pruebas E2E en navegadores representativos, aprovechando herramientas de monitorización para detectar regresiones de compatibilidad o rendimiento tras cada despliegue.
Mejores prácticas y compatibilidad por navegador
Antes de implementar Module/Nomodule conviene auditar la base de usuarios y verificar soporte mediante indicadores como Can I Use para módulos ES y otras características críticas, asegurando que la fracción de navegadores legacy justifica el coste de mantener un build alternativo. La práctica común incluye servir la versión module por defecto a la mayoría de usuarios y mantener la cadena legacy solo para navegadores sin soporte, además de monitorizar errores específicos por variante para priorizar trabajos de modernización. También es importante emplear pruebas de regresión y pruebas automatizadas en navegadores clave para garantizar que las transformaciones del código no introduzcan discrepancias funcionales.
En cuanto a rendimiento, se recomienda minimizar la duplicación de lógica entre builds y aprovechar técnicas como tree shaking, carga diferida y compresión en ambos artefactos; asimismo, documentar la estrategia y los criterios de inclusión de polifills facilita la gobernanza del proyecto. Finalmente, mantener actualizadas las dependencias, instrumentar telemetría y revisar periódicamente los reports de compatibilidad permiten simplificar con el tiempo la necesidad del build legacy y mover la base de usuarios hacia la variante moderna de manera controlada.
El patrón Module/Nomodule es una solución práctica y escalable para servir JavaScript moderno sin sacrificar compatibilidad, siempre que se acompañe de pipelines de build bien organizadas y pruebas consistentes en los navegadores objetivo. Adoptarlo permite mejorar tiempos de carga y aprovechar características avanzadas de ES modules, mientras que una estrategia de entrega y monitoreo adecuada asegura una transición progresiva y controlada hacia código más moderno.