Etiqueta script en HTML: JavaScript, módulos, async y defer
La etiqueta <script> en HTML incorpora código ejecutable, referencia un archivo de scripts o contiene un bloque de datos cuando su tipo no es ejecutable. Úsala para añadir una función que HTML por sí solo no realiza, como actualizar un resultado al pulsar un botón. La etiqueta conecta el documento con el código; las instrucciones y los cálculos se escriben en JavaScript, que es un lenguaje distinto de HTML.
Ejemplo mínimo: cargar un archivo con defer
<script src="/ejemplos/aviso.js" defer></script>
Coloca este elemento en el <head> de un documento servido por HTTP. El navegador descarga el archivo clásico mientras continúa analizando HTML y lo ejecuta después de terminar ese análisis, antes de DOMContentLoaded. La etiqueta no muestra texto por sí misma: el resultado depende de lo que haga aviso.js.
El archivo aviso.js se entrega más abajo y necesita el <output> y el botón del documento completo. Para probar el fragmento mínimo, conserva ese documento y retira sus otros dos scripts externos: aparecerá «Aviso preparado por JavaScript» y se habilitará el botón. La ruta empieza por /, por lo que se resuelve desde la raíz del sitio, no desde la carpeta del HTML.
Sintaxis, cierre y contexto válido
¿Script necesita cierre aunque tenga src? Sí: escribe <script src="archivo.js"></script>. No es un elemento vacío y <script src="archivo.js" /> no lo cierra en HTML. Puede aparecer en <head>, en el <body> y en contextos que admitan contenido de frase o elementos que dan soporte a scripts. No obliga a poner todo el JavaScript al final del cuerpo.
Sin src, el contenido es código o datos según type, no HTML para mostrar. Con src, deja el interior vacío: el código interno no actúa como alternativa si falla la descarga. El estándar permite documentación interna bajo restricciones concretas, pero eso tampoco crea un fallback ejecutable. Estas reglas se detallan en el modelo de contenido de script.
El analizador HTML reconoce la secuencia </script> incluso dentro de una cadena JavaScript o JSON. Para incluir ese texto en datos internos, escapa el signo <, por ejemplo como \u003C/script> en JSON. En cadenas JavaScript también puede usarse \x3C; JSON no admite ese escape. El estándar explica además el tratamiento especial de <!-- y <script en las restricciones del contenido de script. No confíes en comillas JavaScript para proteger el cierre de HTML.
Código interno, externo y módulos
Un script clásico interno se ejecuta cuando el analizador llega a él. Este fragmento, colocado en el cuerpo después de un párrafo, escribe en Consola, sin modificar la página:
<script>
console.log("El navegador ha llegado a este script.");
</script>
Para un clásico, omite type: no hace falta type="text/javascript". Separar el código en un archivo facilita reutilizarlo y mantenerlo; src indica su URL. Si un clásico usa variables o funciones de otro, debes asegurar que su dependencia se ejecute primero. Las declaraciones clásicas pueden compartir el ámbito global; envolver el ejemplo en un bloque evita que sus const y let contaminen ese ámbito.
Con type="module", el código utiliza módulos de JavaScript: puede importar dependencias, se ejecuta en modo estricto y sus declaraciones superiores pertenecen al ámbito del módulo. import y export estáticos requieren ese contexto. Las rutas de los imports relativos se resuelven respecto al archivo que importa, no respecto al HTML.
<script type="module" src="/ejemplos/script/modulo.js"></script>
Prueba módulos mediante un servidor HTTP, no abriendo el documento como file://. El servidor debe responder con un tipo MIME de JavaScript y los recursos de otro origen deben cumplir CORS. La guía de módulos de MDN desarrolla estas diferencias.
Qué diferencia hay entre async y defer
La siguiente tabla describe scripts escritos en el HTML original; los insertados mediante JavaScript tienen reglas adicionales.
| Modalidad | Descarga y ejecución | Orden y acceso al documento |
|---|---|---|
| Clásico externo sin atributos | Detiene el análisis mientras descarga y ejecuta. | Sigue el orden del documento; solo dispone del HTML analizado hasta ese punto. |
Clásico externo con defer | Descarga en paralelo; ejecuta después del análisis. | Conserva el orden entre clásicos diferidos y retrasa DOMContentLoaded hasta terminar. |
Clásico externo con async | Descarga en paralelo; ejecuta cuando está disponible, pudiendo interrumpir el análisis. | No garantiza el orden entre scripts ni que el documento esté completo. |
Módulo sin async | Descarga el módulo y sus dependencias en paralelo; inicia su evaluación después del análisis. | Resuelve dependencias mediante imports; no necesita defer. |
Módulo con async | Evalúa cuando están disponibles el módulo y sus dependencias. | Puede ejecutarse antes de terminar el análisis; no garantiza orden con otros scripts independientes. |
defer no actúa sobre clásicos internos y no debe usarse en ellos; en módulos no tiene efecto. async tampoco sirve para diferir un clásico interno. Si un clásico externo tiene ambos atributos, los navegadores actuales aplican async. async="false" sigue activando async: al ser booleano, se desactiva retirando el atributo. La ejecución de JavaScript aún puede ocupar el hilo principal; descargar en paralelo no garantiza una interfaz siempre fluida.
Para dependencias entre módulos, usa imports en lugar de esperar que una variable aparezca globalmente por la posición de otra etiqueta. La tabla no promete cuándo terminan tareas asíncronas iniciadas por esos scripts, como una petición de datos.
Caso práctico: aviso y módulo en un documento independiente
Abre la demostración de script. Se entrega fuera del artículo: sus IDs, eventos y estilos no se ejecutan en esta guía. Para reproducirla, guarda los cinco archivos siguientes en UTF-8, respetando las carpetas ejemplos/ y ejemplos/script/ dentro de la raíz de tu servidor. Si cambias esa estructura, adapta los src; conserva el import relativo del módulo.
ejemplos/script/index.html:
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Prueba de carga de scripts</title>
<style>
body { max-width: 42rem; margin: auto; padding: 1rem;
font-family: system-ui, sans-serif; line-height: 1.6; }
button { font: inherit; }
:focus-visible { outline: 3px solid #074f85; outline-offset: 3px; }
</style>
<script src="/ejemplos/aviso.js" defer></script>
<script src="/ejemplos/script/detalle.js" defer></script>
<script type="module" src="/ejemplos/script/modulo.js"></script>
</head>
<body>
<main>
<h1>Prueba de carga de scripts</h1>
<p>Puedes consultar la guía aunque los scripts no carguen.</p>
<p>
<label for="script-resultado">Estado del aviso:</label>
<output id="script-resultado">Aviso inicial sin actualizar.</output>
</p>
<button id="script-actualizar" type="button" disabled>Actualizar aviso</button>
<p id="script-modulo">El módulo todavía no ha añadido detalles.</p>
<noscript><p>JavaScript está desactivado; la actualización no está disponible.</p></noscript>
<p><a href="/etiquetas-html/etiqueta-script.html">Consultar la guía de script</a></p>
</main>
</body>
</html>
ejemplos/aviso.js actualiza el resultado existente y habilita un control nativo:
{
const resultado = document.getElementById("script-resultado");
const boton = document.getElementById("script-actualizar");
resultado.value = "Aviso preparado por JavaScript.";
boton.addEventListener("click", () => {
resultado.value = "Aviso actualizado con el botón.";
});
boton.disabled = false;
console.log("1: aviso clásico ejecutado", document.readyState);
}
ejemplos/script/detalle.js añade un texto al resultado que dejó el primer clásico:
{
const resultado = document.getElementById("script-resultado");
resultado.value += " Segundo clásico ejecutado.";
console.log("2: segundo clásico ejecutado", document.readyState);
}
ejemplos/script/modulo.js importa una función y modifica otro párrafo:
import { crearMensaje } from "./mensaje.js";
const detalle = document.getElementById("script-modulo");
detalle.textContent = crearMensaje();
console.log("Módulo ejecutado con su dependencia", document.readyState);
ejemplos/script/mensaje.js contiene la dependencia:
export function crearMensaje() {
return "El módulo ha importado una función de mensaje.js.";
}
Cómo comprobar carga, orden y alternativa
- Abre el documento por HTTP. En Red comprueba respuestas correctas para los cuatro archivos JS;
mensaje.jsse solicita por el import. En Consola, el mensaje1precede al2, aunque el segundo archivo se descargue antes. Ambos se ejecutan con el documento ya analizado. - Lee el resultado completo: «Aviso preparado por JavaScript. Segundo clásico ejecutado». El otro párrafo explica que se importó una función. Usa Tab hasta el botón y actívalo con Enter o Espacio: el resultado cambia a «Aviso actualizado con el botón».
- En una copia, sustituye
deferporasyncen los dos clásicos. Desactiva la caché y varía la velocidad de descarga: el orden puede cambiar y alguno puede ejecutarse antes de que exista el resultado, causando un error. Una recarga que mantenga el orden no demuestra una garantía. Restauradefer. - Desactiva JavaScript y recarga: permanecen el texto inicial, la explicación, el enlace y el botón deshabilitado. Aparece la alternativa de
<noscript>. Bloquea despuésaviso.jscon JavaScript activo: no se habilita el botón y la Consola muestra el fallo de carga;<noscript>no detecta ese fallo. - Retira los tres scripts externos: queda el HTML inicial. Retirar solo el del módulo afecta a su párrafo; cambiar el mensaje de
aviso.jscambia el aviso. Retirar el cierre de<script>es un error de marcado, no una forma de desactivar código.
Un bloque JSON no ejecuta JavaScript
Un type no ejecutable, como application/json, convierte el contenido en datos internos. Este fragmento no muestra ni ejecuta el mensaje:
<script type="application/json" id="script-datos">
{"mensaje": "El texto \u003C/script> forma parte de los datos."}
</script>
Otro script puede leerlo con JSON.parse(document.getElementById("script-datos").textContent). El resultado es un objeto cuyo mensaje contiene el cierre literal. Sin ese código lector, los datos simplemente permanecen en el documento. No añadas src para cargar este bloque: los bloques de datos se escriben internamente. importmap tiene una función específica de resolución de módulos; no debe confundirse con un bloque JSON genérico.
Atributos y accesibilidad
| Atributo específico | Cuándo sirve |
|---|---|
src | URL del archivo ejecutable externo. Una ruta incorrecta no crea código alternativo. |
type | Selecciona clásico, module o un tipo de datos como application/json. |
async, defer | Controlan los modos de ejecución descritos en la tabla; son booleanos. |
crossorigin | Configura CORS y credenciales: anonymous o use-credentials. No concede permisos que el servidor rechace; los módulos entre orígenes requieren CORS. |
integrity | Comprueba el archivo externo frente a un hash real de sus bytes. Si no coincide, se bloquea; en recursos entre orígenes exige una respuesta CORS adecuada. |
referrerpolicy | Configura el referente enviado al cargar el script y sus imports; no-referrer lo omite. No cambia por sí mismo todas las peticiones que después haga el código. |
nomodule | Omite un clásico en navegadores que admiten módulos. Sirve para una alternativa clásica realmente preparada para navegadores antiguos. |
Los atributos globales incluyen id, class, lang y nonce. Un nonce no establece una CSP por sí solo: debe coincidir con una política real entregada para el documento y generarse de forma impredecible para cada respuesta. No copies un valor fijo como sistema de seguridad. .value y .textContent son propiedades DOM; outline es CSS, y click es un evento.
<script> no aporta una interfaz visible ni necesita un nombre accesible. Sí debes revisar la interfaz que produce: aquí el botón funciona por teclado, conserva un foco visible y el <output> tiene un <label>. Su semántica de estado facilita comunicar cambios, pero el anuncio concreto depende del navegador y del lector de pantalla. El contenido y el enlace siguen siendo comprensibles sin scripts; no se depende solo de color.
Errores frecuentes y etiquetas relacionadas
Si un script no funciona, comprueba en este orden: JavaScript habilitado, URL y respuesta en Red, tipo MIME, mensajes de Consola, IDs del DOM y momento de ejecución. Revisa también CORS, CSP e integridad cuando aparezcan bloqueos. Un archivo .js presente no garantiza que su código sea correcto.
No uses eval ni conviertas datos del usuario en código. Para añadir texto, usa textContent o una propiedad apropiada como .value; no construyas etiquetas <script> con esos datos. Los atributos de script en MDN ayudan a identificar cada bloqueo.
<noscript> proporciona una alternativa cuando el scripting está deshabilitado, pero no supervisa errores. <output> presenta un resultado y no lo calcula. Para un simple desplegable, considera <details>, que ya funciona sin JavaScript; para estilos usa CSS, no código que cambie cada propiedad al cargar.
Compatibilidad y fuentes
Fuentes revisadas el 3 de octubre de 2026. El elemento y los modos clásicos async y defer están disponibles en los motores actuales, según el HTML Living Standard. El soporte de la etiqueta no implica soporte de módulos ni de todas las APIs escritas dentro del archivo. MDN documenta las diferencias de módulos y las necesidades de servidor; una alternativa nomodule tampoco transforma código moderno automáticamente.
Los ejemplos se comprobaron en Chromium 153 mediante HTTP. Esa comprobación confirma carga, orden clásico diferido, módulo y alternativa local; no constituye una prueba de todos los navegadores o lectores de pantalla. Para producción, contrasta las funciones JavaScript utilizadas con los navegadores que debas admitir.