14 métodos para diferir o programar JavaScript

Aprende cómo diferir y programar JavaScript

Arjen Karel Core Web Vitals Consultant
Arjen Karel - linkedin
Last update: 2024-11-27

¿Por qué diferir o programar JavaScript?

JavaScript puede (y va a) ralentizar tu sitio web de varias maneras. Esto puede tener todo tipo de impacto negativo en los Core Web Vitals. JavaScript puede competir por recursos de red, puede competir por recursos de CPU (bloquear el hilo principal) e incluso puede bloquear el análisis de una página web. Diferir y programar tus scripts puede mejorar drásticamente los Core Web Vitals.

Para minimizar los efectos negativos que JavaScript puede tener en los Core Web Vitals, generalmente es una buena idea especificar cuándo un script se pone en cola para descarga y programar cuándo puede consumir tiempo de CPU y bloquear el hilo principal.

¿Cómo puede la temporización de JavaScript afectar los Core Web Vitals?

¿Cómo puede la temporización de JavaScript afectar los Core Web Vitals? Solo observa este ejemplo de la vida real. La primera página se carga con JavaScript que 'bloquea el renderizado'.  Las métricas de pintado así como el Time to interactive son bastante malas. El segundo ejemplo es de exactamente la misma página pero con el JavaScript diferido. Verás que la imagen LCP aún sufrió un gran impacto. El tercer ejemplo tiene el mismo script ejecutado después del evento 'load' de la página y tiene las llamadas a funciones divididas en piezas más pequeñas. Este último pasa los Core Web Vitals con margen de sobra.

js render blocking lhnull
js deferred lhnull
js after load lhnull


Por defecto, el JavaScript externo en el head de la página bloqueará la creación del árbol de renderizado. Más específicamente: cuando el navegador encuentra un script en el documento debe pausar la construcción del DOM, ceder el control al motor de JavaScript, y dejar que el script se ejecute antes de continuar con la construcción del DOM. Esto afectará tus métricas de pintado (Largest Contentful Paint y First Contentful Paint).

El JavaScript diferido o asíncrono aún puede impactar las métricas de pintado, especialmente el Largest Contentful Paint porque se ejecutará y bloqueará el hilo principal, una vez que el DOM haya sido creado (y elementos comunes de LCP como imágenes podrían no haberse descargado aún).

Los archivos JavaScript externos también competirán por recursos de red. Los archivos JavaScript externos generalmente se descargan antes que las imágenes. SI estás descargando demasiados scripts, la descarga de tus imágenes se retrasará.

Por último pero no menos importante, JavaScript podría bloquear o retrasar la interacción del usuario. Cuando un script está usando recursos de CPU (bloqueando el hilo principal) un navegador no responderá a la entrada (clics, desplazamiento, etc.) hasta que ese script haya terminado.

¿Cómo soluciona la programación o el diferimiento de JavaScript los Core Web Vitals?

Programar o diferir JavaScript no soluciona los Core Web Vitals per se. Se trata de usar la herramienta correcta para la situación correcta. Como regla, deberías intentar retrasar tus scripts lo menos posible pero ponerlos en cola para descarga y ejecutarlos en el momento apropiado.

¿Cómo elegir el método de diferimiento correcto?

No todos los scripts son iguales y cada script tiene su propia funcionalidad. Algunos scripts son importantes tenerlos temprano en el proceso de renderizado, otros no.

js defer vs async vs blockingnull

Me gusta categorizar los JavaScripts en 4 grupos según su nivel de importancia.

1. Crítico para el renderizado. Estos son los scripts que cambiarán la apariencia de una página web. Si no se cargan, la página no se verá completa. Estos scripts deben evitarse a toda costa. Si no puedes evitarlos por alguna razón, no deberían diferirse. Por ejemplo, un slider superior o un script de pruebas A/B.
2. Crítico. Estos scripts no cambiarán la apariencia de una página web (demasiado) pero la página no funcionará bien sin ellos. Estos scripts deberían diferirse o cargarse de forma asíncrona. Por ejemplo, los scripts de tu menú.
3. Importante. Estos son scripts que quieres cargar porque son valiosos para ti o para el visitante. Yo tiendo a cargar estos scripts después de que el evento load se haya disparado. Por ejemplo, analytics o un botón de 'volver arriba'.
4. Opcional. Estos son scripts sin los que puedes vivir si es absolutamente necesario. Yo cargo estos scripts con la prioridad más baja y solo los ejecuto cuando el navegador está inactivo. Por ejemplo, un widget de chat o un botón de Facebook.

