Carga asíncrona de Javascript

Carga asíncrona de Javascript

Carga asíncrona de Javascript: La queremos, la necesitamos, la odiamos.

En mi artículo del blog cómo incluir javascript en HTML5, lweb20 pedía en un comentario que explicara con algo más de detalle el atributo async de la etiqueta script de HTML5… he tardado un poquito pero aquí está!

Antes de nada, para quien quiera, la versión en vídeo del artículo está disponible en mi canal de youtube:

Realmente, no voy a hablar únicamente del atributo async, sino que voy a intentar explicar qué es esto de la carga síncrona o asíncrona de javascript y por qué nos tenemos que preocupar por ello.

¿Qué es la carga síncrona de Javascript?

Cada vez que incluyes un fichero o código javascript en tu HTML, estás incluyendo un código que se carga de forma síncrona.

Cuando un navegador carga una página web, entre otras cosas, va recibiendo el código fuente HTML a mostrar. A medida que lo va recibiendo, lo lee y procesa. Cuando llega a un fichero Javascript, el navegador detiene la lectura y procesamiento del HTML, hasta que el navegador descarga el fichero javascript y lo ejecuta. Una vez concluye este proceso, continuará la carga de la página.

Cuando el código javascript es escaso, tiene poca importancia, pero si estás trabajando en una webapp o una página compleja, es habitual que tengas 10 librerías de javascript además de tu propio código, que utiliza dichas librerías… y ahí es dónde empezarían los problemas, seguro que te suenan:

  • Durante muchos segundos el navegador aparece cargando y la página totalmente blanca.
  • La página va cargando “a cachos”, de forma que poco a poco y como con retardo, van apareciendo los contenidos de la página, sin formato, y paulatinamente se va reestructurando hasta que al final se ve como debe ser.

Esto ocurre porque el DOM no está disponible al 100% hasta que el navegador ha descargado y procesado el contenido del código HMTL (las etiquetas, el CSS, los Javascript…)

¿Qué es la carga asíncrona de Javascript?

Para solucionar los daños colaterales que tiene la carga síncrona de javascript, en HTML tenemos el atributo async, para indicar al navegador que el código javascript de esa línea, se tiene que cargar de forma asíncrona, por lo que ese código javascript se descargará de forma paralela, sin detener la carga de la página, y se ejecutará nada más estar disponible.

De esta forma, el código javascript no bloquea la carga del DOM y, por lo tanto, la página está disponible mucho antes evitando la eterna página en blanco o la carga a saltitos.

Esto puede parecer maravilloso, pero no todo es tan bonito como parece. Vamos a ver con ejemplos todo lo que conlleva un tipo de carga y otra… y cómo la comunidad nos proporciona grandes soluciones.

Ejemplo de web con carga síncrona de javascript

Para realizar el análisis, utilizaremos las herramientas para desarrolladores de google chrome, en modo incógnito y con la caché desactivada. No hacemos las pruebas en localhost, para que los tiempos sean realistas. Además, utilizamos una conexión de móvil 3G en vez de ADSL, para tener en cuenta el impacto sobre los dispositivos móviles.

Resultado de la carga del primer documento con javascript de forma síncrona

Tenemos el siguiente código fuente, dónde incluimos varias librerías javascript, todas alojadas en el mismo espacio web que la página, para intentar unificar las pruebas lo más posible.

<!DOCTYPE html>
<html>
    <head>
        <title>Carga síncrona de javascript</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <script src="js/libs/html5shiv/html5shiv.js"></script>
        <script src="js/libs/respond.js/respond.js"></script>
        <script src="js/libs/modernizr/modernizr.js"></script>
        <script src="js/libs/jquery/jquery.js"></script>
        <script src="js/libs/jqueryui/jquery-ui.js"></script>
        <script src="js/libs/jquery-infinitescroll/jquery.infinitescroll.min.js"></script>
        <script src="js/libs/jquery-instagram/instagram.js"></script>
        <script src="js/libs/jquery-jcrop/js/jquery.color.js"></script>
        <script src="js/libs/jquery-jcrop/js/jquery.Jcrop.js"></script>
        <script src="js/libs/jquery-nivoslider/jquery.nivo.slider.js"></script>
        <script src="js/libs/jquery.qrcode/jquery.qrcode.min.js"></script>
        <script src="js/libs/jquery.touchswipe/jquery.touchSwipe.js"></script>
        <script type="text/javascript" src="js/global.js"></script>
    </head>
    <body>
        <div>Cargando...</div>
    </body>
</html>

Nuestro fichero global.js tiene una instrucción sencilla utilizando sintaxis jQuery

$(document).ready(function() {
    $("div").html("hola");
});

URL: http://rolandocaldas.com/ejemplos-blog/cargaAsincronaJavascript/indexSync.html

Google Developers Tools Network

Detalle sobre el tiempo de carga de la página

Detalle sobre el tiempo de carga de la página

De forma básica, la información que extraemos de esta gráfica es:

  • El navegador tiene que realizar 14 peticiones web para poder cargar nuestra página (1 del documento HTML y los 13 ficheros javascript)
  • Se han transferido 236KB
  • El DOM está disponible en 2.83s

