javascript eficiente

Más rápido

Menos memoria

Más ligero


http://goo.gl/E9VSs


@gruizdevilla

@adesis

Para optimizar pregúntate:

  • ¿Qué podrías hacer de forma más eficiente con tu código?
  • ¿Cuáles son las optimizaciones comunes de los motores de JavaScript?
  • ¿Qué es lo que el motor de JavaScript NO puede optimizar?
  • ¿Está el recolector de basura (GC) limpiando lo que esperas que limpie?

Hay que saber como funcionan los motores de javascript

  • V8 en Chrome
  • SpiderMonkey en Firefox
  • Chakra en Internet Explorer
  • Carakan en Opera


Hay que optimizar pensando en todos.
Pero analicemos el más documentado: V8

Analicemos el más documentado

Hay que entender los principales conceptos del motor.



Las piezas de V8

Las piezas fundamentales son las siguientes:
  • Base Compiler o compilador base
  • Object model o modelo de objetos
  • Runtime profiler
  • Optimizing Compiler o compilador optimizador
  • Desoptimizaciones
  • Garbage Collector (GC) o recolector de basura

Base compiler


  1. Parsea el código javascript
  2. Genera código nativo
    antes de ejecutarlo



El código inicial no está 
especialmente optimizado porque
el objetivo es compilarlo lo más rápido posible

Object Model


En JavaScript los objetos son arrays asociativos.

En V8 se representan internamente
con clases ocultas que aceleran
el acceso a propiedades.

Runtime Profiler


Monitoriza constantemente
la aplicación que se está ejecutando
identificando las "funciones calientes"

O sea, las que se ejecutan la mayor
parte del tiempo.

Optimizing compiler


Optimiza y recompila el código caliente
localizado por el Runtime Profiler.

Un ejemplo de optimización es el inlining:
reemplazar la llamada a una función
por su cuerpo

Desoptimizaciones

El compilador puede desechar optimizaciones
volviendo a la versión inicialmente compilada.

Esto ocurre cuando las hipótesis que se hicieron
para optimizar dejaron de ser válidas.

Garbage collector


IMPORTANTE,
MUY IMPORTANTE,
IMPORTANTÍSIMO 

ENTENDER CÓMO FUNCIONA

Garbage collector

Hablamos de gestión de memoria:
intenta recuperar y limpiar la memoria
ocupada por objetos que ya no se usan.

Mientras los objetos estén referenciados no se limpian.

¿Pero hay que estar pendiente de la gestión de memoria?

Habitualmente no.
No hace falta referenciar manualmente.
Pon las variables donde las necesites,
lo más local posible y ya está.

Errores habituales


La instrucción  delete  elimina una clave de un hashmap pero no fuerza la liberación de memoria.

var o = { x: 1 }; 
delete o.x; // true 
o.x; // undefined

Esto convierte a    o   en un objeto "lento".
Mejor usar null que delete.

NULL tampoco elimina objetos


var o = { x: 1 }; 
o = null;
o; // null
o.x // TypeError
Al asignar null, no eliminamos al objeto sino la referencia.

Solo si es la última referencia, el objeto podrá ser limpiado por el GC (cuando este lo decida).

Variables globales


Como poniendo

var myGlobalNamespace = {};

¡NUNCA SE LIMPIAN!

¡Solo se libera la memoria refrescando la página!

Regla de oro

Para que el Garbage Collector pueda recolectar el máximo número de objetos lo más pronto posible

NO REFERENCIES A OBJETOS
QUE YA NO NECESITES 

  • Pon las variables en el scope adecuado (el más
    interior posible)
  • Unbind de eventos que ya no se necesitan
    (¿desapareció el DOM del evento?)
  • ¿Cacheas datos locales?
    Controla el descacheo y elimina lo viejo

Funciones

Compara esto:
function foo() {
    var bar = new LargeObject();
    bar.someCall();
}
con esto:
function foo() {
    var bar = new LargeObject();
    bar.someCall();
    return bar;
}

// somewhere else
var b = foo();

Closures

Las función interna tiene acceso al scope exterior incluso después de que la función sea ejecutada:
function sum (x) {
    function sumIt(y) {
        return x + y;
    };
    return sumIt;
}

// Usage
var sumA = sum(4);
var sumB = sumA(3);
console.log(sumB); // Returns 7

CLOSURES II

Compara
var a = function () {
    var largeStr = new Array(1000000).join('x');
    return function () {
        return largeStr;
    };
}();
con esto
var a = function () {
    var smallStr = 'x';
    var largeStr = new Array(1000000).join('x');
    return function (n) {
        return smallStr;
    };
}();

TIMERS

Alguno del equipo
ya está escarmentado de esto ;)

Ojo con los setTimeout y setInterval





TIMERS

Mira atentamente este código
var myObj = {
    callMeMaybe: function () {
        var myRef = this;
        var val = setTimeout(function () { 
            console.log('Time is running out!'); 
            myRef.callMeMaybe();
        }, 1000);
    }
};
Ahora ejecuta
myObj.callMeMaybe();
Observa la consola y ejecuta después
myObj = null

