Cómo se ejecuta JavaScript en el navegador

Cómo el navegador arma el DOM a partir del HTML, en qué orden procesa link y script, el atributo defer, la consola de DevTools y cómo revisar resultados como texto en la consola de los ejercicios.

Si cambias el texto de una página con JavaScript, el código puede funcionar o no según dónde lo cargues. Cuando falla, la página no muestra ningún error, así que no sabes si el problema está en el código o en el orden en que se ejecutan las cosas.

En este artículo verás en qué orden el navegador carga el HTML y ejecuta JavaScript.

Los ejemplos de este artículo se abren como páginas HTML

La consola de los ejercicios no puede reproducir el orden en que se carga el HTML, así que este artículo no tiene ejercicios. Cada ejemplo muestra lo que pasa si pones index.html y app.js en la misma carpeta y abres index.html en Chrome; los resultados están en los comentarios al final de cada línea. En la última sección verás cómo revisar los resultados en la consola de los ejercicios.

Del HTML a elementos anidados — el árbol DOM

Imagina que quieres que app.js cambie el texto «Calculando…» de una página de confirmación de pedido por «3,300 yenes». Lo que JavaScript modifica aquí no es el texto del archivo index.html, sino una estructura de datos que el navegador construye al leer el HTML.

El navegador convierte cada elemento del HTML (una parte de la página delimitada por etiquetas, como <p></p>) en un objeto, y al conjunto de sus relaciones padre-hijo se le llama DOM (Document Object Model, también llamado árbol DOM; su punto de entrada es document).

Lo que se escribe dentro de una etiqueta, como id="total" o defer, se llama atributo. El id debe ser único: ningún otro elemento de la página puede usar el mismo.

<!-- ---- index.html (página de confirmación del pedido) ---- -->
<!DOCTYPE html>
<html lang="es">
  <head>
    <meta charset="UTF-8">
    <title>Confirmación del pedido</title>
    <link rel="stylesheet" href="style.css">   <!-- CSS que define el aspecto -->
  </head>
  <body>
    <h1>Confirmación del pedido</h1>
    <ul id="items">                           <!-- tiene dos hijos li -->
      <li>Taza × 2</li>
      <li>Posavasos × 1</li>
    </ul>
    <p id="total">Calculando…</p>             <!-- el elemento que app.js quiere cambiar -->
  </body>
</html>
El árbol DOM que sale de index.html
Elemento html — la raíz del DOM, a la que se llega desde document
Elemento head — información que no se ve en la página
  • meta — la codificación es UTF-8
  • title — contiene «Confirmación del pedido»
  • link — apunta a style.css
Elemento body — lo que se ve en la página
Elemento h1
  • Contiene «Confirmación del pedido»
Elemento ul — su id es items
  • li — contiene «Taza × 2»
  • li — contiene «Posavasos × 1»
Elemento p — su id es total
  • Contiene «Calculando…»
  • El elemento que app.js quiere cambiar
head y body son hermanos dentro de html, y los li van dentro de ul. El anidamiento de las etiquetas se convierte en la relación padre-hijo del DOM.