Método 1: Usar el atributo defer

Los scripts con el atributo defer se descargarán en paralelo y se añaden a la cola de JavaScript diferido. Justo antes de que el navegador dispare el evento DOMContentLoaded, todos los scripts en esa cola se ejecutarán en el orden en que aparecen en el documento.

<script  src='javascript.js'></script>

El 'truco del defer' generalmente soluciona muchos problemas, especialmente las métricas de pintado. Desafortunadamente no hay garantía, depende de la calidad de los scripts. Los scripts diferidos se ejecutarán una vez que todos los scripts se hayan cargado y el HTML esté analizado (DOMContentLoaded). El elemento LCP (generalmente una imagen grande) podría no haberse cargado para entonces y los scripts diferidos aún causarán un retraso en el LCP.

Cuándo usar:

Usa scripts diferidos para scripts críticos que se necesitan lo antes posible.

Ventajas:

  1. Los scripts diferidos se descargarán en paralelo
  2. El DOM estará disponible en el momento de la ejecución

Desventajas:

  1. Los scripts diferidos podrían retrasar tus métricas de LCP 
  2. Los scripts diferidos bloquearán el hilo principal una vez que se ejecuten
  3. Podría no ser seguro diferir scripts cuando scripts inline o async dependen de ellos

Método 2: Usar el atributo async

Los scripts con el atributo async se descargan en paralelo y se ejecutarán inmediatamente después de que hayan terminado de descargarse.

<script  src='javascript.js'></script>

Los scripts async harán poco para solucionar tus problemas de velocidad de página. Es genial que se descarguen en paralelo pero eso es todo. Una vez que se descargan, bloquearán el hilo principal mientras se ejecutan.

Cuándo usar:

Usa scripts async para scripts críticos que se necesitan lo antes posible y son autocontenidos (no dependen de otros scripts).

Ventajas:

  1. Los scripts async se descargarán en paralelo.
  2. Los scripts async se ejecutarán lo antes posible.

Desventajas:

  1. DOMContentLoaded puede ocurrir tanto antes como después de async.
  2. El orden de ejecución de los scripts será desconocido de antemano.
  3. No puedes usar scripts async que dependan de otros scripts async o diferidos

Método 3: Usar módulos

Los scripts modulares se difieren por defecto a menos que tengan el atributo async. En ese caso se tratarán como scripts async

<script  src='javascript.js'></script>

Los módulos son una nueva forma de pensar sobre JavaScript y solucionan algunos defectos de diseño. Aparte de eso, usar módulos de script no acelerará tu sitio web. 

Cuándo usar:

Cuando tu aplicación está construida de forma modular, tiene sentido usar también módulos de JavaScript.

Ventajas:

  1. Los módulos se difieren por defecto
  2. Los módulos son más fáciles de mantener y funcionan muy bien con el diseño web modular
  3. Los módulos permiten una fácil división de código con importaciones dinámicas donde solo importas los módulos que necesitas en un momento determinado.

Desventajas:

  1. Los módulos por sí solos no mejorarán los Core Web Vitals
  2. Importar módulos justo a tiempo o sobre la marcha podría ser lento y empeorar el FID e INP 

Método 4: Colocar scripts cerca del final de la página

Los scripts en el footer se ponen en cola para descarga en un momento posterior. Esto priorizará otros recursos que están en el documento por encima de la etiqueta script.

<html>
   <head></head>
   <body>
      [el contenido de tu página aquí]
      <script defer src='javascript.js'></script>
   </body>
</html>

Colocar tus scripts al final de la página es una técnica interesante. Esto programará otros recursos (como imágenes) antes que tus scripts. Esto aumentará la probabilidad de que estén disponibles para el navegador y pintados en la pantalla antes de que los archivos JavaScript hayan terminado de descargarse y el hilo principal sea bloqueado por la ejecución del script. Aun así... sin garantía.

