Cargar Javascript. Blocking vs non-blocking

07 Ene 2011

Durante el pasado año, hemos visto numerosos artículos en la prensa especializada hablando sobre cómo los navegadores gestionan la carga de los archivos Javascript. Como resultado de esto, se han puesto de moda los conceptos anglosajones de blocking y non-blocking para referirse a cómo los scripts pueden ser bloqueantes y no-bloqueantes con respecto al contenido de una página web.

Esta distinción la introdujo por primera vez en escena Steve Souders, ingeniero de Google y autor de dos de los manuales sobre optimización de páginas webs más influyentes: High Performance Web Sites: Essential Knowledge for Front-End Engineers y Even Faster Web Sites: Performance Best Practices for Web Developers. Souders centró su atención en cómo las etiquetas <script> bloquean el renderizado de los elementos de una página así como la descarga de otros elementos. Este comportamiento puede causar que nuestro sitio aparezca en blanco durante el tiempo que un script pesado necesite en descargarse, interpretarse y ejecutarse. Para evitar la confusión que puede generar entre nuestros usuarios el ver la página en blanco, surgió la idea de implementar código Javascript no-bloqueante que permita continuar el renderizado mientas el script está siendo descargado.

Cabe aclarar aquí que existe también cierto grado de confusión entre lo que denominamos Javascript no-bloqueante y la descarga en paralelo de archivos.

Como bien explica Nicholas Zakas, la descarga en paralelo no debe ser confundida con la ejecución asíncrona: Javascript es un lenguaje mono hilo, lo que quiere decir que no puede ejecutar dos scripts al mismo tiempo. La descarga en paralelo, disponible en los navegadores recientes, significa únicamente que dos scripts pueden ser descargados a la vez, pero no ejecutados: éstos lo harán manteniendo el orden en el que han sido dispuestos y solo de uno en uno. Cuando hablamos de descargar Javascript de una manera no-bloqueante, nos referimos a que liberamos al navegador para continuar renderizando el contenido de una página mientras el script está siendo descargado. La carga de estos archivos se realiza de una forma asíncrona, pero la ejecución continuará realizándose de forma secuencial.


El porqué Javascript detiene el proceso de renderizado tiene su sentido en la naturaleza de este lenguaje: una página comienza a renderizarse tras descargarse; cuando encuentra una etiqueta <script>, el navegador se detiene porque el código Javascript puede afectar a la interfaz de usuario moviendo elementos, eliminándolos o creando otros nuevos, así que espera. Únicamente cuando todos los cambios se completan, continúa el renderizado con la certeza de que todo está al día.

FORMAS DE CARGAR JAVASCRIPT NO-BLOQUEANTE

Existen tres formas de crear scripts no-bloqueantes en Javascript.

MÉTODO 1

El primer método consiste en utilizar el atributo defer en las etiquetas <script>. Añadiendo defer, el navagador comienza la descarga del archivo Javascript inmediatamente pero no bloquea el renderizado o la descarga del resto de elementos de la página:

<script type="text/javascript" defer src="foo.js"></script>

Cuando se utiliza el atributo defer el script no se ejecuta hasta que la pagina ha sido cargada completamente pero antes del evento DOMContentLoaded. Salvo en Internet Explorer, el orden de ejecución se establece según aparezcan en el documento.

El atributo defer está soportado por IE4+, Firefox 3.5+, Safari 5+, y Chorme 7+.

MÉTODO 2

El segundo método utiliza el atributo async de las etiquetas <script> en HTML5. Igual que el anterior, los scripts se descargan inmediatamente de un modo no-bloqueante.

<script type="text/javascript" async src="foo.js"></script>

La diferencia entre los atributos asyn y defer es que el primero se ejecuta tan pronto como el script es descargado, por lo que la página puede estar aún descargándose. Una segunda diferencia es que el orden de ejecución para los archivos cargados mediante async no está garantizado. La única garantía es que los scripts async son ejecutados antes del evento load.

MÉTODO 3

El último método, y el más popular, es usar etiquetas <script> dinámicas creadas con Javascript.