Como el código javascript se carga de forma síncrona, el navegador no entra en el body hasta que todo el código javascript fue descargado y ejecutado, por lo que nos encontramos con que una página que sólo tiene un div con un texto “cargando…” en su interior, muestra la página totalmente blanca durante dos segundos, aunque el documento HTML lo descargue en sólo 413ms.

Problemas derivados de cargar forma síncrona el Javascript

El gran problema de la carga síncrona del javascript es el tiempo de espera. Como la carga de un código javascript detiene la carga de la página, es habitual que al entrar en una web, ésta empiece a mostrar parte de la misma, pero durante un tiempo quedarse a medias, porque está leyendo un código javascript que está embebido dentro del body del documento HTML.

Si el código Javascript se carga de la forma correcta, éste estará en el head del documento. Mientras el navegador está descargando y ejecutando los ficheros javascript del head de nuestro HTML, no está accediendo al body, por lo que la pantalla sale en blanco hasta que la ejecución termine. Para entender mejor este problema, vamos a utilizar un ejemplo dramatizado.

Mantenemos el mismo código HTML, pero ahora incluimos un nuevo fichero javascript a nuestra listado de ficheros a utilizar, por lo que nuestro HTML queda así:

URL: http://rolandocaldas.com/ejemplos-blog/cargaAsincronaJavascript/indexSyncDelay.html

<!DOCTYPE html>
<html>
    <head>
        <title>TODO supply a title</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <script src="js/libs/html5shiv/html5shiv.js"></script>
        <script src="js/libs/respond.js/respond.js"></script>
        <script src="js/libs/modernizr/modernizr.js"></script>
        <script src="js/libs/jquery/jquery.js"></script>
        <script type="text/javascript" src="js/delay.js"></script>
        <script src="js/libs/jqueryui/jquery-ui.js"></script>
        <script src="js/libs/jquery-infinitescroll/jquery.infinitescroll.min.js"></script>
        <script src="js/libs/jquery-instagram/instagram.js"></script>
        <script src="js/libs/jquery-jcrop/js/jquery.color.js"></script>
        <script src="js/libs/jquery-jcrop/js/jquery.Jcrop.js"></script>
        <script src="js/libs/jquery-nivoslider/jquery.nivo.slider.js"></script>
        <script src="js/libs/jquery.qrcode/jquery.qrcode.min.js"></script>
        <script src="js/libs/jquery.touchswipe/jquery.touchSwipe.js"></script>
        <script type="text/javascript" src="js/global.js"></script>
    </head>
    <body>
        <div>TODO write content</div>
    </body>
</html>

El contenido del fichero delay.js (el nuevo javascript) es el siguiente:

function doDelay(wait) {
    var date = new Date();
    var startDate = date.getTime();
    var a = 1;
    var b = 0;
    while (a !== 0) {
        date = new Date();
        if ((date.getTime() - startDate) >= wait) {
            a = 0;
        }
        b++;
    }
    alert("Salida del bucle.");
}

doDelay(15000);

¿qué hace este código? Bueno, es un código sencillo que emula las típicas funciones de delay de otros lenguajes. Lo que hacemos es que el Javascript entre en un bucle y sólo salga de él cuando hayan transcurrido 15 segundos, tras salir del bucle lanzará una alerta.

Como el javascript se carga de forma síncrona, cuando el navegador descargue este delay.js pasará a ejecutarlo y no continuará la carga de la web hasta que salgamos del bucle que nuestro código javascript ha creado. Por lo tanto, aunque nuestro documento HTML es sencillo, no veremos nada de nuestro HTML hasta que pasen, al menos, los 15 segundos del delay.

En el siguiente vídeo podemos ver el resultado de cargar esta página:

Como hemos podido ver, la carga de la web se ha demorado a los 22 segundos a pesar de que estamos hablando de una web extremadamente básica.

… y por culpa de un inocente fichero Javascript, la que es posiblemente la web más chiquita del mundo tarda 22 segundos en cargar!

Cierto es que no es normal crear un bucle que dure 15 segundos, pero situaciones como ésta son bastante habituales cuando usamos herramientas javascript de terceros como scripts de publicidad, contadores, etc. y, por lo que sea, su servidor está saturado y la descarga del fichero javascript que incluye se demora varios segundos… y como sea algo estilo addThis que te dan un código a colocar dónde quieres que salgan los iconitos de compartir, estás colocando javascript dentro del body, por lo que si existe cualquier problema con la carga de ese javascript, cualquier persona que entre en tu web verá la página hasta la zona dónde está el JS problemático haciendo que se vea feo feo.

Primera solución al problema de carga con el javascript síncrono

Como todo en este mundo, existen varias maneras de solucionar el problema que hemos mostrado en la web que tarda 22 segundos en cargar. La más extendida pasa por hacer las cosas un poco mal…

A grosso modo… si el problema está en que el código javascript bloquea la carga de la web… si cogemos y ponemos nuestras llamadas javascript problemáticas al final de nuestro body, como esos ficheros son lo último que mirará el navegador, al llegar a ese punto, el navegador ya habrá procesado el resto de la web. Sencillo ¿no?

De hecho WordPress utiliza mucho esta solución, si cualquiera ve el código fuente de su WordPress, encontrará bastante código javascript colocado antes del cierre del body.

Peeeeeeeeeeeeero ¿no se supone que no es una buena práctica? Exacto! No es una buena práctica colocar código javascript en el body, debería estar todo siempre en el head. Salvo que tu sitio web requiera una optimización extrema debido a unos niveles de tráfico brutales, en el que hay que recurrir a trucos poco formales (por así decirlo) para arañar todo lo posible, deberíamos seguir las buenas prácticas.

Como somos buenos y procuramos seguir las buenas prácticas, no voy a profundizar en esta “solución”.

Carga asíncrona de Javascript al rescate!

La carga asíncrona de javascript permite que el navegador descargue el javascript al mismo tiempo que continúa con la carga del documento HTML y el “procesado” del DOM.

Vamos a simplificar nuestro HTML, dejando sólo el javascript del delay para que no entre en juego el tiempo de carga de ningún otro fichero. Así pues, el HTML queda así:

<!DOCTYPE html>
<html>
    <head>
        <script type="text/javascript" src="js/delay.js"></script>
    </head>
    <body>
        <div>TODO write content</div>
    </body>
</html>

URL: http://rolandocaldas.com/ejemplos-blog/cargaAsincronaJavascript/indexSyncDelayAsync.html

Este es el resultado de hacerlo con carga síncrona:

La web tarda 18 segundos en mostrarse, aunque sólo cargamos un javascript mínimo

La web tarda 21 segundos en mostrarse, aunque sólo cargamos un javascript mínimo

Al cargarse de forma síncrona, podemos ver que el contenido del HTML no se muestra en el navegador hasta que finaliza la ejecución del código Javascript, o sea, hasta que aceptas la alerta que aparece.

Para evitar este problema, utilizamos el atributo async de HTML, editando nuestro HTML para dejarlo de la siguiente manera, pasando a cargar nuestro javascript de forma asíncrona.

<!DOCTYPE html>
<html>
    <head>
        <script type="text/javascript" src="js/delay.js" async="async"></script>
    </head>
    <body>
        <div>TODO write content</div>
    </body>
</html>

URL: http://rolandocaldas.com/ejemplos-blog/cargaAsincronaJavascript/indexSyncDelayAsync2.html

Gracias a la carga asíncrona, el documento HTML se visualiza casi al instante

Gracias a la carga asíncrona, el documento HTML se visualiza casi al instante

Como se puede comprobar, el resultado es muy diferente aunque los números son parejos. El documento sigue tardando mucho en cargar (18 segundos), pero tenemos un valor muy diferente: el tiempo del DOMContentLoaded.

Carga asíncrona de javascript para reducir el DOMContentLoaded

Cuando hemos cargado nuestro javascript de forma síncrona, el DOMContentLoaded tardó 20 segundos en estar disponible, sin verse nada de nuestra web, porque hasta que el DOM esté cargado no empieza a visualizarse por el navegador.

Esto ocurre porque al ser una carga síncrona, el navegador debe descargar y ejecutar todo el javascript antes de que el DOM esté disponible (por así decirlo). Como nuestro javascript contiene un bucle que dura 15 segundos, que se ejecuta directamente (no está dentro de ningún evento, etc), el problema está servido.

Sin embargo, cuando lo cargamos de forma asíncrona vemos que el evento DOMContentLoaded ocurre al trascurrir sólo 1.47 segundos. ¿Por qué? Al cargar el javascript de forma asíncrona, el navegador descarga este fichero de forma paralela a la preparación del DOM. Esto hace que el DOM estará disponible cuando termine la lectura del documento HTML, por eso en el segundo video, aparece el contenido del HTML en seguida, no como en el primero que hasta que termina el bucle (y aceptamos la alerta) no termina de cargarse el DOM y, por lo tanto, mostrar el contenido del HTML por el navegador. Por supuesto, el código javascript se descarga y ejecuta también al momento, por eso tras 15 segundos se lanza el alert.

DOMContentLoaded vs Load Event

En el informe “Network” de las herramientas para desarrolladores de Google, aparecen dos líneas verticales en el timeline, una azul y otra roja.

La línea azul representa el momento en el que el evento DOMContentLoaded es lanzado, o sea, cuando al web ya se ve por el navegador.

La línea roja, representa el momento en el que el evento Load es lanzado, o sea, cuando todo el contenido de nuestra web ha finalizado su carga y ejecución (HTML, imágenes, CSS y Javascript).

Cuando la carga es síncrona, estos dos eventos coinciden en el tiempo, por eso en las primeras capturas sólo se distingue una línea. Sin embargo, cuando se lleva a cabo la carga asíncrona de javascript, el evento para el DOMContentLoaded se ejecutará cuando el código HTML esté leído y procesado, mientras que el Load Event se demorará al momento en el que nuestro código javascript también se haya ejecutado. Es por eso que cuando cargamos el javascript de forma asíncrona, tendremos las dos líneas diferenciadas. La línea azul nos dirá el tiempo tardado en mostrar el código HTML y la roja el tiempo que tardamos, entre otras cosas, en tener toda nuestra web disponible.

Entonces con la carga asíncrona de Javascript todo son ventajas!

Lamentablemente no. Los ficheros javascript que se cargan de forma asíncrona, se descargan en paralelo sin un orden concreto.

Cuando cargamos el Javascript de forma síncrona, el navegador llega a la primera línea de fichero Javascript y detiene el proceso de carga mientras descarga y procesa ese Javascript. Cuando ya termina de ejecutar ese fichero, continúa la carga.