Cuándo usar:

Cuando tus scripts ya funcionan bastante bien pero quieres priorizar ligeramente otros recursos como imágenes y fuentes web.

Ventajas:

  1. Colocar scripts al final de la página no requiere mucho conocimiento.
  2. Si se hace correctamente no hay riesgo de que esto rompa tu página

Desventajas:

  1. Los scripts críticos podrían descargarse y ejecutarse más tarde
  2. No soluciona ningún problema subyacente de JavaScript

Método 5: Inyectar scripts

Los scripts inyectados se tratan como scripts async. Se descargan en paralelo y se ejecutan inmediatamente después de descargarse.

<script>
    const loadScript = (scriptSource) => {
        const script = document.createElement('script');
        script.src = scriptSource;
        document.head.appendChild(script);
    }
    
    // call the loadscript function that injects 'javascript.js'
    loadScript('javascript.js');
</script>

Desde la perspectiva de los Core Web Vitals, esta técnica es exactamente igual que usar <script async>.

Cuándo usar:

Este método es frecuentemente usado por scripts de terceros que se disparan lo antes posible. La llamada a función facilita encapsular y comprimir código.

Ventajas:

  1. Código contenido que inyecta un script async.

Desventajas:

  1. DOMContentLoaded puede ocurrir tanto antes como después de que el script se haya cargado.
  2. El orden de ejecución de los scripts será desconocido de antemano.
  3. No puedes usar esto en scripts que dependan de otros scripts async o diferidos

Método 6: Inyectar scripts en un momento posterior

Los scripts opcionales en mi opinión nunca deberían cargarse diferidos. Deberían inyectarse en el momento más oportuno. En el ejemplo a continuación, el script se ejecutará después de que el navegador haya enviado el evento 'load'.

<script>
window.addEventListener('load', function () {
  // see method 5 for the loadscript function
  loadScript('javascript.js');
});
</script>

Esta es la primera técnica que mejorará de forma fiable el Largest Contentful Paint. Todos los recursos importantes , incluyendo las imágenes, se habrán descargado cuando el navegador dispare el 'evento load'. Esto podría introducir todo tipo de problemas porque podría tardar mucho tiempo en llamarse el evento load.

Cuándo usar:

Para scripts opcionales que no tienen razón para impactar las métricas de pintado.

Ventajas:

  1. No competirá por recursos críticos porque inyectará el script una vez que la página y sus recursos se hayan cargado

Desventajas:

  1. Si tu página está mal diseñada en términos de Core Web Vitals, podría tardar mucho tiempo en que la página envíe el evento 'load'
  2. Debes tener cuidado de no aplicar esto a scripts críticos (como lazy loading, menú, etc.)

Método 7: Cambiar el tipo de script (y luego cambiarlo de vuelta)

Si se encuentra una etiqueta script en algún lugar de la página que 1. tiene un atributo type y 2. el atributo type no es "text/javascript", el script no será descargado ni ejecutado por el navegador. Muchos cargadores de JavaScript (como RocketLoader de CloudFlare) se basan en este principio. La idea es bastante simple y elegante.

Primero, todos los scripts se reescriben así:

<script  src="javascript.js"></script>

Luego, en algún momento durante el proceso de carga, estos scripts se convierten de vuelta a 'javascripts normales'.

Cuándo usar:

Este no es un método que recomendaría. Solucionar el impacto de JavaScript requerirá mucho más que simplemente mover cada script un poco más abajo en la cola. Por otro lado, si tienes poco control sobre los scripts que se ejecutan en la página o tienes conocimientos insuficientes de JavaScript, esta podría ser tu mejor opción.

Ventajas:

  1. Es fácil, solo habilita rocket loader u otro plugin y todos tus scripts ahora se ejecutan en un momento algo posterior.
  2. Probablemente solucionará tus métricas de pintado siempre que no uses lazy loading basado en JS.
  3. Funcionará para scripts inline y externos.

Desventajas:

  1. No tendrás control detallado sobre cuándo se ejecutan los scripts
  2. Scripts mal escritos podrían romperse
  3. Usa JavaScript para solucionar JavaScript
  4. No hace nada para solucionar scripts de larga ejecución

