Referencias circulares en JavaScript y las fugas de memoria

23 May 2011

Introducción

En la pasada conferencia que impartimos en el Espacio Camon Madrid, mi compañero @eamodeorubio hizo referencia a un tema que se pasa por alto muy a menudo cuando tratamos Javascript a alto nivel: las fugas de memoria en general y lo fácil que es incurrir en ellas cuando tratamos aspectos más o menos triviales como lo puede ser la manipulación del DOM.

El caso más típico de desoptimización lo encontramos en eso que llamamos referencias circulares y que es, a su vez, uno de los aspectos más importantes a los que atacar durante una refactorización.

El DOM y las clausuras

Por lo general, una aplicación Javascript dirigida a un entorno navegador, interactuará con el DOM en algún momento. Puede ser añadiendo elementos, modificándolos o, más frecuentemente, escuchando eventos a los que responderá con alguna acción determinada (este último caso es posiblemente la base de la mayoría de proyectos bajo jQuery).

Además de lo anterior, otro de los aspectos de los más recurrentes es la creación de clausuras para encerrar todos nuestros fragmentos de funcionalidad y tenerlos así disponibles para su reutilización. La forma más básica de crear estos contextos es mediante funciones:

function myClousure(a, b){
  // Code goes here...
}

Esto es algo que repetimos varias veces a lo largo de una aplicación pero nunca tenemos que olvidar que una clausura (por ejemplo la función anterior), crea y mantiene un puntero hacia su propio contexto. Esto quiere decir que, si asociamos un elemento del DOM a una clausura, se crea una referencia circular y, con ella, una fuga de memoria.

Tomemos por ejemplo, el siguiente código:

function init( element, foo, bar ){
  element.onclick = function(){
    // work with foo and bar
  }
}

En este ejemplo, la función mantiene una referencia constante a element, foo y bar. Y puede darse el caso de que no lleguemos nunca a usar element porque, por ejemplo, no disparamos el evento onclick. Como el elemento guarda a su vez una referencia a su propio contexto, tenemos así una referencia cíclica susceptible de provocar problemas.

La recogida de basura en Javascript

Todos los intérpretes de Javascript integran un esquema de gestión de memoria independiente que preveen este tipo de comportamientos y optimizan sus recursos en tiempo de ejecución. Para aquellos casos en los que existen objetos de un solo uso o desechables, Javascript recurre a un sistema denominado ‘recolector de basura’ (garbage recolector). Sin embargo, un objeto DOM es un compenente especial de tipo COM y se gestiona, por tanto, de un modo diferente a los nativos.

Aunque el sistema puede manejar ambos sistemas por separados, cuando se producen referencias cruzadas entre ellos puede darse el caso de que una de las partes no encuentre a la otra si ésta ha sido purgada por el recolector. El resultado de esto es la temible fuga y la inestabilidad general del programa.

Evitando las referencias circulares de tipo DOM

Para evitar caer en este problema, la guía de estilo Javascript publicada por Google, recomienda no asociar a los eventos funciones anónimas sino que en su lugar, aconseja crear funciones nominales con los mismos argumentos requeridos:

function init( element, foo, bar ){
  element.onclick = callback( foo, bar );
}
 
function callback( foo, bar ){
  return function(){
    // work with foo and bar
  }
}

De este modo, el element no apunta directamente a un contexto y viceversa, lo que evita directamente el problema.

Un modo alternativo de evitar que el sistema continúe manteniendo en memoria las referencias es anular el valor del elemento DOM una vez declarado el callback:

function init( element, foo, bar ){
  element.onclick = function(){
    // work with foo and bar
  }
  element = null;
}

A diferencia del caso anterior, aquí si que se llega a dar la referencia; sin embargo, al vaciar su contenido inmediatamente después, se elimina el puntero y con ello el riesgo. Es una solución menos elegante (y que seguramente requerirá de algún comentario por parte del desarrollador) pero igual de válida.

Conclusión

El problema de las fugas de memoria era bien conocido en Internet Explorer al menos hasta su versión 7: su recolector de basura no era capaz de gestionar correctamente los recursos cuando existían referencias circulares. Las versiones posteriores han ido paliando el problema sin llegar a la solución perfecta. Mozilla también parecía, a raíz de sus recomendaciones, tener ciertos problemas en este aspecto; sin embargo, hay muy poca información al respecto. Por su parte, el V8 de Chrome si parece implementar una política inteligente (quizá una de sus mejores bazas) en cuanto a la gestión de recursos.

Como comentamos una y otra vez, dado que las aplicaciones Javascript exigen en su gran mayoría de una compatibilidad y máximo rendimiento en entorno multinavegador, no podemos confiar excesivamente en las bondades y perfecciones del intérprete en según qué escenarios.

Las reglas que hemos visto anteriormente pueden ser de gran ayuda para tratar de aumentar tanto el rendimiento como la estabilidad de nuestras aplicaciones Javascript siendo especialmente interesantes en aquellas que hacen un uso muy fuerte del DOM y de sus eventos.

Más:

{14} Comentarios.

  1. Diego

    Bueno, lo primero disculpa mi ignorancia porque debe ser algo básico que no entiendo pero ¿no se ejecutaría directamente la función “callback” en el ejemplo que pones de funciones nominales en vez de hacerlo en el evento onclick? Supongo que no y que tiene algo que ver con el return function… pero no lo pillo…

    Muchas gracias.

    • Carlos Benítez

      Hola;
      es cierto que el ejemplo puede parecer algo confuso, pero leído con cuidado no lo es tanto: la función callback en el ejemplo ‘nominal’ devuelve a su vez una función anónima con aquello que queremos ejecutar. Así, cuando es llamada desde el ‘onclick’, se crea esa función para ejecutarse inmediatamente pero desligada completamente del contexto del elemento. Esa es la diferencia clave con respecto al modo ‘tradicional’.

      Dicho de otra forma, las clausuras se hacen independientes y no existe referencia circular alguna.

      Además de esto, evitamos el problema de la fuga de memoria y facilitamos el trabajo al recolector de basura que no tiene mayor dificultad en purgar funciones de este tipo si el sistema lo precisa.

      Saludos!

  2. Diego

    Vaya, así dicho queda muy claro. Si lo he entendido bien nada más crear la función callback en init() se llama a la misma y esta devuelve una función que se queda como función anónima para el elemento.onclick, pero sin tener una referencia directa al mismo.

  3. Diego

    Lo que aprende uno por aquí… ¡Gracias! 🙂

  4. Madman

    ¡Qué interesante! ni me habría imaginado que ocurría esto…

    La solución que propones está genialmente pensada pero haría el código más voluminoso ¿no?, pero lo que se gana es mucho, eso está claro.

    Comentas que con la solución poco elegante (igualar a ‘null’ la referencia del DOM), el desarrollador debería comentar por qué ha hecho eso, pero… ¿también debería hacerlo con la otra solución, no? Al menos para mi, a priori, si viera las dos cosas me quedaría un poco perplejo, igualmente no sabría el por qué de una u otra solución…

    Gracias, una vez más, por compartir todo esto 🙂

    • Carlos Benítez

      Si; seguramente también sería interesante que el desarrollador comentara el porqué de la opción ‘buena’.

      Sin embargo, como es una solución estructural, los que vienen después suelen dar por hecho que responde a alguna necesidad y no se lían a mover funciones de un lado a otro. No pasa lo mismo cuando encontramos instrucciones sueltas que a simple vista no tienen utilidad; algún programador puede pensar que es parte de un código antiguo o inacabado y, como en tiempo de ejecución no se nota cambio alguno si se comenta, pues es fácil que se pierda tras la primera revisión…

      Saludos!

  5. yeikos

    Por lo que veo jQuery ya tiene implementada esta solución.

    Se agradece el artículo =)

  6. leifsk8er

    En JQUERY esto sigue siendo necesario? Por ejemplo en la version 1.7
    Se me ocurre lo siguiente:

    //Si bindeas un evento para que haga algo
    function hacemosAlgo(elemento, valor1){
    elemento.click(function(){
    var html = ‘Accion’;
    $(“#listaDeCosas”).append(html).find(“.accion:last”).click(function(){
    if(valor1 == ‘hola’){
    alert(“Bienvenido”);
    }else{
    alert(“adios”);
    }
    });
    });
    }
    hacemosAlgo($(“#cosa”), ‘hola’);

    Con esto caeriamos en la trampa del memory leak?

    • Carlos Benítez

      No; jQuery gracias a su propia arquitectura trata de ir evitando las referencias cíclicas. Como siempre, recomiendo echarle un ojo al código fuente de esta biblioteca para ver cómo encaran algunos de los problemas más frecuentes en Javascript.

      No obstante, en tu ejemplo, recomendaría no utilizar ‘click’ sino ‘on’ (por temas de rendimiento, scopes y bubbling) además de hacer una llamada a una función independiente en lugar de una anónima ‘inline’ para el callback. Estas ‘buenas práctivas’ consiguen dar a los códigos algo más de claridad y permiten una cierta modularidad que más adelante puedes ser reutilizada.

      Digamos que el artículo anterior muestra prácticas a evitar cuando construimos nuestro propio framework o cuando utilizamos funciones nativas del lenguaje sin prestar atención a estas peculiaridades.

      Saludos!

  7. leifsk8er

    Gracias, pero tengo la duda de que en la función anónima inline que he declarado como ejemplo realmente la variable “elemento” no la utilizo dentro, y si formara parte de un código mucho más extenso, habrían muchas más variables que no usaria justo en ese código, y en cambio de la forma que yo he puesto si que puedo acceder a ellas, y supongo entonces que con el consiguiente consumo de memoria sin sentido. En cambio en la forma que tu explicas (que es la forma correcta) no se podria acceeder, con lo que pienso que seria lo correcto, no utilizando asi esa referencia sin sentido no? No acabo de entender todo esto y no se si estoy diciendo locuras :P.

    Y en caso de que se cumpla lo que yo digo tampoco se si es que da igual que lo hagamos asi en jquery por que el optimiza bien estos aspectos en cuanto a la memoria.

    Un saludo!

  8. jaiem

    muchas gracias se nota que eres pro en javascript 😉

  9. oscar

    ¿La frase no debería ser : “[…] crea y mantiene un puntero hacia su propio contexto”.?

    • Carlos Benítez

      Si; queda más claro expresado de ese modo. Corregido!

      Saludos!

Deja un comentario

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