Al hacerlo de forma asíncrona, el proceso es el mismo, pero sin detener en ningún momento la carga, lo que significa que todos nuestros ficheros javascript iniciarán su descarga prácticamente al mismo tiempo y se ejecutarán cada uno a su tiempo (cuando esté disponible). Pero, ¿por qué esto es malo? Pensemos en cómo usamos el Javascript!

Normalmente utilizamos jQuery u otro framework de Javascript y nuestro código depende del framework.  Además, nuestro código suele ser más liviano que el framework (lo que es normal claro), por lo que pesa menos. A la hora de descargar ambos ficheros en paralelo, nuestro javascript se descargará antes que el framework, lo que significa que se ejecutará también antes… y claro, en ese momento, como nuestro framework JS no está disponible, el navegador no encontrará, por ejemplo, el típico:

$(document).ready(function() {
    /* ... */
});

Por lo que nos dará un error de Javascript …. y ala, se acabó la ejecución de nuestro Javascript en la página. Desastre absoluto!

Probando el problema de la carga asíncrona

Vamos a comprobar cómo una carga asíncrona de javascript sin control, causa problemas.

Tenemos este código HTML:

<!DOCTYPE html>
<html>
    <head>
        <title>TODO supply a title</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <script src="http://ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js" type="text/javascript" async="async"></script>
        <script src="js/global.js" type="text/javascript" async="async"></script>
    </head>
    <body>
        <div>TODO write content</div>
    </body>
</html>

y nuestro global.js es el siguiente:

$(document).ready(function() {
    $("div").html("hola");
});

Ejecutamos la página!

URL: http://rolandocaldas.com/ejemplos-blog/cargaAsincronaJavascript/index_async.html

Explicando el problema de la carga asíncrona de JavaScript

En el vídeo podemos comprobar cómo se produce un error de javascript, pero ¿por qué si nuestro código Javascript es correcto? Sencillo: Estamos cargando de forma asíncrona ficheros dependientes.

Echemos un vistazo a la captura de la pestaña network del google developer tools:

El proceso de carga de nuestra web con el javascript de forma asíncrona

El proceso de carga de nuestra web con el javascript de forma asíncrona

Como el Javascript se carga de forma asíncrona, podemos ver cómo se inicia al mismo tiempo la descarga de los dos ficheros de javascript incluidos en nuestro HTML: El framework jQuery y nuestro global.js

La descarga del fichero global.js se realiza antes de la de jquery, puesto que ocupa mucho menos espacio. Recordemos también que una vez se descarga el fichero JS, se ejecuta el javascript.

Al mismo tiempo, tenemos que el evento DOMContentLoaded se lanza transcurridos 1.86 segundos de carga, momento en el cual aún no está descargado el jQuery.

Volviendo a nuestro global.js, el fichero se descarga y se ejecuta… y en el momento en el que se ejecuta lo que lee el navegador es esta línea:

$(document).ready(function() {

Ahí es cuando obtenemos el siguiente error:

Error al cargar ficheros Javascript dependientes de forma asíncrona uando HTML5

Error al cargar ficheros Javascript dependientes de forma asíncrona uando HTML5

El error que el navegador nos indica es que $ no está definido. El identificador $ es creado por el framework jQuery y como nuestro global se ejecuta antes que jquery (al descargarse antes), el problema está servido.

Solucionando el problema de la carga asíncrona de javascript

¿Cómo podemos solucionar el problema? Lo primero es recordar por qué queremos cargar el javascript de forma asíncrona. El objetivo es que el visitante de nuestra página tenga, lo antes posible, una versión de nuestra web cargada y evitar las eternas esperas con la pantalla en blanco o esos momentos en los que la página está cargada por partes y se ve horrible al no estar todo el contenido todavía disponible.

Bien, con ese objetivo en mente, podemos valorar una solución alternativa: Que el javascript que necesitamos, se descargue y ejecute una vez esté el DOM cargado. Con esta idea en mente, podemos encontrar muchas soluciones en la red, aplicando el patrón lazy loading y la carga condicional de Javascript, como yepnope (cargador implementado en Modernizr aunque ya marcado como deprecate), angular-loadscript-with-documentwrite.js para AngularJS (el framework MVC de Javascript by Google), RequireJS, etc.

Este tipo de herramientas se merecen una entrada entera para cada una, por lo que siguiendo su espíritu, aplicaremos un código “javascript puro”.

En stackoverflow encontramos un interesante hilo sobre la carga asíncrona de javascript y una función de javascript para llevar a cabo la carga de javascript, permitiendo la ejecución de una función vía callback, para poder cargar javascript de forma dependiente.

Vamos a utilizar esa función para cargar jQuery y nuestro global.js. Para ello, creamos un fichero load.js con el siguiente código:

<!DOCTYPE html>
<html>
    <head>
        <title>TODO supply a title</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <script src="js/load.js" type="text/javascript" async="async"></script>
    </head>
    <body>
        <div>TODO write content</div>
    </body>
</html>

Lo que hacemos es incluir la función de javascript para la carga asíncrona de javascript. A esta función la llamaremos cuando se lance el evento window onload, que forma que ya tengamos el DOM disponible. Como necesitamos que el global.js se cargue una vez tenemos disponible a jQuery, cargaremos jQuery utilizando como callback la llamada a la carga del global.js

Como utilizamos el fichero load.js para cargar el código javascript que necesitamos, en el HTML sólo llamaremos al load.js, por lo que nuestro HTML queda de la siguiente manera:

function addEvent(element, event, fn) {
    if (element.addEventListener) {
        element.addEventListener(event, fn, false);
    } else if (element.attachEvent) {
        element.attachEvent('on' + event, fn);
    }
}

//this function will work cross-browser for loading scripts asynchronously
function loadScript(src, callback) {
  var s,
      r,
      t;
  r = false;
  s = document.createElement('script');
  s.type = 'text/javascript';
  s.src = src;
  s.onload = s.onreadystatechange = function() {
    //console.log( this.readyState ); //uncomment this line to see which ready states are called.
    if ( !r && (!this.readyState || this.readyState == 'complete') )
    {
      r = true;
      if (callback !== undefined) {
        callback();
      }
    }
  };
  t = document.getElementsByTagName('script')[0];
  t.parentNode.insertBefore(s, t);
}

addEvent(window, 'load', function(){ loadScript(
        'http://ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js', 
        function () { loadScript('js/global.js')} );
});

Veamos el resultado!

URL: http://rolandocaldas.com/ejemplos-blog/cargaAsincronaJavascript/index_async_sol.html

Conseguimos que el DOM esté disponible antes de cargar nuestros javascript

Conseguimos que el DOM esté disponible antes de cargar nuestros javascript

Como se ve en el video y en la captura del Developer tools, el navegador descarga el documento HTML y el cargador de Javascript antes de que el evento DOMContentLoaded se dispare. Por este motivo, desde el primer momento el navegador muestra la web con el texto “TODO write content”.

Una vez la web está cargada y el contenido base de la web se visualiza, se lanza el evento de window loaded ejecutándose la carga de nuestro ficheros Javascript. Primero se carga el framework jQuery para acto seguido cargarse el global.js cuya ejecución seiguiendo la sintaxis de jQuery, cambia nuestro “TODO write content” por “hola”.

Con esto conseguimos que casi desde el principio de la carga de nuestra web, el visitante no tiene la sensación de que la página no carga, lo que favorece que espere a que ésta finalice.

Ejemplo final: Carga asíncrona de Javascript

Para finalizar, vamos con un ejemplo final de carga asíncrona de Javascript. Como cada vez es más habitual que nuestra web o webapp tarde unos segundos en estar 100% disponible, podemos aprovechar las animaciones de CSS3 junto con la carga asíncrona de Javascript para mostrar una ventana estilo splash screen que vaya mostrando el proceso de carga de nuestra web.

En el código existe una llamada a un alert para que se pueda apreciar mejor el splash puesto que como el ejemplo es pequeño, no es apreciable sin una “ayudica”.

El código HTML:

<!DOCTYPE html>
<html>
    <head>
        <title>TODO supply a title</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link href="style.css" rel="stylesheet" type="text/css">
        <script src="js/load_plus.js" type="text/javascript" async="async"></script>
    </head>
    <body>
        <div id="page">
            TODO write content
        </div>
        <div class="loading">
            <div class="big-circle"></div>
            <div class="little-circle"></div>
            <div class="content" id="loadingContent">Loading...</div>
        </div>
    </body>
</html>

El fichero load_plus.js:

function addEvent(element, event, fn) {
    if (element.addEventListener) {
        element.addEventListener(event, fn, false);
    } else if (element.attachEvent) {
        element.attachEvent('on' + event, fn);
    }
}

function loadScript(src, callback)
{
  var s,
      r,
      t,
      write;
      
  write = src.split("/");
  document.getElementById('loadingContent').innerHTML = 'Loading ... ' + write[(write.length - 1)] + ' ... ';
  alert(''); // eliminar para evitar la detencion de carga.
  r = false;
  s = document.createElement('script');
  s.type = 'text/javascript';
  s.src = src;
  s.onload = s.onreadystatechange = function() {
    if ( !r && (!this.readyState || this.readyState == 'complete') )
    {
      r = true;
      if (callback !== undefined) {
        callback();
      }
    }
  };
  t = document.getElementsByTagName('script')[0];
  t.parentNode.insertBefore(s, t);
}

addEvent(window, 'load', function(){ loadScript(
        'http://ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js', 
        function () { loadScript('js/global_plus.js')} );
});

El fichero global_plus.js:

$(document).ready(function() {
    $(".loading").hide();
    $("#page").show();
});

El style.css:

body {
    background: rgba(0, 0, 0, 0.2);
}

#page {
    display: none;
}

.loading {
    background: rgba(0, 43, 255, 0.3);
    bottom: 0;
    box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.5);
    box-sizing: border-box;
    height: 200px;
    left: 0;
    margin: auto;
    overflow: hidden;    
    padding: 25px;
    position: fixed;
    right: 0;
    top: 0;
    width: 250px;
}

.loading .big-circle {
    -moz-animation: circleEffectOut 1s infinite linear;
    -o-animation: circleEffectOut 1s infinite linear;
    -webkit-animation: circleEffectOut 1s infinite linear;
    animation: circleEffectOut 1s infinite linear;

    background: rgba(0,0,0,0);
    border-bottom: 10px solid rgba(86, 16, 36, 0.7);
    border-left: 5px solid rgba(0,0,0,0);
    border-radius: 100%;
    border-right: 5px solid rgba(0,0,0,0);
    border-top: 10px solid rgba(86, 16, 36, 0.7);
    box-shadow: 0 0 35px rgb(16, 86, 66);
    box-sizing: border-box;
    height: 150px;
    margin: 0 auto;
    opacity: 0.9;
    width: 150px;
}

