Seleccionar página

Cuando uno busca información sobre cómo optimizar un código, lo que esperamos encontrar es una serie de sencillas recetas y con un impacto muy significativo en la velocidad de nuestro programa. Lamentablemente, la optimización es algo bastante complejo y pocas veces es algo tan sencillo como cambiar una instrucción por otra o utilizar un sencillo truco. Las optimizaciones dependen mucho del contexto de ejecución, del objetivo de nuestro programa, de las restricciones con las que nos encontremos, etc.

En general, la optimización no es un área que permita una aproximación teórica, ya que los modernos motores de Javascript realizan una optimización muy intensiva en la ejecución del código y aquello que a primera vista podría parecer un código más complejo y con mayor número de instrucciones, en la práctica puede resultar más rápido al ejecutarse.

Vamos a dar 12 pistas prácticas sobre técnicas de optimización que se pueden aplicar en Javacript. Para ello vamos a utilizar un caso práctico: la función equal() que venimos explicando en los últimos artículos y que permite comparar cualquier tipo de dato. Vamos a ver que optimizaciones realizamos desde el principio del desarrollo, que pruebas hemos realizado para conocer que instrucciones eran más o menos rápidas y cómo hemos mejorado su velocidad desde unas 135.000 ejecuciones por segundo hasta conseguir 320.000 ejecuciones por segundo (2,3 veces más rápido).

1. Definir el nivel de optimización que debemos obtener

Aunque pueda parecer un poco extraño, lo primero que tenemos que definir el nivel de rendimiento y optimización que debe tener nuestro código. Por ejemplo, en el caso de la función equal() es una función de bajo nivel y esperamos que pueda ejecutarse cientos de miles de veces por segundo sin ningún problema. Si estas funciones de bajo nivel no son muy eficientes, sobrecargan de forma muy significativa a los programas que las utilizan y, por lo tanto, deben ser lo más eficientes posible.

Otros programas que podamos crear no requieren de esta velocidad, ya que nunca se van a intentar ejecutar cientos de miles de veces por segundo, como mucho se ejecutan unas pocas veces por segundo. A todos nos gusta que todos nuestros códigos sean eficientes, pero sólo hay que gastar tiempo y recursos en optimizar los programas críticos o base de nuestros sistemas y no debemos tener una preocupación desmedida en el rendimiento. El equilibrio entre esfuerzo de desarrollo y rendimiento es importante.

También tenemos que mantener el necesario equilibrio entre la legibilidad del código y la optimización del mismo. Escribimos código para que sea interpretado por los ordenadores, pero también escribimos código para que pueda ser mantenido en el futuro por nosotros o por otros compañeros, por lo que debemos cuidar que el código que generemos sea comprensible.

2. Definir el contexto de ejecución

Para poder medir con efectividad las mejoras que vamos incorporando a nuestro programa debemos establecer un conjunto de entornos bien definidos donde vamos a poner a prueba el rendimiento de nuestro código. No es lo mismo si debe ejecutarse especialmente en entorno mobile o en desktop, sólo en el servidor, o en diferentes navegadores. Intentar hacer pruebas de rendimiento y optimizaciones para todas las versiones de todos los motores de Javascript no es viable en la práctica.

En el caso de equal() el objetivo y principales restricciones que nos hemos planteando, y que las tendremos que tener en cuenta a la hora de medir su rendimiento, son:

  • Debe aceptar cualquier tipo de dato u objeto válido en Javascript y devolver si los dos valores son equivalentes, es decir, ir más allá de una simple comparación con == para comprobar si los valores, propiedades y características de los dos valores pasados como parámetros son equivalentes.
  • Debe soportar todos los elementos de ES6, pero estar escrito en ES5.1, por lo que se debe poder ejecutar en cualquier entorno que soporte ES5.1.
  • No tiene ninguna dependencia y por lo tanto todo lo que necesite está contenido dentro de la función.
  • Debe funcionar tanto en navegadores como en Node.