Trampas que afectan
al rendimiento


Recuerda: nunca optimices hasta que lo necesites.

Mide el efecto de tus optimizaciones,
igual la mejora es imperceptible.

Ejemplo

Supongamos que hay que crear un módulo que:

  1. Utiliza una fuente local de datos
    con elementos con un id numérico
  2. Pinta una tabla con estos ids
  3. Añade eventos para alternar 
    una clase al hacer click
    en cualquier celda

Cuestiones


  • ¿Cómo almacenamos los datos?
  • ¿Cómo escribimos una tabla
    eficientemente y la adjuntamos al DOM?
  • ¿Cómo organizamos los eventos
    de una forma óptimo?


1ª Aproximación

  1. Guardamos los datos en un objeto
  2. Almacenamos los objetos en un array
  3. Con jQuery iteramos sobre los datos,
    pintando la tabla y la adjuntamos
    al DOM
  4. Finalmente, enganchamos los eventos
    para gestionar los estilos.


1ª Optimización

  • Si utilizamos solo los ids, en el array
    interesa solo guardar los ids
  • Usar DocumentFragment y metodos
    nativos de DOM es más rápido (jQuery
    utiliza internamente DocumentFragment)
  • Binding común de click, en lugar de a cada 
    td individual.


Midámos las mejoras: http://jsperf.com/first-pass

2ª Optimización

Se suele comentar en internet
que el patrón de prototype es mejor
que el patrón de módulo.

Y además, que hay super herramientas de templating


Y las mejoras de rendimiento resultan ser menos importantes:

Consejos para optimizar v8

  • Hay patrones de código que deshacen
    optimizaciones como el try-catch.
  • Procura que tus funciones sean
    monomórficas, es decir, que las variables
    siempre sean de la misma clase oculta.
    NO HAGAS ESTO:

function add(x, y) { 
   return x+y;
} 

add(1, 2); 
add('a','b'); 
add(my_custom_object, undefined);

Más consejos

  • No escribas funciones enormes,
    son más difíciles de optimizar
  • Cuidado con elementos no inicializados

a = new Array();
for (var b = 0; b < 10; b++) {
  a[0] |= b;  // Oh no!
}
//vs.
a = new Array();
a[0] = 0;
for (var b = 0; b < 10; b++) {
  a[0] |= b;  // Much better! 2x faster.
}

Para cuando tengas un rato


Google I/O 2012 -
Breaking the JavaScript Speed Limit with V8

Optimizing for V8 - A series

Objetos vs Arrays

  • Si almacenas un listado de números
    o de objetos del MISMO TIPO, usa un array
  • Si semánticamente necesitas un objeto con
    muchas propiedades, usa un objeto, será rápido
  • Las propiedades indexados por un número son
    mucho más rápidas en array u objeto que las
    propiedades normales
    http://jsperf.com/performance-of-array-vs-object/3
  • Las propiedades son más complicadas que
    los accesos a elementos de un array, y por tanto
    se pueden hacer menos optimizaciones.

Try Catch

El try...catch impide optimizar
el código que tiene dentro.
Además, si haces caso a Uncle Bob, una función
con try...catch no debe tener más responsabilidades.
function perf_sensitive() {
  // Resuelve aqui la funcionalidad sensible al rendimiento
}

try {
  perf_sensitive()
} catch (e) {
  // Gestiona la excepcion
}

Usando objetos

  • Crear objetos con una función de construcción 
    es más eficiente porque todos tienen la misma
    clase oculta
  • No hay límite al número de objetos diferentes 
    o a su complejidad, pero...
  • En los objetos calientes, intenta mantener
    las cadenas de prototipos (prototype chains)
    cortas y un bajo número de propiedades.

CLonando objetos

  • Cuidado con los bucles for...in, nunca serán
    rápidos
  • Lo más eficiente siempre será usar
    una función constructora (para usar en 
    código caliente):

function clone(original) {
  this.foo = original.foo;
  this.bar = original.bar;
}
var copy = new clone(original);

Funciones cacheadas

Para usar dentro del patrón de módulo.

USANDO ARRAYS

No borres elementos del array.

Internamente pasa a un modelo más lento.

Cuando las claves son escasas,  eventualmente
pasa a un modo hashmap mucho más lento.

Array literals

Dan una pista al compilador del tipo y tamaño
de los arrays.

// Aquí V8 ve que quieres un array de 4 elementos con números:
var a = [1, 2, 3, 4];

// No hagas esto:
a = []; // V8 no sabe nada del array
for(var i = 1; i <= 4; i++) {
     a.push(i);
}

Tipos simples o mezclados

No es buena idea mezclar distintos tipos 
en el mismo array:
 booleanos, enteros, undefined, textos, ...

Arrays dispersos o completos

Cuando el array no está completo, V8 empieza
a usar un diccionario que consume
menos recursos pero es más lento.

Arrays compactos o COn HUECOS

Evita los "agujeros" en un array. 
Con que solo haya un hueco ya será más lento.

Preasignar o crecer