.loading .content {
    padding: 0 10px;
    position: relative;
    text-align: left;
    top: -100px;
}

.loading .little-circle {
    -moz-animation: circleEffectIn 1s infinite linear;
    -o-animation: circleEffectIn 1s infinite linear;
    -webkit-animation: circleEffectIn 1s infinite linear;
    animation: circleEffectIn 1s infinite linear;
    
    background-color: rgba(0,0,0,0);
    
    border-bottom: 5px solid rgba(16, 86, 66, 0.7);
    border-left: 5px solid rgba(0,0,0,0);
    border-radius: 100%;
    border-right: 5px solid rgba(0,0,0,0);
    border-top: 5px solid rgba(16, 86, 66, 0.7);
    box-shadow: 0 0 15px rgb(86, 16, 36);
    box-sizing: border-box;

    height: 100px;
    margin: 0 auto;
    opacity: 0.9;
    position: relative;
    top: -125px;
    width: 100px;
}

@-moz-keyframes circleEffectOut {
    0% {
        -moz-transform: rotate(160deg);
        opacity: 0;
        box-shadow: 0 0 1px rgb(16, 86, 66);
    }

    50% {
        -moz-transform: rotate(145deg);
        opacity: 1;
    }

    100% {
        -moz-transform: rotate(-320deg);
        opacity: 0;
    };
}

@-webkit-keyframes circleEffectOut {
    0% {
        -webkit-transform: rotate(160deg);
        opacity: 0;
        box-shadow: 0 0 1px rgb(16, 86, 66);
    }

    50% {
        -webkit-transform: rotate(145deg);
        opacity: 1;
    }

    100% {
        -webkit-transform: rotate(-320deg);
        opacity: 0;
    }
}

@-moz-keyframes circleEffectIn {
    0% {
        -moz-transform: rotate(0deg);
    }

    100% {
        -moz-transform: rotate(360deg);
    };
}

@-webkit-keyframes circleEffectIn {
    0% {
        -webkit-transform: rotate(0deg);
    }

    100% {
        -webkit-transform: rotate(360deg);
    }
}

Y el resultado:

URL: http://rolandocaldas.com/ejemplos-blog/cargaAsincronaJavascript/index_async_loading.html

Cada vez es más importante que el usuario tenga buenas sensaciones desde que empieza a acceder a la página mejorando así su experiencia desde el principio. Una cuidada estrategia en la carga de los ficheros javascript es un buen punto de inicio. Además, si hacemos lo mismo con los ficheros CSS, podemos mostrar una ventana de carga al más puro estilo de las aplicaciones de escritorio o móviles y mostrar la página cuando la experiencia pueda ser completa… junto con la sensación de profesionalidad que aporta!

Actualización 12 de Agosto de 2014

Se han incorporado dos ejemplos nuestros en la demo online del artículo.

http://rolandocaldas.com/ejemplos-blog/cargaAsincronaJavascript/index_async_loading_jquery.html
Muestra cómo utilizar la función loadScript para cargar varios plugins de jQuery, de forma que carga el framework jquery y se van anidando callbacks para ir incluyendo cada plugin de jQuery preciso, dejando para el último callback nuestro fichero js.

http://rolandocaldas.com/ejemplos-blog/cargaAsincronaJavascript/index_async_loading_jquery_multiple.html
Es una modificación de loadScript para permitir que el primer parámetro sea un string o un array, de forma que si se trata de un array incluya como script cada elemento del array y lance el evento para ejecutar el callback sólo en el último elemento del array. De esta manera, reducimos el anidamiento de callbacks, pudiendo cargar un fichero javascript, asociándolo como carga dependiente todos los plugins jquery que querramos y asociar, a la carga de todos los plugins, la dependencia de nuestro fichero js propio.

Recursos disponibles