Por lo tanto, para poder medir el rendimiento debemos hacer mediciones en los principales navegadores y en Node. No podemos conformarnos con hacer pruebas en un solo entorno, ya que esto nos puede dar resultados parciales. Tendremos que comprobar su funcionamiento en todos los entornos disponibles, pero no necesariamente su rendimiento. Aunque tenemos que soportar navegadores antiguos, no quiere decir que tengamos que dar un rendimiento excelente en estos navegadores, ya que su utilización es menor.

En este caso vamos a utilizar como referencia para el análisis de rendimiento los siguientes entornos:

  • Node 7.6.0
  • Chrome 56
  • Edge 14
  • Internet Explorer 11
  • Firefox 50

El código se ha comprobado que funciona en muchos más navegadores y versiones, pero es en estas en las que se ha realizado el proceso de análisis de rendimiento y optimización.

Optimización general

Durante el desarrollo hemos seguido una serie de prácticas de codificación que nos ayudan a que el rendimiento general del código sea lo más óptimo posible. Vamos a repasarlas.

3. Cuanto menos se haga, mejor

Puede parecer que este planteamiento es un poco absurdo y que nuestro programa ya hace lo que tiene que hacer y no hace nada más. Pero si analizamos nuestro código es seguro que podemos encontrar líneas que se han quedado ahí desde las versiones iniciales de nuestra programación, que hacen algún tipo de comprobación, envían algún mensaje a consola o hacen dos veces la misma cosa.

Esta es la más importante recomendación de todas. Las grandes optimizaciones vienen dadas por cambiar la estrategia de nuestros programas para hacer muchas menos cosas que las que hacíamos inicialmente. Es complicado mostrar ejemplos sencillos de este tipo de situaciones, pero estamos seguros que todos recordaremos algún programa donde hemos dejado este tipo de cosas superfluas o redundantes.

4. Lo que no varía, se comprueba una sola vez

Esta es una sencilla recomendación, pero en muchas ocasiones las prisas nos hacen dejar el código de tal forma que se repiten una y otra vez determinadas comprobaciones que podríamos hacer menos veces. Veamos a ver un ejemplo concreto, pero se puede aplicar a muchos otros casos.

En muchos programas estamos continuamente comprobando si una determinada característica está disponible en el motor Javascript donde se está ejecutando nuestro código. Por ejemplo, podemos comprobar si tiene soporte a los objetos Map con un código similar a este:

function myFunction() {
    if (typeof Map !== 'undefined') {
        // ...
    }
}

Este código es perfectamente válido y no tiene ningún problema, pero si lo vamos a ejecutar muchas veces una solución es definir una variable con el resultado de la comprobación a la carga del programa, en una clausura o un modelo de módulos para mantener variables dentro de un contexto privado a nuestro programa:

const MAP_SUPPORT = typeof Map !== 'undefined';

function myFunction() {
    "use strict";
    if (MAP_SUPPORT) {
        // ...
    }
}

Podemos aprovechar la carga del programa para hacer este tipo de comprobaciones generales de elementos que no cambian durante la ejecución y evitar, en la medida de lo posible hacer comprobaciones repetidas de este tipo de características.

5. Empezar por lo más frecuente y terminar en cuanto sea posible

Esta es una recomendación general muy importante, aunque no siempre se puede aplicar, pero debemos preguntarnos: ¿Cuál es el caso más habitual? ¿Cómo puedo hacer que el programa termine en cuanto tenga un resultado?

En el desarrollo de equal() se ha tenido especial cuidado en situar en primer lugar las comprobaciones que se han considerado más frecuentes, saliendo del programa en cuanto se tiene una confirmación válida sobre si los dos valores son iguales o no. De esta forma, el orden de las comprobaciones en nuestro caso ha sido:

  • Comparación estricta. Si los dos valores son iguales con === no tenemos nada más que comparar.
  • Comparación no estricta. Si se ha pasado el parámetro de comparación no estricta y los dos valores son iguales con == no tenemos nada más que comparar.
  • undefined. Si los dos elementos son de este tipo, son iguales.
  • NaN. Si los dos elementos son de este tipo, son iguales.
  • Objetos sobre tipos nativos. Se obtienen sus valores primitivos y se comparan de forma natural.
  • Comparación de objetos de forma recursiva. Se comparan todas las propiedades de los objetos.
  • Objetos RegExp y Error. Se tienen en cuenta sus características para poder compararlos.
  • Objetos Map y Set. Se comparan sus contenidos.
  • Objetos ArrayBuffer y DataView. Se compara su contenido binario.
  • Funciones. Comparación experimental sobre el código fuente.