var script = document.createElement("script");
script.type = "text/javascript";
script.src = "foo.js";
document.getElementsByTagName("head")[0].appendChild(script);

Cuando un script se inserta de forma dinámica, se comienza a descargar inmediatamente de una manera no-bloqueante. El código se ejecuta tan pronto como es descargado. En la mayoría de navegadores, no se garantiza el orden de ejecución.

CONSIDERACIONES IMPORTANTES

Los tres métodos anteriores permiten al navegador continuar renderizando el contenido mientras descargamos los archivos Javascripts necesarios.

Sin embargo, hay que tener en cuenta que todos estos métodos bloquean el evento onload de la página retrasándolo hasta que los scripts se han ejecutado completamente.

Dependiendo de nuestros intereses, retrasar el evento window.onload puede ser útil; sin embargo, para la mayoría de aplicaciones, suele interesar lanzar este evento tan pronto como sea posible. Si estamos utilizando una arquitectura de escalado progresivo donde puede ser correcto el cargar los archivos más tarde, podemos considerar el incluir las etiquetas <script> usando un temporizador:

//doesn't block the load event
setTimeout(function(){
  var script = document.createElement("script");
  script.type = "text/javascript";
  script.src = "foo.js";
  document.getElementsByTagName("head")[0].appendChild(script);
}, 0);

Utilizando un temporizador con un valor 0, el código se ejecuta tan pronto como sea posible una vez cargada la página principal. Hay que considerar sin embargo que este método no bloquea el evento, pero tampoco garantiza si se ejecutará antes o después del mismo.

Si queremos asegurarnos de que el código no comenzará a ejecutarse hasta que el evento window.onload haya sido lanzado, podemos recurrir a él para insertarlo:

//doesn't block the load event
window.onload = function(){
  var script = document.createElement("script");
  script.type = "text/javascript";
  script.src = "foo.js";
  document.getElementsByTagName("head")[0].appendChild(script);
};

CONCLUSIÓN

Con este último ejemplo, hemos cubierto todas las posibilidades de las que disponemos para cargar Javascript de una manera no-bloqueante. Defer, async y los scripts cargados de forma dinámica permiten que los navegadores continúen renderizado el contenido mientras se descargan. Sin embargo, todos estos métodos paralizan el evento onload de la página hasta que no se han ejecutado completamente. Añadiendo un temporizador, podemos asegurarnos de que los scripts no bloquean el evento y nuestro window.onload se disparará tan pronto como le sea posible.

MÁS INFORMACIÓN:

Nicholas Zakas, What is a non-blocking script?
Steve Souders, Loading Scripts Without Blocking
Felix Geisendörfer, Run intense JS without freezing the browser
John Resig, How JavaScript Timers Work

Más:

{3} Comentarios.

  1. emilio

    interesante artículo, lo voy a poner en practica en mi sitio web para minimizar los tiempos de carga
    además estaría bueno destacar, que se puede generar un solo archivo js en vez de varios archivos beneficiando aun mas la carga del servicio.

  2. Claudio

    Excelente artículo. Cuesta bastante diferenciar entre async y defer aunque los dos son efectivos.

  3. Lucas

    Hay mas metodos para bajar en paralelo, respetar el orden o no… etc. Te dejo algo propio que va a salir mal parseado (como texto plano) pero bueno:

    var LKZ = {};
        LKZ.Script = {
            loadScript: function(url, onload) {
                LKZ.Script.loadScriptDomElement(url, onload);
            },
    
            loadScripts: function(aUrls, onload) {
                var nUrls = aUrls.length;
                var bDifferent = false;
                for(var i = nUrls; i--;) {
                    if(LKZ.Script.differentDomain(aUrls[i])) {
                        bDifferent = true;
                        break;
                    }
                }
    
                var loadFunc = LKZ.Script.loadScriptXhrInjection;
                if(bDifferent) {
                    if(-1 != navigator.userAgent.indexOf('Firefox') || -1 != navigator.userAgent.indexOf('Opera')) {
                        loadFunc = LKZ.Script.loadScriptDomElement;
                    } else {
                        loadFunc = LKZ.Script.loadScriptDocWrite;
                    }
                }
    
                for(var i = 0; i < nUrls; i++) {
                    loadFunc(aUrls[i], (i+1 == nUrls ? onload : null), true);
                }
            },
    
            differentDomain: function(url) {
                if(0 === url.indexOf('http://') || 0 === url.indexOf('https://')) {
                    var mainDomain = document.location.protocol + "://" + document.location.host + "/";
                    return (0 !== url.indexOf(mainDomain));
                }
                return false;
            },
    
            loadScriptDomElement: function(url, onload) {
                var domscript = document.createElement('script');
                domscript.src = url;
                if(onload) {
                    domscript.onloadDone = false;
                    domscript.onload = function() {
                        if(!domscript.onloadDone) {
                            domscript.onloadDone = true;
                            onload();
                        }
                    };
                    domscript.onreadystatechange = function() {
                        if(("loaded" === domscript.readyState || "complete" === domscript.readyState) && !domscript.onloadDone) {
                            domscript.onloadDone = true; //Opera trick
                            onload();
                        }
                    }
                }
                document.getElementsByTagName('head')[0].appendChild(domscript);
            },
    
            loadScriptDocWrite: function(url, onload) {
                document.write('');
                if(onload) {
                    LKZ.addHandler(window, "load", onload);
                }
            },
    
            queuedScripts: new Array(),
    
            loadScriptXhrInjection: function(url, callback, inOrder) {
                var iQueue = LKZ.Script.queuedScripts.length;
                if(inOrder) {
                    var qScript = {response: null, onload: callback, done: false};
                    LKZ.Script.queuedScripts[iQueue] = qScript;
                }
    
                var xhrObj = LKZ.Script.getXHRObject();
                xhrObj.onreadystatechange = function() {
                    if(xhrObj.readyState == 4) {
                        if(inOrder) {
                            LKZ.Script.queuedScripts[iQueue].response = xhrObj.responseText;
                            LKZ.Script.injectScripts();
                        } else {
                            var se = document.createElement('script');
                            document.getElementsByTagName('head')[0].appendChild(se);
                            se.text = xhrObj.responseText;
                            if(callback) {
                                callback();
                            }
                        }
                    }
                };
                xhrObj.open('GET', url, true);
                xhrObj.send('');
            },
    
            injectScripts: function() {
                var len = LKZ.Script.queuedScripts.length;
                for(var i = 0; i < len; i++) {
                    var qScript = LKZ.Script.queuedScripts[i];
                    if(!qScript.done) {
                        if(!qScript.response) {
                            // STOP! need to wait for this response
                            break;
                        } else {
                            var se = document.createElement('script');
                            document.getElementsByTagName('head')[0].appendChild(se);
                            se.text = qScript.response;
                            if(qScript.onload) {
                                qScript.onload();
                            }
                            qScript.done = true;
                        }
                    }
                }
            },
    
            getXHRObject: function() {
                var xhrObj = false;
                try {
                    xhrObj = new XMLHttpRequest();
                } catch(e){
                    var aTypes = ["Msxml2.XMLHTTP.6.0",
                        "Msxml2.XMLHTTP.3.0",
                        "Msxml2.XMLHTTP",
                        "Microsoft.XMLHTTP"];
                    var len = aTypes.length;
                    for(var i=0; i < len; i++) {
                        try {
                            xhrObj = new ActiveXObject(aTypes[i]);
                        } catch(e) {
                            continue;
                        }
                        break;
                    }
                } finally {
                    return xhrObj;
                }
            }
        };
    
        LKZ.addHandler = function(elem, type, func) {
            if(elem.addEventListener) {
                elem.addEventListener(type, func, false);
            } else if(elem.attachEvent) {
                elem.attachEvent("on" + type, func);
            }
        };
    
    function init() {
            //Este es el callback
        }
    
    LKZ.Script.loadScripts(["jquery-172.js", "jquery-ui-1820.js", "functions.js"], init);
    

    Saludos desde Buenos Aires.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *