Seleccionar página

letLa implementación nativa de ES6 en Node 4.x empieza a ser significativa y nos hemos planteado comprobar el efecto que tiene en el rendimiento el uso de funcionalidades de ES6 con respecto a un código equivalente en ES5. Los resultados se presentaron en la Mesa Redonda: Rendimiento en EcmaScript 6. Aquí se recogen las pruebas realizadas y los resultados que han obtenido.

Para poder ejecutar estas pruebas necesitamos instalar el paquete BenchmarkJS.

Para instalarlo simplemente tenemos que utilizar npm con la siguiente instrucción:

npm install benchmarkjs

Con este paquete y Node 4.x instalado en nuestra máquina podemos empezar a probar el rendimiento. Los resultados que vamos a mostrar se han obtenido con Node 4.2.1 y pueden diferir de los obtenidos en otras versiones.

Uso de var, let, const

Esta es una de las novedades más sencillas de ES6, pero no por ello menos interesante. Permite definir constantes, que como su nombre indica su valor no puede ser cambiado, y variables con let que tienen un alcance limitado a conjunto de { } en el que ha sido declarado. ¿Afecta su uso al rendimiento? Vamos a comprobarlo:

"use strict";
var benchmark = require('benchmarkjs');
benchmark('var', function() {
    var loops = 1000;
    var NUM = 7;
    var result = 0;
    for (var i = 0; i < loops; i++) {
        result += i * NUM;
    }
});
benchmark('const', function() {
    const loops = 1000;
    const NUM = 7;
    var result = 0;
    for (var i = 0; i < loops; i++) {
        result += i * NUM;
    }
});
benchmark('let', function() {
    const loops = 1000;
    const NUM = 7;
    let result = 0;
    for (let i = 0; i < loops; i++) {
        result += i * NUM;
    }
});
console.log(benchmark.results);

left-const-var

El resultado es muy llamativo: el uso de let produce un efecto especialmente negativo. Si observamos los resultados vemos que el uso de let produce que la función no esté optimizada por v8 y sea extremadamente lento. Por su parte const no tiene un rendimiento muy diferente a var, por lo que comprobamos que su uso no penaliza el rendimiento.

A fin de comprobar estos resultados hemos realizado una segunda prueba entre var y let, ahora manteniendo let sólo dentro de la definición del bucle for.

"use strict";
var benchmark = require('benchmarkjs');
benchmark('var', function() {
    var loops = 1000;
    var NUM = 7;
    var result = 0;
    for (var i = 0; i < loops; i++) {
        result += i * NUM;
    }
});
benchmark('let', function() {
    var loops = 1000;
    var NUM = 7;
    var result = 0;
    for (let i = 0; i < loops; i++) {
        result += i * NUM;
    }
});
console.log(benchmark.results);

let-var

El resultado es un poco mejor que la prueba anterior y let no tiene un rendimiento tan malo, pero aun así el rendimiento es malo.

La conclusión es sencilla, el uso de let tiene una penalización importante en el rendimiento y deberíamos pensarlo bien antes de utilizar esta nueva funcionalidad.

Template string

ES6 permite realizar la interpolación de variables dentro de cadenas por medio de las template string. Vamos a ver cómo afecta al rendimiento su utilización:

"use strict";
var benchmark = require('benchmarkjs');
var loremIpsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit." +
  "Vivamus semper enim leo, id pharetra mauris convallis nec." +
  "Morbi iaculis ipsum sed quam condimentum, at rutrum massa ultrices." +
  "Integer hendrerit risus turpis, ac luctus massa ultrices quis." +
  "Sed a eros neque. Duis orci arcu, lobortis vel neque quis, lobortis feugiat felis." +
  "Nunc sed nibh condimentum, accumsan urna quis, congue justo." +
  "In iaculis odio et magna convallis, et rutrum nulla imperdiet." +
  "Nullam varius sed lorem id placerat.";