Seguro que alguno pueda pensar que quizás el orden debería ser diferente, ya que es más común un caso que otro y que de haberlo resuelto antes hubiéramos obtenido una ejecución más optimizada. Nos hemos basado en nuestra experiencia, pero el contexto donde se va a ejecutar nuestra función pueden variar y que, por ejemplo, sea más importante comparar objetos ArrayBuffer y DataView antes que el objeto RegExp que no es muy común. El orden de los elementos es siempre un compromiso y afecta de forma muy significativa a la velocidad efectiva que obtendremos al hacer llamadas a un programa que gestiona varios casos.

De forma similar, algo que debemos recordar a la hora de situar los elementos en una expresión if, o cualquier otra sentencia de control, es que los elementos se evaluarán de izquierda a derecha y en cuanto uno de los elementos evaluados incumpla la condición que estamos estableciendo, la evaluación se detiene y el programa sigue en la cláusula else o al final de la sentencia de control.

Ajustar esta evaluación de izquierda a derecha es algo importante a la hora de obtener un código que se ejecute con rapidez. Debemos situar a la izquierda los elementos que más fácilmente van a cortar la ejecución o aquellos que nos aseguran que el resto de criterios de evaluación van a ejecutarse sin problemas. Debemos pensar siempre un par de veces si el orden de los elementos es el más adecuado o si se podría simplificar o mejorar.

Optimizando algunas instrucciones

Una vez que hemos dado un repaso a una recomendaciones generales, vamos a profundizar en algunos casos concretos que nos hemos encontrado en el desarrollo de equal() y que pueden servir a cualquier programador que se encuentre con situaciones similares.

Para comprobar la velocidad de dos piezas de código vamos a utilizar la librería benchmarkjs que desarrollamos hace algún tiempo y que se puede instalar desde npm.

Esta librería nos permite conocer el número de ejecuciones que se pueden realizar en un segundo. Por el orden de magnitud podemos clasificar la optimización cómo:

  • Una optimización que afecta instrucciones que se ejecutan millones de veces por segundo la denominaremos nano optimización y no va a afectar de forma significativa a la velocidad general de nuestro programa, aunque todo cuenta.
  • Una optimización que afecta a instrucciones que se ejecutan cientos de miles de veces por segundo la denominaremos micro optimización y si se presenta repetidas veces en nuestro programa si puede afectar a la velocidad general de nuestro programa
  • Una optimización que afecta a instrucciones que se ejecutan decenas de miles de veces por segundo o menos las denominaremos simplemente optimización y afecta de forma muy significativa a nuestro programa. Algunas de estas optimizaciones suelen estar relacionadas con el uso del DOM, el acceso a datos en un servidor o al manejo de sistemas de bases de datos.

Vamos a ver varios ejemplos.

6. Object.keys() vs. for in

Object.keys() es una función muy interesante que devuelve los nombres de todas las propiedades de un objeto. Parece una función inocente, pero es una función algo ineficiente en muchas implementaciones de Javascript, pudiendo hacer lo mismo que ella directamente por medio de un bucle for in y algunas instrucciones adicionales.

Estos son los dos casos que vamos a contrastar:

var bigArray = new Array(1000);

benchmarkjs('for in', function () {
    var result = [];
    for (var c in bigArray) {
        if (bigArray.hasOwnProperty(c)) {
            result.push(c);
        }
    }
    return result;
});

benchmarkjs('Object.keys', function () {
    return Object.keys(bigArray);
});

Aunque a primera vista diríamos que la función que utiliza for in tiene más líneas de código y por lo tanto es menos eficiente, lo cierto es que obtenemos estos resultados en los entornos de referencia.

Recordemos que no estamos comprobando la velocidad entre entornos, estamos comprobando la velocidad de un código frente a otro en diferentes entornos.

Como podemos observar, en Node, Chrome y Edge el resultado del código basado en for in es mucho mejor (se pueden ejecutar más operaciones por segundo). En caso de Chrome es especialmente llamativo, ya que pasamos de ejecutar 115.000 a más de 6.000.000 de operaciones por segundo. El resultado en IE es más o menos igual y sólo en el caso de FireFox es mucho mejor el resultado de Object.Keys().

Ahora tenemos que preguntarnos qué hacer. Si Firefox fuera nuestra plataforma de referencia, estaría claro, pero normalmente debemos mantener un adecuado equilibrio entre todas las plataformas, por lo que en nuestro caso hemos decidido cambiar Object.Keys() por un código basado en for in.

7. else y return vs. solo return

Una recomendación que vimos hace algún tiempo y que quisimos saber si realmente nos ayudaría a mejorar la velocidad de nuestro código es evitar concatenar return else return. Algo de este tipo

    return 1; 
} else {
    return 2;
}

En estos casos podemos eliminar el else. Para comprobar si evitar este tipo de estructura mejora el tiempo de ejecución hemos creado estos dos casos de prueba:

benchmarkjs('else and return', function () {
    var n = 0 | Math.random() * 100;
    if (n & 2) {
        return 1;
    } else {
        return 0;
    }
});
benchmarkjs('return', function () {
    var n = 0 | Math.random() * 100;
    if (n & 2) {
        return 1;
    }
    return 0;
});

En general la diferencia es pequeña, por ejemplo, en Node podemos ejecutar 127 millones de operaciones por segundo con else y return y 128 millones de operaciones por segundo sí sólo utilizamos return evitando el else.

Obtenemos un resultado diferente en el caso de Chrome. Hemos ejecutado la prueba varias veces, para evitar que alguna interferencia pueda falsear los resultados. Obtenemos resultados más o menos similares. Seguramente algún tipo de optimización se aplica y nos cambia el resultado.

En cualquier caso, la mejora que obtendremos será realmente pequeña, ya que estamos hablando de un código que es capaz de ejecutarse hasta cientos de millones de veces por segundo y la mejora que experimentará nuestro código será minúscula, pero, todo cuenta y el resultado es un código incluso más sencillo de leer, combinando una pequeña nano optimización con una mayor legibilidad del código.

8. comprobar y asignar vs. solo asignar

En nuestro caso no podemos utilizar los valores por defecto de los parámetros de ES6 y tenemos que trabajar con ES5.1, por lo que tenemos que comprobar si los parámetros opcionales vienen con valor y si no es así, asignar un valor por defecto. Existen varios modelos para hacer este tipo de asignación.

Un amable colaborador nos propuso en GitHub utilizar una estructura del tipo options || (options = {}); en vez del options = options || {}; que habíamos incluido en nuestra versión inicial.

Ya que nos poníamos a ver que opción era más rápida, incluimos otras opciones. Estas son las pruebas realizadas:

benchmarkjs('assign', function () {
    function test(options1) {
        options1 = options1 || {};
        return options1;
    }
    test();
    test();
    test({});
});
benchmarkjs('check || assign', function () {
    function test(options1) {
        options1 || (options1 = {});
        return options1;
    }
    test();
    test();
    test({});
});
benchmarkjs('if assign', function () {
    function test(options1) {
        if (!options1) {
            options1 = {};
        }
        return options1;
    }
    test();
    test();
    test({});
});
benchmarkjs('if (typeof) assign', function () {
    function test(options1) {
        if (typeof options1 !== 'object') {
            options1 = {};
        }
        return options1;
    }
    test();
    test();
    test({});
});

En el código de prueba hacemos dos llamadas sin parámetro de entrada y una llamada con parámetro. Esto afecta a la prueba. En nuestro caso consideramos que de media 2 de cada 3 llamadas se realizará sin parámetros opcionales, por lo que ese es el caso que más de debemos considerar a la hora de hacer las pruebas. Este tipo de decisiones condicionan nuestros resultados y es importante pararse a pensar antes de diseñar el modelo a probar.

En Firefox hemos obtenido un resultado extraño, ya que se ejecuta miles de millones de veces. Parece que su motor es capaz de identificar que estamos haciendo algo absurdo y simplemente no ejecuta el código haciendo una optimización radical. Por ello no podemos tenerla en cuenta.