Método 8: Usar el intersection observer

Con el intersection observer puedes ejecutar una función (que en este caso carga un JavaScript externo) cuando un elemento se desplaza hacia el viewport visible.

<script>
const handleIntersection = (entries, observer) => {
    if (entries?.[0].isIntersecting) {
        // load your script or execute another 
           function like trigger a lazy loaded element
        loadScript('javascript.js');

        // remove the observer
        observer.unobserve(entries?.[0].target);
    }
};
const Observer = new window.IntersectionObserver()
Observer.observe(document.querySelector('footer'));
</script>

Este es por lejos el método más efectivo para diferir JavaScript que existe. Solo carga los scripts que necesitas, justo antes de necesitarlos. Desafortunadamente, la vida real casi nunca es tan limpia y no muchos scripts pueden vincularse a un elemento que se desplaza hacia la vista.

Cuándo usar:

¡Usa esta técnica tanto como sea posible! Siempre que un script solo interactúe con un componente fuera de pantalla (como un mapa, un slider, un formulario) esta es la mejor manera de inyectar este script.

Ventajas:

  1. No interferirá con los Core Web Vitals LCP y FCP
  2. Nunca inyectará scripts que no se usen. Esto mejorará el FID e INP

Desventajas:

  1. No debería usarse con componentes que podrían estar en el viewport visible
  2. Es más difícil de mantener que un básico <script src="...">
  3. Podría introducir un layout shift

Método 9: Usar readystatechange

document.readystate puede usarse como alternativa a los eventos 'DOMContentloaded' y 'load'. El readystate 'interactive' generalmente es un buen lugar para llamar scripts críticos que necesitan cambiar el DOM o agregar manejadores de eventos. 
El readystate 'complete' es un buen lugar para llamar scripts que son menos críticos.

document.addEventListener('readystatechange', (event) => {
  if (event.target.readyState === 'interactive') {
    initLoader();
  } else if (event.target.readyState === 'complete') {
    initApp();
  }
});

Método 10: Usar setTimeout sin timeout

setTimeout es un método mal visto pero muy subestimado en la comunidad de velocidad de página. setTimeout ha recibido una mala reputación porque a menudo se usa mal.  Muchos desarrolladores creen que setTimeout solo puede usarse para retrasar la ejecución de scripts por la cantidad establecida de milisegundos. Aunque esto es cierto, setTimeout en realidad hace algo mucho más interesante. Crea una nueva tarea al final del bucle de eventos del navegador. Este comportamiento puede usarse para programar tus tareas de manera efectiva. También puede usarse para dividir tareas largas en tareas más pequeñas separadas

<script>
   setTimeout(() => {
       // load a script or execute another function
       console.log('- I am called from a 0ms timeOut()')
    }, 0);

   console.log('- I was last in line but executed first')
   /*
      Output:
      - I was last in line but executed first
      - I am called from a 0ms timeOut()
   */
</script>

Cuándo usar:

setTimeout crea una nueva tarea en el bucle de eventos del navegador. Usa esto cuando tu hilo principal esté siendo bloqueado por muchas llamadas a funciones que se ejecutan secuencialmente.

Ventajas:

  1. Puede dividir código de larga ejecución en piezas más pequeñas.

Desventajas:

  1. setTimeout es un método bastante crudo y no ofrece priorización para scripts importantes.
  2. Añadirá el código a ejecutar al final del bucle

Método 11: Usar setTimeout con un timeout

Las cosas se ponen aún más interesantes cuando llamamos a setTimeout con un timeout de más de 0ms

<script>
   setTimeout(() => {
       // load a script or execute another function
       console.log('- I am called from a 10ms timeOut()')
    }, 10);

   setTimeout(() => {
       // load a script or execute another function
       console.log('- I am called from a 0ms timeOut()')
    }, 0);

   console.log('- I was last in line but executed first')
   /*
      Output:
      - I was last in line but executed first
      - I am called from a 0ms timeOut()
      - I am called from a 10ms timeOut()
   */
</script>

Cuándo usar:

Cuando necesitas un método fácil para programar un script después de otro, un pequeño timeout hará el trabajo