benchmark('+ concat', function() {
    var result1 = '';
    result1 = '1'+loremIpsum+'2'+loremIpsum+'3'+loremIpsum+'4'+loremIpsum+'\n';
});
benchmark('template string concat', function() {
    var result3 = '';
    result3 = `1${loremIpsum}2${loremIpsum}3${loremIpsum}4${loremIpsum}\n`;
});
console.log(benchmark.results);

template-string-1

El resultado es muy sorprendente, tanto que hace sospechar que algo ha pasado, ya que la función que concatena las cadenas con + es capaz de realizar más de mil millones de operaciones. Analizando un poco más vemos que el uso de una cadena fija hace que el optimizador haga un trabajo muy intensivo.

Por eso hemos construido otro modelo más complejo donde la cadena varía entre cada llamada al construirse de forma aleatoria y de esta forma evitar que la optimización produzca resultados tan diferentes. Este es el código de esta nueva prueba:

"use strict";
var benchmark = require('benchmarkjs');
function loremIpsum() {
  var text = [],
      r = 0,
      result = '';
  text[0] = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
  text[1] = "Vivamus semper enim leo, id pharetra mauris convallis nec.";
  text[2] = "Morbi iaculis ipsum sed quam condimentum, at rutrum massa ultrices.";
  text[3] = "Integer hendrerit risus turpis, ac luctus massa ultrices quis.";
  text[4] = "Sed a eros neque. Duis orci arcu, lobortis vel neque quis, lobortis " +
            "feugiat felis.";
  text[5] = "Nunc sed nibh condimentum, accumsan urna quis, congue justo.";
  text[6] = "In iaculis odio et magna convallis, et rutrum nulla imperdiet.";
  text[7] = "Nullam varius sed lorem id placerat.";
  for (var i = 7; i < 0; i--) {
    r = Math.floor(i*Math.random());
    result += text[r];
    text.splice(r, 1);
  }
  return result;
}
benchmark('+ concat', function() {
    var result1 = '';
    result1 = '1'+loremIpsum()+'2'+loremIpsum()+'3'+loremIpsum()+'4'+loremIpsum()+'\n';
});
benchmark('template string concat', function() {
    var result3 = '';
    result3 = `1${loremIpsum()}2${loremIpsum()}3${loremIpsum()}4${loremIpsum()}\n`;
});
console.log(benchmark.results);

template-string-2
Este resultado es más coherente, pero revisado con detenimiento vemos que lo que ha pasado es que ahora el tiempo de ejecución se ha trasladado a la función que genera el código aleatorio. A pesar de ello, el uso de template string produce que el optimizador no pueda realizar su trabajo de forma eficiente y por lo tanto conlleva una pérdida de rendimiento bastante significativa en la mayoría de los casos.

Funciones vs. Clases

ES6 permite definir clases como tales y no sólo funciones que pueden ser instanciadas como en ES5. Las funcionalidades que ofrece ES6 en el manejo de clases son bastante completas, pero para realizar una primera aproximación al rendimiento hemos seleccionado una sencilla instanciación de un objeto desde una función y desde una clase a fin de detectar si hay una diferencia de rendimiento significativa.

"use strict";
var benchmark = require('benchmarkjs');
class Polygon1 {
    constructor(height, width) {
        this.height = height;
        this.width = width;
    }
}
benchmark('class', function() {
    var h = Math.random();
    var w = Math.random();
    var t = new Polygon1(h, w);
});
function Polygon2(height, width) {
    this.height = height;
    this.width = width;
}
benchmark('function', function() {
    var h = Math.random();
    var w = Math.random();
    var t = new Polygon2(h, w);
});
console.log(benchmark.results);

function-class

Las diferencias de rendimiento son mínimas (un poco menos del 2% de diferencia). El uso de clases de ES6, por lo tanto, no tiene efectos negativos en cuanto al rendimiento y puede tener algunas ventajas al ofrecer algunos mecanismos y plasticidad mayor que el uso de funciones como haríamos utilizando ES5.

Object literals

ES6 permite una sencilla y abreviada forma de nombrar las propiedades de un objeto simplemente poniendo el nombre de la variable. El objeto tomará como nombre de la propiedad el nombre de la variable y como valor el valor de la variable. Vamos a comprobar si tiene algún efecto secundario.

"use strict";
var benchmark = require('benchmarkjs');
benchmark('object literal ES6', function () {
    var random = (0 | Math.random() * 1000);
    var today = Date.now();
    var o = {
        random,
        today
    };
});
benchmark('object literal ES5', function () {
    var random = (0 | Math.random() * 1000);
    var today = Date.now();
    var o = {
        random: random,
        today: today
    };
});
console.log(benchmark.results);

object-literal
Los resultados son prácticamente iguales y por lo tanto podemos el uso de la notación de ES6 para la creación de objetos no afecta al rendimiento.

Ahora vamos a ver una nueva característica con la notación literal de objetos: la posibilidad de dar nombres dinámicamente a las propiedades por medio de [ ] en la parte izquierda de la definición de las propiedades. Esta funcionalidad de ES6 nos permite dar nombres calculados a las propiedades dentro de la notación literal de los objetos y no despúes de su definición como tenemos que hacer en ES5.

"use strict";
var benchmark = require('benchmarkjs');
benchmark('dynamic property name ES6',
function () {
    var random = 0 | Math.random()*1000;
    var today = Date.now();
    var o = {
        ['prop_' + random] : today,
        random,
        today
    };
});
benchmark('dynamic property name ES5',
function () {
    var random = 0 | Math.random()*1000;
    var today = Date.now();
    var o = {
        random: random,
        today: today
    };
    o['prop_' + random] =  today;
});
console.log(benchmark.results);

computed-name

El uso del computed name tiene un efecto negativo en el rendimiento bastante significativo. Si comprobamos la optimización que ha realizado v8 podemos ver que el uso de las computed name activa la optimización turbofan que no es muy eficiente en estos momentos.

Arrow functions

Ahora disponemos de una nueva forma de definir funciones por medio de las arrow function. Esta forma de definición de funciones es muy práctica para pequeñas porciones de código que queremos definir de forma compacta. Vamos a comprobar si afecta al rendimiento.

"use strict";
var benchmark = require('benchmarkjs');
var factorial1 = (n) => {
    if(n === 0) {
        return 1;
    }
    return n * factorial1(n - 1);
};
benchmark('arrow function', function () {
    var random = 0 | Math.random()*10;
    return factorial1(random);
});
function factorial2(n) {
    if (n === 0) {
        return 1;
    }
    return n * factorial2(n - 1);
}
benchmark('function', function () {
    var random = 0 | Math.random()*10;
    return factorial2(random);
});
console.log(benchmark.results);

arrow-function

El resultado es que no hay diferencia significativa de rendimiento por el uso de las arrow funcion y las funciones tradicionales, lo cual nos anima a su utilización sin temer efectos secundarios no deseados.

Otra de las funcionalidades que esconden las arrow funcion es el uso léxico de this, lo cual nos puede simpificar bastante el manejo dentro de contextos que de otra forma podrían ser confusos o producir errores sobre a que está apuntando this.

"use strict";
var benchmark = require('benchmarkjs');
var adder1 = {
    base : 0,
    add : function(a) {
        var f = v => this.base += v;
        return f(a);
    }
};
benchmark('arrow function', function () {
    var random = 0 | Math.random()*10;
    return adder1.add(random);
});
var adder2 = {
    base: 0,
    add: function add(a) {
        var _this = this;
        var f = function f(v) {
            return _this.base += v;
        };
        return f(a);
    }
};
benchmark('function', function () {
    var random = 0 | Math.random()*10;
    return adder2.add(random);
});
console.log(benchmark.results);

arrow-function-this