En el resto de entornos la opción options || (options = {}); es la que mejor resultado da, pero es muy parecido al que obteníamos con la asignación directa, excepto en Edge e IE, que la segunda opción más rápida es utilizar if (!options) options = {};. Las mejoras son muy pequeñas, pero aceptamos de buen grado la aportación de wchiquito, aunque no vaya a hacer que nuestro programa mejore significativamente, pero no empeora la legibilidad y de esta forma gradecemos la contribución.

9. for vs. while

Un buen amigo nos recomendó utilizar while en vez de for parar recorrer las matrices. Nos advirtió que el cambio no era muy radical, pero que se notaba. Este ha sido un caso curioso de estudiar, ya que el resultado de las pruebas asiladas no ha coincidido con las pruebas con nuestro programa completo. Para comprobar la velocidad de una instrucción frente a otra utilizamos este código:

var nums = [1,2,3,4,5,6,7,8,9,10,
    11,12,13,14,15,16,17,18,19,20,
    21,22,23,24,25,26,27,28,29,30,
    31,32,33,34,35,36,37,38,39,40,
    41,42,43,44,45,46,47,48,49,50,
    51,52,53,54,55,56,57,58,59,60,
    61,62,63,64,65,66,67,68,69,70,
    71,72,73,74,75,76,77,78,79,80,
    81,82,83,84,85,86,87,88,89,90,
    91,92,93,94,95,96,97,98,99,100];

benchmarkjs('for', function () {
    var odd = 0;
    for (var n = 0; n < nums.length; n++) {
        if (nums[n] % 2 === 0) {
            odd++;
        }
    }
    return odd;
});

benchmarkjs('while', function () {
    var odd = 0;
    var n = nums.length;
    while(n--) {
        if (nums[n] % 2 === 0) {
            odd++;
        }
    }
    return odd;
});

Para que la prueba tenga un mínimo sentido tenemos que poner contenido dentro del bucle, ya que en el caso de poner un contenido vacío los motores se dan cuenta y realizan una optimización radical.

En todos los casos de prueba for es más rápido que while, excepto en Chrome que es prácticamente igual o un poco mejor for. Pero estas pruebas no han sido suficientes y decidimos probar cambiando estas instrucciones en el código de nuestra función.

Lo curioso es que este resultado no coincide con el que obtenemos con este mismo cambio en el código equal(), donde conseguimos entre un 7% y 8% de mejora de rendimiento utilizando while en vez de for. Un juego de pruebas como el que planteamos tiene poco código fuente y los optimizadores pueden aplicar técnicas más agresivas de mejora que cuando se encuentran códigos con algunos cientos de líneas y donde hay otros muchos más factores a tener en cuenta.

Lo que realmente importa es cómo se comporta una determinada instrucción en nuestro programa, y aunque en unos casos nos pueda ser más rápido ejecutar for, en nuestra función tiene mejor rendimiento while.

10. Propiedad vs. variable

Una recomendación relativamente antigua, y que ya no suele realmente efectiva, es evitar los “puntos”, es decir, evitar utilizar algo como objeto.propiedad. En general esto ya no es así y el acceso a las propiedades es muy rápido, no mostrando diferencias de velocidad. En nuestro caso sí hay diferencia, porqué hemos definido los valores de retorno en propiedades de la función desde donde los utilizamos y en este caso la optimización de los motores de Javascript no funciona tan bien como en otros casos.

var CERO = 0, UNO = 1, DOS = 1, TRES = 3, CUATRO = 4, CINCO = 5, 
    SEIS = 6, SIETE = 7 ,OCHO = 8, NUEVE = 9, DIEZ = 10;

function test1() {
    var n = 0 | Math.random() * 100;
    if (n === CERO) {          return CERO;
    } else if (n === UNO)    { return UNO;
    } else if (n === DOS)    { return DOS;
    } else if (n === TRES)   { return TRES;
    } else if (n === CUATRO) { return CUATRO;
    } else if (n === CINCO)  { return CINCO;
    } else if (n === SEIS)   { return SEIS;
    } else if (n === SIETE)  { return SIETE;
    } else if (n === OCHO)   { return OCHO;
    } else if (n === NUEVE)  { return NUEVE;
    } else if (n === DIEZ )  { return DIEZ; 
	}
}