Antes que reservar un espacio grande (por ej, 64k)
para un array,  suele ser mejor crecer a medida que se necesite (aunque depende del VM), porque al reservar
apenas ganamos tiempo.
// Empty array
var arr = [];
for (var i = 0; i < 1000000; i++) {
    arr[i] = i;
}

// Pre-allocated array
var arr = new Array(1000000);
for (var i = 0; i < 1000000; i++) {
    arr[i] = i;
}

OPTIMIZANDO TU APLICACIÓN

 La velocidad lo es todo 

Los tres pasos:
  1. Mide: encuentra los puntos
    lentos de tu aplicación (≈45%)
  2. Comprende: averigua cual es
    problema real (≈45%)
  3. Arregla. (≈10%)

Benchmark

Mide el rendimiento de tus aplicaciones.
Por ejemplo, comparando tiempos:

Ten en cuenta que esto no es 100% fiable.
Por ejemplo, el GC puede afectar las pruebas.

Más detalles sobre benchmarks:



Profiling

El perfilador de Chrome te permite localizar
las funciones que más tiempo consumen:

Profiling

Puedes medir uso de CPU, rendimiento 
de los selectores de CSS y el uso de memoria.
Para leer: Como hacer profiling (en inglés)

Memoria

Creencia habitual:
Más memoria == Mejor rendimiento
Realidad: 
más memoria implica más latencia y variabilidad

causas de memory leaks

Las causas más habituales suelen ser:
  • Event Listeners. 
  • Caches locales
  • Almacenamiento en objetos en arrays y hashes
    que nunca se limpian
  • Timers

evolución de memoria

Con la papelera puedes forzar el GC

Memory leaks

Nos apoyamos en las herramientas de Devs
Haz varias tareas, se fuerza el GC y mira el número de nodos DOM. Si no baja hay un problema.
Toca aplicar la técnica de los Tres SnapShots
 

Técnica 3 snapshots


¿Por qué funciona?

  • Cuando das a "Take SnapShot", fuerza al GC  y luego toma una foto
  • #1 representa una línea base
  • Los nodos amarillos en #2 son los nuevos añadidos por las acciones realizadas. 
  • Las líneas punteadas representan los objetos asignados por las acciones correctamente tratados por el GC antes del snapshot.
  • Hasta este punto, resulta dificil determinar si la memoria asignada es un leak o está intencionadamente dejada ahi (por ej, caches)
  • Por lo tanto, realizamos las mismas tareas tomando un tercer snapshot.
  • Tras el 3er snapshot, seleccionamos el  "Summary" del Snapshot 3 y filtramos para que
    muestre solo los objetos que han sido asignados entre el 1 y el 2. Ahora tenemos claros
     que los objetos alojados entre Snapshots 1 y 2 no deberían estar ahi, por lo que
    los objetosmostrados son potenciales leaks.

Localizando memory leaks

¿Qué hacer entonces?

  • Examina el camino que retiene a los objetos perdidos
  • Si no se puede inferir fácilmente las referencias (p. ej. event listeners): 
    • instrumenta el constructor de los objetos mediante la consola para identificar mejor el stack trace
    • ejecuta el escenario de nuevo
  • Arréglalo:
    • Desecha correctamente los objetos basura
    • unlisten() en event listeners

La foto final:

REFLOWS

Reflow: cuando un navegador tiene que
recalcular las posiciones y dimensiones
de un elemento en un documento para repintarlo

El reflow bloquea el navegador
frente a interacción del usuario
por lo que es importante entenderlo
 para mejorar el tiempo de reflow.

Reflows


Mejorando reflows

  • Batch the metodos que lanzan reflows o repintan
  • Mejorar procesar elementos fuera del DOM.
    Usando un DocumentFragment, por ejemplo.
  • Por ejemplo, ¿cual es el problema del siguiente
    código?
function addDivs(element) {
  var div;
  for (var i = 0; i < 20; i ++) {
    div = document.createElement('div');
    div.innerHTML = 'Heya!';
    element.appendChild(div);
  }
}

Evitamos el reflow

Utilizamos un DocumentFragment y luego adjuntamos
al DOM

function addDivs(element) {
  var div; 
  // Creates a new empty DocumentFragment.
  var fragment = document.createDocumentFragment();
  for (var i = 0; i < 20; i ++) {
    div = document.createElement('a');
    div.innerHTML = 'Heya!';
    fragment.appendChild(div);
  }
  element.appendChild(fragment);
}
    

CAUSAS DE REFLOW

Hay que minimizarlas en la medida de lo posible:

  • Redimensionar la pantalla
  • Cambiar la fuente
  • Agregar o quitar una hoja de estilos
  • Cambios en contenidos (como al escribir en un input) 
  • Activación de psudo clases CSS como :hover 
  • Manipulación del atributo class
  • Un script que manipule el DOM
  • Cálculos de medidas: offsetWidth y offsetHeight
  • Cambiar una propiedad del atributo style

CONCLUSION

Mide
Entiende
Arregla
Repite

Fuentes

javascript eficiente

By Gonzalo Ruiz de Villa

javascript eficiente

  • 42,612