En este caso las arrow function ofrecen un rendimiento algo superior al uso de las funciones tradicionales si tenemos que cambiar el contexto de this guardando su contenido en una variable. En definitiva, las arrow function son una funcionalidad que ofrece una buena respuesta al rendimiento y es posible aprovechar sus ventajas sin perder velocidad.

Generadores

Por último vamos a ver una de las funcionalidades que tiene los generadores de ES6 frente a las funciones tradicionales de ES5 implementando un patrón de diseño lazy. Las funciones generadoras tienen muchos más usos que este, pero nos sirve para hacer una aproximación a las diferencias de rendimiento.

"use strict";
var benchmark = require('benchmarkjs');
function* fibonacci1(){
    var fn1 = 1;
    var fn2 = 1;
    while (true){
        var current = fn2;
        fn2 = fn1;
        fn1 = fn1 + current;
        yield current;
    }
}
var sequence1 = fibonacci1();
benchmark('generator', function () {
    return sequence1.next().value;
});
function fibonacci2(){
    var fn1 = 1;
    var fn2 = 1;
    return function() {
        var current = fn2;
        fn2 = fn1;
        fn1 = fn1 + current;
        return current;
    }
}
var sequence2 = fibonacci2();
benchmark('lazy function', function () {
    return sequence2();
});
console.log(benchmark.results);

generator
Como podemos ver, el uso de generadores tiene un efecto muy significativo de rendimiento, al menos para este caso de uso y por lo tanto las funciones con un patrón lazy dan un resultado mucho más eficiente.

Conclusiones

Como podemos comprobar, hay bastantes funcionalidades de ES6 que tienen un efecto negativo en cuanto al rendimiento. El uso de generadores, computed names, template string o let producen una pérdida muy significativa de velocidad de ejecución. Es cierto que ofrece funcionalidades que nos costaría bastante reproducir en ES5, pero la mejora en el código no parece justificar su uso ante la importante pérdida de velocidad si el rendimiento es un factor es importante para nosotros.

Por el contrario las arrow function, de la nueva notación literal de objetos, clases o const no presentan estos problemas de rendimiento y pueden ser utilizados sin temor a perder velocidad de forma significativa.

Podemos decir que en general la implementación de ES6 que hace V8 y que utiliza Node.js es todavía poco eficiente en términos generales y que deberemos esperar a futuras versiones para que tengamos la confianza general de que su utilización no va a esconder serios problemas para nuestras aplicaciones.

Novedades

Datos inmutables en Javascript

Datos inmutables en Javascript

En Javascript todo parece mutable, es decir, que se puede cambiar, pero lo cierto es que también nos ofrece varios mecanismos para conseguir que los datos que manejamos, especialmente los objetos, sean inmutables. Te invitamos a descubrir cómo…

Copiar objetos en Javascript

Copiar objetos en Javascript

Copiar objetos no es algo sencillo, incluso se podría decir que en si mismo no es posible, ya que el concepto «copiar» no entra dentro del paradigma de los objetos. No obstante, por medio de instrucciones como Object.assign() hemos aprendido como obtener objetos con las mismas propiedades, pero está técnica no se puede aplicar a todos los tipos de objetos disponibles en Javascript. Vamos a ver cómo podemos copiar cualquier tipo de objeto…

Descubre los Javascript Array Internals

Descubre los Javascript Array Internals

El Array es una de las estructuras más utilizadas en Javascript y no siempre bien comprendida. Hoy os invitamos a analizar el comportamiento interno de este objeto y descubrir cómo Javascript implementa las diferente acciones con los Array y que operaciones internas se realizan en cada caso.

Web Components: pasado, presente y futuro

Web Components: pasado, presente y futuro

Los Web Components aparecieron en el panorama de desarrollo hace ya bastante tiempo. Desde su presentación se les ha prestado mucha atención, pero lo cierto es que no han sido acogidos de forma generalizada, quizás por la difusión de nuevos y potentes frameworks. Nos preguntamos qué ha pasado con este estándar y, sobre todo, que puede pasar de aquí en adelante con el uso práctico de los componentes web.