function test2 () {
    var n = 0 | Math.random() * 100;
    if (n === test2.CERO)          { return test2.CERO;
    } else if (n === test2.UNO)    { return test2.UNO;
    } else if (n === test2.DOS)    { return test2.DOS;
    } else if (n === test2.TRES)   { return test2.TRES;
    } else if (n === test2.CUATRO) { return test2.CUATRO;
    } else if (n === test2.CINCO)  { return test2.CINCO;
    } else if (n === test2.SEIS)   { return test2.SEIS;
    } else if (n === test2.SIETE)  { return test2.SIETE;
    } else if (n === test2.OCHO)   { return test2.OCHO;
    } else if (n === test2.NUEVE)  { return test2.NUEVE;
    } else if (n === test2.DIEZ )  { return test2.DIEZ;
    }
}
test2.CERO = 0;
test2.UNO = 1;
test2.DOS = 1;
test2.TRES = 3;
test2.CUATRO = 4;
test2.CINCO = 5;
test2.SEIS = 6;
test2.SIETE = 7;
test2.OCHO = 8;
test2.NUEVE = 9;
test2.DIEZ = 10;

benchmarkjs('variable', test1);
benchmarkjs('property', test2);

El caso de prueba está bastante forzado, pero sirve para comprobar las diferencias de velocidad en un caso y en otro. En Node, Chrome y Edge es algo más rápido el uso de variables, en IE y Firefox es prácticamente igual, con una pequeña ventaja accediendo a las propiedades. Podemos considerar que en general el uso de variables es un poco más rápido en este caso, estamos hablando decenas de millones de ejecuciones por segundo, por lo que es una nano optimización y nuestro programa no va a mejorar mucho por este cambio.

11. usar o no usar sort()

Este caso es bastante interesante. En un primer momento nuestro programa no hacía uso de sort() para ordenar matrices, pero descubrimos un problema a la hora de comparar los nombres de las propiedades de los objetos, ya que el orden en el que se obtienen las propiedades depende del orden en el que se han creado. La solución es aplicar un sort() que orden los resultados y de esta forma evitar que el orden de creación de las propiedades afectase al comportamiento de nuestro programa.

Cualquier que haya implementado una ordenación sabe que es un proceso bastante complejo y que afecta de forma significativa al rendimiento. Se deben realizar un buen número de comparaciones y reordenar los elementos de posición, por lo que hacer ordenaciones podemos considerarlo como un proceso razonablemente lento.
Nuestra primera aproximación para optimizar nuestro código fue hacer sólo la ordenación con sort() cuando el resultado de comparar las propiedades sin ordenar de dos objetos fallase. Sólo en ese caso se ordenaba y se volvía a comparar. Esta solución es ingeniosa, ya que sólo aplica una operación costosa cuando la operación directa ha fallado, pero depende del tipo de datos que recibamos. En la práctica obtenemos una mejora sustancial del rendimiento, ya que la mayoría de los objetos han definido sus propiedades en el mismo orden y la llamada a sort() se realizaba en pocos casos, pero en otras ocasiones podríamos obtener una pérdida de rendimiento, ya que si todos los datos vienen desordenados, hacemos dos comparaciones en vez de una.

La solución definitiva consiste el cambiar completamente la aproximación al problema y no necesitar tener ordenadas las propiedades para hacer una comparación completa y consistente. Para ello se comparan primero los tamaños de las dos matrices y si son iguales recorre una matriz y se buscan los valores en la otra matriz.

Estos son los tres casos de prueba que vamos a utilizar:

var aKeys1 = ['uno', 'dos', 'tres', 'cuatro' ];
var aKeys2 = ['uno', 'dos', 'tres', 'cuatro' ];
var aKeys3 = ['uno', 'dos', 'cuatro', 'tres' ];
var aKeys4 = ['uno', 'dos', 'cuatro', 'tres', 'cinco' ];

benchmarkjs('sort always', function () {
    function e(a, b) {
        return a.sort().join() === b.sort().join();
    }
    var equal_1_2 = e(aKeys1, aKeys2);
    var equal_1_3 = e(aKeys1, aKeys3);
    var equal_3_4 = e(aKeys3, aKeys4);
});