38 pensamientos en “Carga asíncrona de Javascript

  1. ¡Fantástica explicación! Aunque tengo una pregunta: ¿Si aparte de jQuery, tuviera que cargar más scripts como plugins de jQuery, Boostrap y otros, funcionaría correctamente la función que has explicado o debería hacer algo más?
    Muchas gracias

    1. Buenas! gran pregunta… Pues en principio con esa función llegaría, aunque tendrías que ir anidando callbacks, he dejado un ejemplo de cómo sería en http://rolandocaldas.com/ejemplos-blog/cargaAsincronaJavascript/index_async_loading_jquery.html de forma que agrega varios plugins de jquery y un js propio para utilizar el plugin de jquery.qr

      Además, se puede modificar un poco la función, para que el loadScript pueda aceptar como primer parámetro un string o un array, de forma que al pasarle un array se incluya cada elemento del array como script y que sólo al último se le incluya la captura del evento onload u onreadystatechange para cargar el fichero js dependiente de todos los elementos del array, de forma que simplifiquemos un poco la llamada al cargador. Esto lo puedes ver en http://rolandocaldas.com/ejemplos-blog/cargaAsincronaJavascript/index_async_loading_jquery_multiple.html.

      En ambos casos el resultado es el mismo, aunque un poco más interesante es el segundo.

      saludos!

  2. ¡Muchísimas gracias por la respuesta! La verdad es que el tema me interesa mucho ya que estoy desarrollando una nueva versión de la web de mi empresa y uno de los requisitos es que sea más rápida que la actual pero sin renunciar a ciertos elementos JavaScript que aparecen en ella.
    Repito, muchísimas gracias.
    Saludos

  3. Muy intesesante el articulo. estoy tratando de aplicarlo pero al tratar de llamar una funcionalidad de jquery desde global_plus.js algunas veces lo carga y otras no. algunas veces dice que la function $ no esta definida

    1. Hola! Alguno de los ejemplos del artículo están hechos a propósito para que se genere ese error. Cuando cargas Javascript de forma asíncrona, ese error es habitual. Por eso es necesario que cuando se carga Javascript de forma asíncrona se establezca una forma de que los ficheros JS que dependen de otro no carguen hasta que finalice la carga de su dependencia.

      saludos!

  4. Pingback: ¿Cómo funciona Google Tag Manager? -

  5. Buenas,

    Enhorabuena por el artículo lo he seguido al pie de la letra y me ha funcionado genial. Estoy montando una tienda y ya es asíncrona. Muchas Gracias !!!!

  6. Sos una máquina!! algún día te voy a mostrar como quedó mi app después de este post. Gracias
    Saludos desde Argentina

  7. Raymundo Toledo
    Responder

    Increíble! Me han quedado claras bastantes cosas que alguna vez me pregunte! Muchas gracias por el aporte y sobre todo la forma de explicarle.

  8. Manuel Alcocer
    Responder

    Hola, que tal? Excelente tutorial, solo me queda una duda, en caso de que tenga scripts adicionales que esten implícitos en el html ‘$(function(){…});’ que están debajo de este script, como puedo esperar o detectar a que jquery sea cargado, ya que estos varian de según la pagina.

    He tratado de modificar el script para que acepte funciones directas y no solo scripts, pero no nada me ha funcionado en concreto.

    De antemano, gracias.

  9. Hola muy buena la explicación una pregunta a mi sitio web se ha introducido un script de Async que yo no he agregado y tienen como destino (pcm1.map.pulsemgr) y otras mas similaress como hago pa quitarlas o detenerlas pa que no vuelvan a aparecer

    gracias de antemano

    1. Buenas, pues o bien estás utilizando algún plugin-script de terceros que carga ese fichero o te han “hackeado”. A priori pensaria en un plugin… es muy habitual que en CMS como wordpress instalemos plugins “a casco porro” que luego carguen un sinfín de ficheros externos

      saludos

  10. Buenas Rolando, excelente artículos los publicados sobre optimización de carga de css y javascript. Así mismo no se si te seria posible uno sobre optimización en la carga de imagenes. O sea, ademas de compresión de la misma y reducción de tamaño, una forma de que se carguen como en background, que no afectasen el desenvolvimiento del contenido. Ejemplo, suponte un sitio que tiene un slider de 10 imágenes de media pantalla cada uno, todos cargándose en arranque, se vuelve lentísimo, agrégale que son imágenes hd de unos 400k cada una, prácticamente se guindaría el sitio. Que solución seria buena para este sitio?

  11. Hola Rolando ¿cómo estás?
    Veo que eres un experto en programación, pues aquí te lanzo mi pregunta entonces:

    ¿Cuáles pueden ser los motivos por los que un código Javascript – de un pixel de anunciante – en algunas ocasiones no se vea en el código fuente? ¿es un tema del navegador, es un tema de la pc?

    Muchas gracias por tu asistencia.

  12. Hola, en blogger he puesto en el head el código y antes de la etiqueta de cierre del script el código largo del function addEvent, creo que lo he puesto bien pero no me deja guardar la plantilla, me da este fallo, “The entity name must immediately follow the ‘&’ in the entity reference”.

  13. Hola, la parte del código que contiene el function addEvent, ¿en qué parte de la plantilla se coloca?, por si influye en algo estoy en blogger.

    1. Ya lo he visto, se pone inmediatamente debajo encerrado entre dos nuevas etiquetas script, había otra cosa entre el primer y segundo código y me daba error.

  14. No tengo servidor ni fichero, estoy en blogger, ¿qué línea tendría que poner para realizar la carga asíncrona sin fallar jquery?

  15. Hola, tengo una duda, he aplicado tu solución asíncrona a una aplicación pero carga todo el html junto, cada div uno encima del otro según se los va encontrando dentro de hasta que se ejecute el último script y ya comience el funcionamiento normal de la página. ¿cómo se puede solucionar esto de manera que mientras se estén descargando los scripts se muestre un solo div de todos los que hay y no se vayan mostrando todos los que hay en el body?.

    Gracias

  16. Buenos días amigo, excelente explicación, pero todavía tengo dudas, le explico un poco lo que quiero hacer.

    Estoy desarrollando una aplicación que genera unas cuotas en lotes, y la cantidad de clientes son mas de 4000, lo he hecho que el ciclo for lo haga del lado del servidor, es decir, en php pero siempre supera el limite de tiempo de ejecución, así lo aumente a mas de 5 horas por la configuración del php.ini siempre lo supera, entonces opte por hacerlo del lado del cliente, es decir en javascript, entonces lo que hice fue cargar la lista de los clientes por ajax trayendo el resultado en json y luego en la propiedad success de este primer ajax hago el ciclo for y cargo otro ajax para que llame constantemente la creación de las cuotas por cada cliente. y lo realiza en 15min aproximadamente, pero el navegador chrome (no he probado en otro) cada 2 minutos muestra un mensaje que la pagina esta esta tardando mucho, que si esperar o cerrar y es muy tedioso.. yo le hice la parte en que bloquea la pagina mientras el primer ajax se ejecuta mostrando un mensaje que se esta generando las cuotas pero se queda congelado y empieza el mensaje de chrome que esta tardando mucho la pagina, este ajax esta de forma asíncrono, y me imagino que se queda congelado es por el ciclo for dentro del success, porque luego de un rato completa el codigo con el mensaje que se genero correctamente y todo vuelve normal, pero todavia estan muchas peticiones del otro ajax ejecutandose y si la persona actualiza la pagina obviamente el trabajo se pierde o queda por la mitad. todo esto es para saber si hay alguna manera de saber si se completaron todos los ajax en ejecucion o no se otra lógica que pudiera aplicar para que este tenga un mensaje de espera sin que se quede congelado, lo intente por rango de clientes pero es tedioso también de 500 en 500, espero me haya entendido y me pueda guiar a la idea que necesito… muchas gracias.

  17. Hermes Sanchez
    Responder

    Excelente articulo, muchas gracias! 🙂 Ahora tengo un problema, tengo un proceso que dura mas de 5min y talves tenga que durar mucho mas; Es un proceso absolutamente necesario. Cuando ocurre, me salta un mensaje de que un proceso de javascript esta demorando mucho y por eso se cuelga mi navegador. Ahora deseo poner una barra de progreso para ver que sucede pero como soy nuevo en esto, no estoy seguro por donde caminar. Muchas gracias y disculpas por mi ignorancia de antemano.

    1. Buenas! Si estás haciendo un proceso con JS que dura tanto como dices es un problema de diseño de tu aplicación, tienes que buscar una forma diferente de modelizar la solución

  18. Quiero que me jale la variable en la función , pero no lo hace, mis librerías las he puesto en
    Disculpa está bien este proceso?

    //CAMPO DEL FORMULARIO
    <input name="cccodcli" class=detailedViewTextBoxTVdis id="cccodcli" type="text" value="” size=”20″>

    function selectPotentialC()
    {
    <?php

    //LLAMANDO A LA VARIABLE GLOBAL
    global $adb;

    //JALANDO EL CODIGO DEL CLIENTE A PARTIR DE JAVASCRIPT Y GUARDANDOLO EN UNA VRIABLE PHP
    $var=' document.getElementById(“reflejar”).innerHTML;
    document.write(mensaje);’;

    //guardando el query de consulta de id del cliente del registro
    $query=”SELECT DISTINCT vacf.accountid as accountid FROM vtiger_accountscf as vacf

    INNER JOIN vtiger_account as va ON va.accountid=vacf.accountid
    INNER JOIN vtiger_potential as vp ON vp.related_to=va.accountid

    WHERE vacf.cf_616=’$var'”;

    //asignando en el query de la variable de registro
    $result = $adb->pquery($query,””);
    $accountid = $adb->query_result($result, 0, “accountid”);
    ?>
    // To support both B2B and B2C model
    var record_id = “”;

    ….
    ….
    ….

  19. una pregunta sencilla soy novato tengo un registro con opcion eliminar le puse un scrip que me difa q esta eliminado o error al eliminar me funciona el problema es que una ves q carga el escrip con el mensaje se va a una pagina blanca como puede hacer para q solo me apresca el escrip y no se vaya a otra pagina

  20. Hola Rolando, de una vez por todas quedo satisfecho con una explicación, creé y mantengo actualmente la web de mi empresa y he podido ver en tu artículo lo que yo siempre he pensado y es que cuando algo es importante se ha de explicar cómo se debe, aunque en ello se emplee tiempo y texto algo que según algunos por norma dicen que deben de ser lo más reducido y explícito posible aunque en el camino se quede lo verdaderamente importante que es que se entienda el mensaje, contenido, ensamblaje y calidad de las cosas.

    Por este motivo te felicito incluso antes de poner en práctica tus consejos, algo explicado con tantas ganas de que se entienda no puede estar mal, seguro.

    Gracias por poner ese empeño en que quede claro con todo lo necesario para que así sea, sigue así porque es la única forma en la que se ayuda a los demás si es eso lo que se persigue.

    Un saludico y un abracico. ¡Animo!

  21. hola es mi primera vez que utilizo javascript quisiera saber cuando iniciamos a utlizar javascript en el archivo .js siempre empiesa function a si como inicia y termina es que a mi me aparece en blanco al cargar el navegador sera que la sintaxis de javascript esta mal ? por favor necesito ayuda por favor necesito respuesta pronta es ES URGENTE gracias

  22. Hola estoy trabajando con un spreadsheet y google apps script, estoy configurando dos diferentes formularios para cargar la informacion en el spreadsheet todo iba bien hasta que tuve que configurar el index para cargar los formularios tuve que separar el codigo script del html y llamarlos desde el index (llamo el codigo script con ;)el problema con eso es que llama el script antes de cargar el formulario no hay otra forma de hacerlo???

Deja un comentario