Appearance
JavaScript en HTML
Objetivo
Comprobar las diferentes formas de integrar JavaScript en un documento HTML y analizar cómo afectan la posición del script y sus atributos al momento de ejecución.
1. Script interno
Crea un archivo index.html que contenga un encabezado <h1> y un botón.
Añade al final de <body> un elemento <script> con la siguiente instrucción:
js
console.log("JavaScript is running");Abre la página en el navegador y comprueba el resultado desde las DevTools.
Después, mueve el elemento <script> a <head> y comprueba que el código continúa ejecutándose.
2. Script externo
Crea un archivo main.js y mueve a él el código JavaScript del ejercicio anterior.
La estructura debe quedar:
text
project/
├── index.html
└── main.jsCarga el archivo desde HTML:
html
<script src="./main.js"></script>Comprueba desde las pestañas Console y Network de las DevTools que el archivo se carga y ejecuta correctamente.
Explica brevemente qué ventajas tiene mantener el código JavaScript en un archivo independiente.
3. Posición y momento de ejecución
Vamos a comprobar cómo la posición del elemento <script> y el atributo defer afectan al procesamiento del documento HTML y al momento en el que se ejecuta JavaScript.
Script sin defer
Crea un archivo index.html con el siguiente contenido:
html
<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<title>JavaScript bloqueante</title>
<!--
Script clásico situado en el <head>.
Como no tiene async ni defer, cuando el parser HTML
llega aquí detiene el procesamiento del documento,
descarga el archivo JavaScript y lo ejecuta completamente.
-->
<script src="./main.js"></script>
</head>
<body>
<!--
Cuando main.js se ejecuta, el parser todavía no ha
llegado a esta parte del documento.
-->
<h1 id="title">Contenido de la página</h1>
<p>Este contenido está después del script.</p>
</body>
</html>Utiliza el siguiente código en main.js:
js
console.log("Inicio del JavaScript");
// Intentamos acceder al <h1> situado en el <body>.
const title = document.querySelector("#title");
console.log("¿Existe el h1?", title);
// Bloqueamos deliberadamente el hilo principal durante 5 segundos.
const start = Date.now();
while (Date.now() - start < 5000) {
// Mientras se ejecuta este bucle, el parser HTML
// tampoco puede continuar procesando el documento.
}
// Volvemos a intentar acceder al <h1>.
const titleAfter = document.querySelector("#title");
console.log("¿Existe el h1 después de 5 segundos?", titleAfter);
console.log("Fin del JavaScript");Abre index.html utilizando Live Server y observa la consola del navegador.
El resultado será similar a:
text
Inicio del JavaScript
¿Existe el h1? null
← pasan 5 segundos
¿Existe el h1 después de 5 segundos? null
Fin del JavaScriptCuando el parser encuentra el <script> situado en el <head>, detiene el procesamiento del HTML.
Primero se descarga el archivo JavaScript y después se ejecuta. Solo cuando termina su ejecución puede continuar el parsing del documento.
text
Tiempo ──────────────────────────────────────────────────────→
HTML ████████████│ │████████████████
│ │
└── <script> └── continúa
↓
JavaScript ████████│██████████
descarga │ ejecución
← parsing detenido →Por este motivo, cuando se ejecuta:
js
document.querySelector("#title");el parser todavía no ha llegado al <body> y el elemento <h1> todavía no existe en el DOM.
Aunque transcurran 5 segundos, el segundo querySelector() también devuelve null. Mientras JavaScript continúa ejecutándose, el parser no puede continuar procesando el documento.
Lo que vemos al refrescar
Al refrescar la página puede ocurrir que el <h1> y el <p> de la carga anterior continúen visibles durante los 5 segundos.
Esto no significa que el nuevo HTML haya sido procesado.
El navegador puede mantener en pantalla el último contenido que había renderizado mientras comienza la nueva carga. Además, el bucle JavaScript está ocupando el hilo principal, por lo que el navegador no puede actualizar normalmente la representación visual de la página.
Por eso pueden darse simultáneamente estas dos situaciones:
text
DOM de la nueva carga → el <h1> todavía no existe.
Pantalla → continúa mostrando el <h1> de la carga anterior.La pantalla no representa necesariamente de forma instantánea el estado actual del DOM. Por este motivo utilizamos querySelector() para comprobar si el elemento existe realmente.
Script con defer
Modifica únicamente la etiqueta <script>:
html
<script src="./main.js" defer></script>No realices ningún cambio en main.js.
Vuelve a cargar la página y observa la consola.
Ahora el resultado será similar a:
text
Inicio del JavaScript
¿Existe el h1? <h1 id="title">...</h1>
← pasan 5 segundos
¿Existe el h1 después de 5 segundos? <h1 id="title">...</h1>
Fin del JavaScriptCon defer, el archivo JavaScript se descarga de forma asíncrona mientras continúa el parsing del HTML, pero su ejecución se aplaza hasta que el documento ha terminado de procesarse.
text
Tiempo ──────────────────────────────────────────────────────→
HTML ███████████████████████████████████│
↓
fin del parsing
JavaScript ████████████ ███████████
descarga ejecución
asíncronaPor tanto, cuando comienza a ejecutarse main.js, el parser ya ha procesado el <body> y el <h1> ya existe en el DOM.
defer no ejecuta JavaScript en paralelo
defer permite que la descarga del archivo JavaScript se realice de forma asíncrona mientras continúa el parsing del HTML.
Sin embargo, la ejecución del código JavaScript sigue utilizando el hilo principal.
Por ello, el bucle de 5 segundos:
js
while (Date.now() - start < 5000) {}continúa bloqueando el hilo principal durante su ejecución.
Comparación
| Comportamiento | Sin defer | Con defer |
|---|---|---|
| Descarga del script. | Detiene el parsing. | Asíncrona respecto al parsing. |
| Parsing durante la descarga. | Se detiene. | Continúa. |
| Ejecución del script. | Al terminar la descarga. | Después de finalizar el parsing. |
<h1> al comenzar el script. | No existe (null). | Ya existe. |
| Ejecución de JavaScript. | Utiliza el hilo principal. | Utiliza el hilo principal. |
Responde:
- ¿Por qué
document.querySelector("#title")devuelvenullcuando el script convencional se encuentra en<head>? - ¿Por qué esperar 5 segundos dentro del código JavaScript no permite que el navegador procese el
<body>? - ¿Qué ocurre con la descarga del archivo JavaScript al utilizar
defer? - ¿Cuándo se ejecuta el archivo JavaScript al utilizar
defer? - ¿Qué ocurriría si colocásemos el script convencional al final de
<body>?
4. defer y async
Crea tres archivos JavaScript:
text
first.js
second.js
third.jsAñade en cada uno una instrucción diferente:
js
console.log("First");js
console.log("Second");js
console.log("Third");Carga los tres archivos utilizando defer:
html
<script src="./first.js" defer></script>
<script src="./second.js" defer></script>
<script src="./third.js" defer></script>Recarga varias veces la página y observa el orden de ejecución.
Sustituye después defer por async, vuelve a realizar varias recargas y compara los resultados.
Responde:
- ¿Qué mecanismo mantiene el orden de los scripts?
- ¿Cuándo se ejecutan los scripts con
defer? - ¿De qué depende el momento de ejecución de un script con
async? - ¿Cuál utilizarías para scripts de una aplicación que dependen entre sí?
INFO
Con archivos locales muy pequeños es posible que los scripts con async terminen descargándose siempre en un orden similar.
Lo importante es comprender que async no garantiza el orden de ejecución, aunque durante una prueba concreta parezca mantenerlo.
5. Módulos JavaScript
Modifica la carga de main.js para utilizar un módulo:
html
<script type="module" src="./main.js"></script>Crea después un archivo utils.js:
js
export function sayHello() {
console.log("Hello from utils");
}Impórtalo desde main.js:
js
import { sayHello } from "./utils.js";
sayHello();Comprueba desde la consola que el código se ejecuta correctamente.
Identifica qué mecanismos permiten:
- Exportar elementos desde un módulo.
- Importar elementos desde otro módulo.
- Indicar al navegador que un archivo JavaScript debe tratarse como módulo.