benchmarkjs('sort only in some cases', function () {
    function e(a, b) {
        var result = a.join() === b.join();
        if (!result) {
            return a.sort().join() === b.sort().join();
        }
        return result;
    }
    var equal_1_2 = e(aKeys1, aKeys2);
    var equal_1_3 = e(aKeys1, aKeys3);
    var equal_3_4 = e(aKeys3, aKeys4);
});
benchmarkjs('no sort', function () {
    function e(a, b) {
        if (a.length === b.length) {
            for (var i = 0; i < a.length; i++) {
                if (b.indexOf(a[i]) === -1) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }
    var equal_1_2 = e(aKeys1, aKeys2);
    var equal_1_3 = e(aKeys1, aKeys3);
    var equal_3_4 = e(aKeys3, aKeys4);
});

En todos los casos el uso de sort() es mucho peor que las alternativas que evitan completamente su utilización. La diferencia es enorme, en algunos casos pasamos de 300.000 operaciones por segundo hasta casi 9.000.000. Esta es una optimización muy significativa y nuestro código va a mejorar mucho con ella.

La versión que sólo utiliza sort() en algunos casos realmente no consigue una buena optimización y dependerá de los datos que utilicemos, ya que puede llegar a dar peor resultado si todos los datos vienen desordenados. Esta es una estrategia que podemos utilizar en algunas ocasiones, pero siempre con cuidado.

12. forEach() vs. for

Nuestra programación solemos evitar el uso de forEach(), que aunque es un código muy elegante y fácil de mantener, ya sabíamos que forEach() no es muy rápido. Aunque no solemos utilizarlo, para este artículo hemos querido mostrar cual es la diferencia de velocidad que podemos llegar a obtener. Para ello hemos utilizado este juego de pruebas:

var nums = [1,2,3,4,5,6,7,8,9,10,
    11,12,13,14,15,16,17,18,19,20,
    21,22,23,24,25,26,27,28,29,30,
    31,32,33,34,35,36,37,38,39,40,
    41,42,43,44,45,46,47,48,49,50,
    51,52,53,54,55,56,57,58,59,60,
    61,62,63,64,65,66,67,68,69,70,
    71,72,73,74,75,76,77,78,79,80,
    81,82,83,84,85,86,87,88,89,90,
    91,92,93,94,95,96,97,98,99,100];

benchmarkjs('for', function () {
    var result = [];
    for (var n = 0; n < nums.length; n++) {
        if (nums[n] % 2) {
            result.push(nums[n]);
        }
    }
    return result;
});

benchmarkjs('forEach', function () {
    var result = [];
    nums.forEach(function (a) {
        if (a % 2) {
            result.push(a);
        }
    });
    return result;
});

El resultado es bastante contundente. Aunque en Firefox el rendimiento de forEach() es un poco mejor, en todos los casos for es mucho más eficiente que su versión funcional forEach(). No queremos despreciar aquí las virtudes de forEach(), pero claramente entre ellas no se encuentra el rendimiento.

Cada día más programadores desprecian for a favor de modelos funcionales como forEach(), pero si en nuestro caso es más importante el rendimiento que el estilo de programación, tendremos que reconsiderar el uso de for como solución de mayor rendimiento. Vamos a ver otro caso a continuación.

13. firter() vs. for

Para filtrar el contenido de una matriz disponemos de la función filter(). Podemos suponer que filter() es una función optimizada y que si intentamos hacer nosotros por nuestra cuenta el filtrado de una matriz deberíamos obtener un peor resultado. Vamos a comprobarlo con este sencillo código:

var nums = [1,2,3,4,5,6,7,8,9,10,
    11,12,13,14,15,16,17,18,19,20,
    21,22,23,24,25,26,27,28,29,30,
    31,32,33,34,35,36,37,38,39,40,
    41,42,43,44,45,46,47,48,49,50,
    51,52,53,54,55,56,57,58,59,60,
    61,62,63,64,65,66,67,68,69,70,
    71,72,73,74,75,76,77,78,79,80,
    81,82,83,84,85,86,87,88,89,90,
    91,92,93,94,95,96,97,98,99,100];

benchmarkjs('for', function () {
    var result = [];
    for (var n = 0; n < nums.length; n++) {
        if (nums[n] % 2) {
            result.push(nums[n]);
        }
    }
    return result;
});

benchmarkjs('filter', function () {
    return nums.filter(function(n) { return n % 2; });
});

Podríamos considerar que utilizar 8 líneas de código con dos variables adicionales, un bucle y una comprobación no pueden ser más rápidos que lo que se puede hacer en una sola línea de código gracias a filter(), pero nuevamente for es mucho más eficiente para fitrar que su versión funcional filter().

Aunque requiera muchas más líneas de código, es mucho más rápido utilizar bucles para gestionar matrices que las funciones que nos ofrece el lenguaje como forEach(), filter(), map(), reduce(), etc. Desconocemos la razón de esta diferencia de rendimiento.

Por si alguno tenía alguna duda, hemos hecho pruebas con funciones flecha en vez de funciones tradicionales, y el resultado no mejora, incluso llega a dar peor resultado en algunos casos.

14. Comprobar cómo hemos mejorado

Para confirmar si realmente hemos mejorado el rendimiento de nuestro programa, hemos aprovechado el juego de pruebas funcionales que hemos desarrollado y que incluye 900 casos diferentes. Como este juego de pruebas hemos comparado el rendimiento de diferentes versiones de nuestro código. De esta forma podemos comprobar todos los casos, desde los más sencillos, hasta los más complejos, y ver si hemos ido mejorado o empeorado con las diferentes versiones.

Aunque en la gráfica es difícil observar las pequeñas variaciones, se puede ver un punto de inflexión entre la v7 y la v8. Este cambio corresponde a dos medidas unidas: la eliminación de sort() y de filter(), dos instrucciones pesadas y cuya eliminación ha cambiado de forma muy significativa la velocidad de respuesta. Prácticamente se duplica el número de operaciones por segundo.

Aunque quizás cueste verlo, aunque hemos intentado siempre mejorar un poco el rendimiento, pero en la práctica no siempre lo hemos conseguido. En algunas versiones hemos tenido que añadir código para corregir errores que han surgido y que han ampliado las operaciones que realiza el programa, lo que ha supuesto una pérdida de rendimiento. En general hemos recuperado esta pérdida de velocidad con alguna micro optimización adicional en versiones posteriores, pero no siempre hemos conseguido mejorar.

Conclusiones

Optimizar no es tarea sencilla. Aquí sólo hemos podido recoger algunos casos concretos en la confianza de que puedan servir a otros programadores en sus proyectos, pero cada código es un mundo, por lo que no podemos asegurar que lo que nos haya funcionado a nosotros funcione en todos los casos.

Al presentaros un caso práctico también hemos intentado mostrar las técnicas que se pueden aplicar para optimizar un código desde su versión inicial hasta otra con mayor rendimiento, sin por ello hacer que sea menos legible y mantenible.

Con este artículo ya casi hemos acabado la serie sobre el desarrollo de la función equal(). En la próxima entrega mostraremos cómo publicar el código en GitHub y en npm. Puedes descargar la versión final de esqueal.js con npm.

Novedades

JSDayES en directo

Sigue en directo las charlas y talleres del impresionante JSDayES 2017. Ponentes de primer orden, tanto nacionales como internacionales, nos mostrarán todos los aspectos de Javascript. El viernes 12 de mayo de 16:00 a 21:00 y el sábado 13 de mayo de 9:30 a 21:00 (hora de Madrid).

The CITGM Diaries by Myles Borins [video]

Myles Borins de Google, miembro del CTC de Node.js (Node.js Core Technical Committee), nos cuenta (en inglés, por supuesto) como funciona CITGM (Canary In The Gold Mine), una herramienta que permite obtener un módulo de NPM y probarlo usando una versión específica de NodeJS.

Debate: Tecnologías de Front Web [vídeo]

Desde las principales comunidades de desarrollo de tecnologías de front (Madrid JS, Polymer Madrid, Angular Madrid y VueJS Madrid) se ha organizado este debate que pretende ser un ejercicio de sentido común en relación a las tecnologías de front actuales centradas en componentes.