app.js obtiene el elemento p con document.querySelector("#total") (busca el elemento cuyo id va después del #, o devuelve null si no existe) y cambia su textContent (el texto que hay dentro del elemento). Como el p está dentro de body, solo se puede encontrar si ya se agregó al DOM.

El HTML se lee de arriba abajo — el orden de link y script

Supón que cargas app.js, que actualiza el total, en <head> junto al CSS. index.html sí contiene <p id="total">, pero al abrir la página sigue diciendo «Calculando…» y el cambio de app.js no tiene efecto.

Durante el análisis del HTML (el navegador lee el HTML desde el principio y va agregando al DOM cada elemento que encuentra), cuando llega a un <link>, sigue analizando mientras descarga el CSS. En cambio, cuando llega a un <script src>, pausa el análisis, descarga el JavaScript y lo ejecuta hasta el final antes de leer lo que sigue.

// ---- index.html (carga app.js en head) ----
// <head>
//   <link rel="stylesheet" href="style.css">   ← el análisis continúa
//   <script src="app.js"></script>             ← el análisis se pausa para ejecutarlo
// </head>
// <body>
//   <p id="total">Calculando…</p>               ← entra al DOM después de app.js
// </body>

// ---- app.js ----
const total = document.querySelector("#total");   // null (todavía no está en el DOM)
total.textContent = "3,300 yenes";
// TypeError: Cannot set properties of null (setting 'textContent')
Cuando se ejecuta el script, el p aún no existe
<link href="style.css">Descargael CSSSigue con lalínea siguiente<script src="app.js">Pausa el análisisy ejecuta app.jsNo hay #total,devuelve null<p id="total">Reanuda y loagrega al DOM#total llegademasiado tarde</html>Fin del análisis;DOM completoSigue mostrando«Calculando…»
El análisis se pausa en la línea del script y lee body solo después de ejecutar app.js. Aunque app.js lance un error, el análisis se reanuda y la página se muestra.

El cambio no funcionó porque el p todavía no estaba en el DOM cuando se ejecutó app.js. En la siguiente sección vas a mantener las dos líneas de app.js tal cual y retrasar su ejecución hasta que el DOM esté completo.

Ejecutar cuando el DOM está completo — el atributo defer

Si mueves <script> justo antes de </body>, se ejecuta después de leer el p, así que encuentra #total. Pero entonces que funcione o no depende de dónde esté la etiqueta, y si más adelante vuelves a poner la línea de carga en <head> junto al CSS, obtienes el mismo null otra vez.

El atributo defer (una opción de <script> que descarga el JavaScript sin pausar el análisis del HTML y lo ejecuta cuando el DOM está completo) se escribe <script defer src="app.js"></script> y puede ir en <head>, igual que el <link> del CSS.

// ---- index.html (carga en head con defer) ----
// <head>
//   <link rel="stylesheet" href="style.css">
//   <script defer src="app.js"></script>       ← empieza la descarga, el análisis continúa
// </head>
// <body>
//   <p id="total">Calculando…</p>               ← app.js se ejecuta tras leer todo hasta </html>
// </body>

// ---- app.js (las mismas 2 líneas de antes, más 1 para comprobar) ----
const total = document.querySelector("#total");   // el elemento p
total.textContent = "3,300 yenes";                // cambia el texto en la página
console.log(total.textContent);                   // 3,300 yenes
defer descarga en paralelo y ejecuta al final
<script defersrc="app.js">Sigue leyendo yagrega body al DOMTermina de leerhasta </html>DOM completoEmpieza adescargar app.jsSigue la descarga;el análisis sigueYa descargado,pero sin ejecutarEjecuta app.jsy halla #total
app.js se descarga mientras sigue el análisis y, aunque la descarga termine antes, espera para ejecutarse. Solo se ejecuta cuando el DOM está completo.

Si hay varios scripts con defer, se ejecutan en el orden en que aparecen en el HTML, no en el orden en que terminan de descargarse. La tabla de abajo muestra, para cada forma de escribir el script, cuándo se ejecuta app.js y si se encuentra #total.

Cómo se escribe el scriptCuándo se ejecuta app.js#total
<script src="app.js"> en headAl instante, pausando el análisisNo está (null)
<script src="app.js"> antes de </body>Después de leer el pEstá
<script defer src="app.js"> en headCon el DOM completoEstá
Código dentro de <script defer> en headAl instante (sin src, defer no aplica)No está (null)
<script type="module" src="app.js"> en headCon el DOM completo, sin deferEstá (no si abres el archivo directo)

Dónde se detuvo y cómo revisar el DOM — DevTools

En la versión sin defer, app.js lanzó un TypeError, pero la página no mostró nada. Si no sabes en qué archivo y en qué línea falló, no puedes averiguar por qué la página se queda en «Calculando…».

DevTools (un panel integrado en el navegador para revisar el DOM y los errores de una página) tiene una pestaña Console, que en Chrome se abre con Ctrl + Shift + J en Windows o Command + Option + J en Mac. Ahí aparece la salida de console.log, junto con los errores y el nombre de archivo y número de línea donde se lanzó cada uno.

// ---- Error en la pestaña Console (haz clic en app.js:2, a la derecha, para abrir esa línea) ----
// Uncaught TypeError: Cannot set properties of null (setting 'textContent')
//     at app.js:2:19       ← se lanzó en la línea 2, columna 19

// ---- Con la página ya cargada, escribe esto después del > de Console y pulsa Enter ----
document.querySelector("#total");                 // <p id="total">Calculando…</p>
document.querySelector("#total").textContent;     // 'Calculando…'

// ---- Línea para agregar después de la línea 1 de app.js (no la escribas en Console) ----
console.log("total al ejecutar:", total);         // total al ejecutar: null
La misma expresión devuelve el p tras la carga
app.js se ejecutadurante la cargaEl análisis estápausado en headquerySelectordevuelve nullTypeErroren la línea 2En Console, conla página cargadaAnálisis hecho,DOM completoquerySelectordevuelve el pSu texto dice«Calculando…»
La expresión que devolvía null en app.js devuelve el elemento p en Console. Que algo funcione en Console no significa que funcione en app.js.

Una expresión que escribes en Console se ejecuta sobre el DOM tal como está en ese momento. Para revisar un valor dentro de app.js, agrega un console.log antes de la línea sospechosa; así verás en la pestaña Console el valor que tenía cuando se ejecutó el código.

Revisar el resultado como texto — innerHTML y outerHTML

En los ejercicios de los próximos artículos vas a buscar, cambiar y agregar elementos. Pero la consola de los ejercicios no tiene una página visible y, hasta que recargues, todos los ejercicios comparten una misma página invisible, así que cada ejecución empieza con los elementos que dejó la anterior.

Si asignas un valor a innerHTML (una propiedad que lee y escribe el contenido de un elemento como texto HTML), el contenido se reemplaza por completo. Cada ejercicio empieza asignando al innerHTML de document.body (el elemento body) para borrar los elementos anteriores, y el resultado se revisa imprimiendo outerHTML (el texto HTML que incluye las etiquetas del propio elemento).

// ① Reemplaza todo el contenido de body (aquí se borran los elementos que dejó la ejecución anterior)
document.body.innerHTML = `<p id="total">Calculando…</p>`;

// ② Busca el elemento y cambia su texto
const total = document.querySelector("#total");
total.textContent = "3,300 yenes";

// ③ La página no se ve, así que imprime los resultados como texto
console.log(total.textContent);          // 3,300 yenes
console.log(total.outerHTML);            // <p id="total">3,300 yenes</p>
console.log(document.body.innerHTML);    // <p id="total">3,300 yenes</p>
La página invisible de los ejercicios
Página del artículo — la consola de los ejercicios
  • Solo vuelve la salida de console.log, como texto
Página invisible — una para todos los ejercicios
  • Contenido de document.body — se conserva hasta la siguiente ejecución
Primera ejecución
  • const total — solo existe dentro de esta ejecución
  • Asigna un p a document.body.innerHTML
Segunda ejecución
  • const total — se crea como otra variable, distinta de la anterior
  • La asignación del principio borra el p de la primera ejecución
Lo que hay dentro de body se queda ahí hasta la siguiente ejecución. La asignación a innerHTML del principio borra los elementos que quedaron.

El código de la consola se ejecuta cada vez dentro de una función nueva, así que const total se crea como una variable distinta de la ejecución anterior y no da un error de redeclaración. Además, se ejecuta cuando la página ya terminó de cargar, así que no tienes que preocuparte por defer.

Si imprimes un elemento directamente, verás {}

La consola de los ejercicios no tiene una página que mostrar y convierte la salida en texto antes de devolverla. Si pasas un elemento tal cual a console.log, aparece como {}, sin nada visible adentro, y no puedes saber si tu cambio funcionó. Para revisar un elemento, imprime su outerHTML o su textContent.

QUIZ

Verificación de conocimientos

Responde cada pregunta una a una.

Pregunta 1En un script cargado en head sin defer, ¿qué devuelve document.querySelector("#total")?

Pregunta 2Con <script defer src="app.js">, ¿cuándo se ejecuta app.js?

Pregunta 3Si ejecutas dos veces document.body.innerHTML = '<p>Calculando…</p>';, ¿cuántos elementos p hay?