Ventajas:

  1. Compatible con todos los navegadores

Desventajas:

  1. No ofrece programación avanzada

Método 12: Usar una promesa para establecer una microtarea

Crear una microtarea también es una forma interesante de programar JavaScript. Las microtareas se programan para ejecutarse inmediatamente después de que el bucle de ejecución actual haya terminado.

<script>
   const myPromise = new Promise((resolve, reject) => {
      resolve();
   }).then(
      () => {       
         console.log('- I was scheduled after a promise')
         }
   );   
   console.log('- I was last in line but executed first')

   /*
      Output:
      - I was last in line but executed first
      - I was scheduled after a promise
   */
</script>

Cuándo usar:

Cuando una tarea necesita programarse inmediatamente después de otra tarea. 

Ventajas:

  1. La microtarea se programará inmediatamente después de que la tarea haya terminado de ejecutarse.
  2. Una microtarea puede usarse para retrasar piezas de código JavaScript menos importantes en el mismo evento. 

Desventajas:

  1. No dividirá el hilo principal en partes más pequeñas. El navegador no tendrá oportunidad de responder a la entrada del usuario.
  2. Probablemente nunca necesitarás usar microtareas para mejorar los Core Web Vitals a menos que ya sepas exactamente lo que estás haciendo.

Método 13: Usar una microtarea

El mismo resultado puede lograrse usando queueMicrotask(). La ventaja de usar queueMicrotask() sobre una promesa es que es ligeramente más rápido y no necesitas manejar tus promesas.

<script>
   queueMicrotask(() => {
      console.log('- I am a microtask')
   })
     
   console.log('- I was last in line but executed first')

   /*
      Output:
      - I was last in line but executed first
      - I am a microtask
   */
</script>


Método 14: Usar requestIdleCallback

El método window.requestIdleCallback() pone en cola una función para ser llamada durante los períodos de inactividad del navegador. Esto permite a los desarrolladores realizar trabajo de fondo y de baja prioridad en el bucle de eventos principal, sin impactar eventos críticos de latencia como animación y respuesta de entrada. Las funciones se llaman generalmente en orden de primero en entrar, primero en salir; sin embargo, los callbacks que tienen un timeout especificado pueden llamarse fuera de orden si es necesario para ejecutarlos antes de que el timeout expire.

<script>
requestIdleCallback(() => {
    const script = document.createElement('script');
    script.src = 'javascript.js';
    document.head.appendChild(script);
});
</script>

Cuándo usar:

Usa esto para scripts opcionales o para manejar tareas no críticas después de la entrada del usuario

Ventajas:

  1. Ejecutar JavaScript con mínimo impacto para el usuario
  2. Muy probablemente mejorará el FID e INP

Desventajas:

  1. Compatible con la mayoría de navegadores pero no todos. Existen polyfills que recurren a setTimeout();
  2. No hay garantía de que el código se ejecute alguna vez 

Método 15: Usar postTask

El método permite a los usuarios especificar opcionalmente un retraso mínimo antes de que la tarea se ejecute, una prioridad para la tarea, y una señal que puede usarse para modificar la prioridad de la tarea y/o abortarla. Devuelve una promesa que se resuelve con el resultado de la función callback de la tarea, o se rechaza con la razón de aborto o un error lanzado en la tarea.

<script>
scheduler.postTask(() => {
    const script = document.createElement('script');
    script.src = 'javascript.js';
    document.head.appendChild(script);
}, { priority: 'background' });
</script>

Cuándo usar:

postTask es la API perfecta para programar scripts. Desafortunadamente, el soporte de navegadores en este momento es malo, así que no deberías usarlo.

Ventajas:

  1. ¡Control completo sobre la programación de ejecución de JavaScript!

Desventajas:

  1. No es compatible con algunos navegadores importantes.

Performance is a Feature.

Treating speed as an afterthought fails. Build a performance culture with a dedicated 2-sprint optimization overhaul.

Initialize Project >>

  • 2-Sprint Overhaul
  • Culture Building
  • Sustainable Speed
14 métodos para diferir o programar JavaScriptCore Web Vitals 14 métodos para diferir o programar JavaScript