Buscar..


Introducción

JavaScript, como cualquier idioma, requiere que seamos juiciosos en el uso de ciertas funciones del idioma. El uso excesivo de algunas características puede disminuir el rendimiento, mientras que algunas técnicas pueden usarse para aumentar el rendimiento.

Observaciones

Recuerda que la optimización prematura es la raíz de todo mal. Escriba primero el código claro y correcto; luego, si tiene problemas de rendimiento, use un generador de perfiles para buscar áreas específicas para mejorar. No pierda tiempo optimizando el código que no afecta al rendimiento general de una manera significativa.

Medir, medir, medir. El rendimiento a menudo puede ser contrario a la intuición y cambia con el tiempo. Lo que ahora es más rápido podría no serlo en el futuro y puede depender de su caso de uso. Asegúrese de que las optimizaciones que realice en realidad mejoren, no afecten el rendimiento, y que el cambio valga la pena.

Evite probar / atrapar en funciones de rendimiento crítico

Algunos motores de JavaScript (por ejemplo, la versión actual de Node.js y versiones anteriores de Chrome antes de Ignition + turbofan) no ejecutan el optimizador en funciones que contienen un bloque try / catch.

Si necesita manejar excepciones en el código crítico para el rendimiento, en algunos casos puede ser más rápido mantener el try / catch en una función separada. Por ejemplo, esta función no será optimizada por algunas implementaciones:

function myPerformanceCriticalFunction() {
    try {
        // do complex calculations here
    } catch (e) {
        console.log(e);
    }
}

Sin embargo, puede refactorizar para mover el código lento a una función separada (que puede optimizarse) y llamarlo desde dentro del bloque try .

// This function can be optimized
function doCalculations() {
    // do complex calculations here
}

// Still not always optimized, but it's not doing much so the performance doesn't matter
function myPerformanceCriticalFunction() {
    try {
        doCalculations();
    } catch (e) {
        console.log(e);
    }
}

Aquí hay un punto de referencia jsPerf que muestra la diferencia: https://jsperf.com/try-catch-deoptimization . En la versión actual de la mayoría de los navegadores, no debería haber mucha diferencia, si la hubiera, pero en las versiones menos recientes de Chrome y Firefox, o IE, la versión que llama a una función de ayuda dentro del sistema try / catch es probablemente más rápida.

Tenga en cuenta que las optimizaciones de este tipo deben realizarse con cuidado y con evidencia real basada en la creación de perfiles de su código. A medida que los motores de JavaScript mejoran, podría terminar dañando el rendimiento en lugar de ayudar, o no hacer ninguna diferencia (pero complicando el código sin ninguna razón). Si ayuda, duele o no hace ninguna diferencia puede depender de muchos factores, por lo que siempre mida los efectos en su código. Esto se aplica a todas las optimizaciones, pero especialmente a las microoptimizaciones como esta que dependen de los detalles de bajo nivel del compilador / tiempo de ejecución.

Use un memoizer para funciones de computación pesada

Si está creando una función que puede ser pesada en el procesador (tanto en el lado del cliente como en el lado del servidor), es posible que desee considerar un memoizer que es un caché de ejecuciones de funciones anteriores y sus valores devueltos . Esto le permite verificar si los parámetros de una función se pasaron antes. Recuerde, las funciones puras son aquellas que reciben una entrada, devuelven una salida única correspondiente y no causan efectos secundarios fuera de su alcance, por lo que no debe agregar memoizers a funciones que son impredecibles o que dependen de recursos externos (como las llamadas AJAX o aleatoriamente). valores devueltos).

Digamos que tengo una función factorial recursiva:

function fact(num) {
  return (num === 0)? 1 : num * fact(num - 1);
}

Si paso valores pequeños de 1 a 100, por ejemplo, no habría ningún problema, pero una vez que comencemos a profundizar, podríamos hacer estallar la pila de llamadas o hacer que el proceso sea un poco doloroso para el motor de Javascript en el que estamos haciendo esto. especialmente si el motor no cuenta con la optimización de llamadas de cola (aunque Douglas Crockford dice que el ES6 nativo incluye la optimización de llamadas de cola).

Podríamos codificar nuestro propio diccionario de 1 a Dios sabe qué número con sus correspondientes factoriales, pero no estoy seguro de si lo aconsejo. Vamos a crear un memoizer, ¿vale?

var fact = (function() {
  var cache = {}; // Initialise a memory cache object
  
  // Use and return this function to check if val is cached
  function checkCache(val) {
    if (val in cache) {
      console.log('It was in the cache :D');
      return cache[val]; // return cached
    } else {
      cache[val] = factorial(val); // we cache it
      return cache[val]; // and then return it
    }
    
    /* Other alternatives for checking are:
    || cache.hasOwnProperty(val) or !!cache[val]
    || but wouldn't work if the results of those
    || executions were falsy values.
    */
  }

  // We create and name the actual function to be used
  function factorial(num) {
    return (num === 0)? 1 : num * factorial(num - 1);
  } // End of factorial function

  /* We return the function that checks, not the one
  || that computes because  it happens to be recursive,
  || if it weren't you could avoid creating an extra
  || function in this self-invoking closure function.
  */
  return checkCache; 
}());

Ahora podemos empezar a utilizarlo:

Así es como se hace.

Ahora que empiezo a reflexionar sobre lo que hice, si incrementara de 1 en lugar de disminuir de num , podría haber almacenado en caché todos los factoriales de 1 a num en el caché de forma recursiva, pero lo dejaré para usted.

Esto es genial, pero ¿y si tenemos múltiples parámetros ? ¿Esto es un problema? No del todo, podemos hacer algunos buenos trucos como usar JSON.stringify () en la matriz de argumentos o incluso una lista de valores de los que dependerá la función (para enfoques orientados a objetos). Esto se hace para generar una clave única con todos los argumentos y dependencias incluidos.

También podemos crear una función que "memorice" otras funciones, usando el mismo concepto de alcance que antes (devolviendo una nueva función que usa el original y tiene acceso al objeto de caché):

ADVERTENCIA: la sintaxis de ES6, si no le gusta, reemplace ... por nada y use la var args = Array.prototype.slice.call(null, arguments); truco; Reemplaza const y deja con var, y las otras cosas que ya sabes.

function memoize(func) {
  let cache = {};

  // You can opt for not naming the function
  function memoized(...args) {
    const argsKey = JSON.stringify(args);
    
    // The same alternatives apply for this example
    if (argsKey in cache) {
      console.log(argsKey + ' was/were in cache :D');
      return cache[argsKey];
    } else {
      cache[argsKey] = func.apply(null, args); // Cache it
      return cache[argsKey]; // And then return it
    }
  }

  return memoized; // Return the memoized function
}

Ahora note que esto funcionará para múltiples argumentos, pero creo que no será de mucha utilidad en métodos orientados a objetos. Es posible que necesite un objeto adicional para las dependencias. Además, func.apply(null, args) se puede reemplazar con func(...args) ya que la desestructuración de matrices los enviará por separado en lugar de como una forma de matriz. Además, solo como referencia, pasar una matriz como argumento a func no funcionará a menos que use Function.prototype.apply como lo hice.

Para utilizar el método anterior simplemente:

const newFunction = memoize(oldFunction);

// Assuming new oldFunction just sums/concatenates:
newFunction('meaning of life', 42);
// -> "meaning of life42"

newFunction('meaning of life', 42); // again
// => ["meaning of life",42] was/were in cache :D
// -> "meaning of life42"

Evaluación comparativa de su código: medición del tiempo de ejecución

La mayoría de las sugerencias de rendimiento dependen mucho del estado actual de los motores JS y se espera que solo sean relevantes en un momento dado. La ley fundamental de la optimización del rendimiento es que primero debe medir antes de intentar optimizar y volver a medir después de una supuesta optimización.

Para medir el tiempo de ejecución del código, puede usar diferentes herramientas de medición de tiempo como:

Interfaz de rendimiento que representa la información de rendimiento relacionada con el tiempo para la página dada (solo disponible en los navegadores).

process.hrtime en Node.js le da información de tiempo como tuplas [segundos, nanosegundos]. Llamado sin argumento, devuelve un tiempo arbitrario pero se llama con un valor devuelto previamente como argumento, devuelve la diferencia entre las dos ejecuciones.

Los temporizadores de console.time("labelName") inician un temporizador que puede usar para rastrear cuánto tiempo lleva una operación. Le da a cada temporizador un nombre de etiqueta único y puede tener hasta 10,000 temporizadores ejecutándose en una página determinada. Cuando llama a console.timeEnd("labelName") con el mismo nombre, el navegador finalizará el temporizador para el nombre de pila y emitirá el tiempo en milisegundos, que transcurrió desde que se inició el temporizador. Las cadenas pasadas a time () y timeEnd () deben coincidir; de lo contrario, el temporizador no terminará.

La función Date.now() devuelve la marca de tiempo actual en milisegundos, que es una representación numérica de tiempo desde el 1 de enero de 1970 a las 00:00:00 UTC hasta ahora. El método now () es un método estático de Date, por lo tanto, siempre lo usas como Date.now ().

Ejemplo 1 usando: performance.now()

En este ejemplo, vamos a calcular el tiempo transcurrido para la ejecución de nuestra función, y vamos a utilizar el método Performance.now () que devuelve un DOMHighResTimeStamp , medido en milisegundos, con una precisión de milésima de milisegundo.

let startTime, endTime;

function myFunction() {
    //Slow code you want to measure
}

//Get the start time
startTime = performance.now();

//Call the time-consuming function
myFunction();

//Get the end time
endTime = performance.now();

//The difference is how many milliseconds it took to call myFunction()
console.debug('Elapsed time:', (endTime - startTime));

El resultado en la consola se verá algo así:

Elapsed time: 0.10000000009313226

El uso de performance.now() tiene la más alta precisión en navegadores con una precisión de milésima de milisegundo, pero la compatibilidad más baja.

Ejemplo 2 usando: Date.now()

En este ejemplo, vamos a calcular el tiempo transcurrido para la inicialización de una gran matriz (1 millón de valores), y vamos a utilizar el método Date.now()

let t0 = Date.now(); //stores current Timestamp in milliseconds since 1 January 1970 00:00:00 UTC
let arr = []; //store empty array
for (let i = 0; i < 1000000; i++) { //1 million iterations
   arr.push(i); //push current i value
}
console.log(Date.now() - t0); //print elapsed time between stored t0 and now

Ejemplo 3 usando: console.time("label") & console.timeEnd("label")

En este ejemplo, estamos haciendo la misma tarea que en el Ejemplo 2, pero vamos a utilizar los métodos console.time("label") & console.timeEnd("label")

console.time("t"); //start new timer for label name: "t"
let arr = []; //store empty array
for(let i = 0; i < 1000000; i++) { //1 million iterations
   arr.push(i); //push current i value
}
console.timeEnd("t"); //stop the timer for label name: "t" and print elapsed time

Ejemplo 4 usando process.hrtime()

En los programas Node.js, esta es la forma más precisa de medir el tiempo pasado.

let start = process.hrtime();

// long execution here, maybe asynchronous

let diff = process.hrtime(start);
// returns for example [ 1, 2325 ]
console.log(`Operation took ${diff[0] * 1e9 + diff[1]} nanoseconds`);
// logs: Operation took 1000002325 nanoseconds

Prefiere variables locales a globales, atributos y valores indexados

Los motores de Javascript primero buscan variables dentro del ámbito local antes de ampliar su búsqueda a ámbitos más grandes. Si la variable es un valor indexado en una matriz, o un atributo en una matriz asociativa, primero buscará la matriz primaria antes de encontrar el contenido.

Esto tiene implicaciones cuando se trabaja con código crítico para el rendimiento. Tomemos, por ejemplo, un bucle común for :

var global_variable = 0;
function foo(){
    global_variable = 0;
    for (var i=0; i<items.length; i++) {
        global_variable += items[i];
    }
}

Para cada iteración en el bucle for , el motor buscará items , buscará el atributo de length dentro de los elementos, buscará nuevamente los items , buscará el valor en el índice i de los items y, finalmente, global_variable , primero probando el alcance local antes de verificar el alcance global.

Una reescritura ejecutante de la función anterior es:

function foo(){
    var local_variable = 0;
    for (var i=0, li=items.length; i<li; i++) {
        local_variable += items[i];
    }
    return local_variable;
}

Para cada iteración en el reescrita for bucle, el motor va a las operaciones de búsqueda li , de búsqueda de items , buscar el valor en el índice i , y la búsqueda de local_variable , esta vez sólo necesitan para comprobar el alcance local.

Reutilizar objetos en lugar de recrear

Ejemplo A

var i,a,b,len;
a = {x:0,y:0}
function test(){ // return object created each call
    return {x:0,y:0};
}
function test1(a){ // return object supplied 
    a.x=0;
    a.y=0;
    return a;
}   

for(i = 0; i < 100; i ++){ // Loop A
   b = test();
}

for(i = 0; i < 100; i ++){ // Loop B
   b = test1(a);
}

El bucle B es 4 (400%) veces más rápido que el bucle A

Es muy ineficiente crear un nuevo objeto en el código de rendimiento. Loop A test() función de llamadas test() que devuelve un nuevo objeto a cada llamada. El objeto creado se descarta en cada iteración, el Loop B llama a test1() que requiere que se devuelva el objeto. Por lo tanto, utiliza el mismo objeto y evita la asignación de un nuevo objeto, y el exceso de hits GC. (GC no fueron incluidos en la prueba de rendimiento)

Ejemplo B

var i,a,b,len;
a = {x:0,y:0}
function test2(a){
    return {x : a.x * 10,y : a.x * 10};
}   
function test3(a){
    a.x= a.x * 10;
    a.y= a.y * 10;
    return a;
}  
for(i = 0; i < 100; i++){  // Loop A
    b = test2({x : 10, y : 10});
}
for(i = 0; i < 100; i++){ // Loop B
    a.x = 10;
    a.y = 10;
    b = test3(a);                
}

El bucle B es 5 (500%) veces más rápido que el bucle A

Limitar las actualizaciones de DOM

Un error común que se ve en JavaScript cuando se ejecuta en un entorno de navegador es actualizar el DOM con más frecuencia de la necesaria.

El problema aquí es que cada actualización en la interfaz DOM hace que el navegador vuelva a renderizar la pantalla. Si una actualización cambia el diseño de un elemento en la página, se debe volver a calcular todo el diseño de la página, y esto es muy pesado en el rendimiento, incluso en los casos más simples. El proceso de volver a dibujar una página se conoce como reflow y puede hacer que un navegador se ejecute lentamente o incluso deje de responder.

La consecuencia de actualizar el documento con demasiada frecuencia se ilustra con el siguiente ejemplo de agregar elementos a una lista.

Considere el siguiente documento que contiene un elemento <ul> :

<!DOCTYPE html>
<html>
    <body>
        <ul id="list"></ul>
    </body>
</html>

Agregamos 5000 elementos a la lista de ciclos 5000 veces (puede intentar esto con un número mayor en una computadora potente para aumentar el efecto).

var list = document.getElementById("list");
for(var i = 1; i <= 5000; i++) {             
    list.innerHTML += `<li>item ${i}</li>`;  // update 5000 times
}

En este caso, el rendimiento puede mejorarse agrupando los 5000 cambios en una sola actualización de DOM.

var list = document.getElementById("list");
var html = "";
for(var i = 1; i <= 5000; i++) {
    html += `<li>item ${i}</li>`;
}
list.innerHTML = html;     // update once

La función document.createDocumentFragment() se puede utilizar como un contenedor ligero para el HTML creado por el bucle. Este método es ligeramente más rápido que modificar la propiedad innerHTML del elemento contenedor (como se muestra a continuación).

var list = document.getElementById("list");
var fragment = document.createDocumentFragment();
for(var i = 1; i <= 5000; i++) {
    li = document.createElement("li");
    li.innerHTML = "item " + i;
    fragment.appendChild(li);
    i++;
}
list.appendChild(fragment);

Inicializando propiedades de objeto con nulo

Todos los compiladores de JavaScript JIT modernos intentan optimizar el código en función de las estructuras de objetos esperadas. Algunos consejos de mdn .

Afortunadamente, los objetos y las propiedades son a menudo "predecibles", y en tales casos su estructura subyacente también puede ser predecible. Los JIT pueden confiar en esto para que los accesos predecibles sean más rápidos.

La mejor manera de hacer que un objeto sea predecible es definir una estructura completa en un constructor. Entonces, si va a agregar algunas propiedades adicionales después de la creación del objeto, defínalos en un constructor con null . Esto ayudará al optimizador a predecir el comportamiento del objeto durante todo su ciclo de vida. Sin embargo, todos los compiladores tienen diferentes optimizadores, y el aumento de rendimiento puede ser diferente, pero en general es una buena práctica definir todas las propiedades en un constructor, incluso cuando su valor aún no se conoce.

Tiempo para algunas pruebas. En mi prueba, estoy creando una gran variedad de instancias de clase con un bucle for. Dentro del bucle, asigno la misma cadena a la propiedad "x" de todos los objetos antes de la inicialización de la matriz. Si el constructor inicializa la propiedad "x" con un valor nulo, la matriz siempre se procesa mejor incluso si está haciendo una declaración adicional.

Este es el código:

function f1() {
    var P = function () {
        this.value = 1
    };
    var big_array = new Array(10000000).fill(1).map((x, index)=> {
        p = new P();
        if (index > 5000000) {
            p.x = "some_string";
        }

        return p;
    });
    big_array.reduce((sum, p)=> sum + p.value, 0);
}

function f2() {
    var P = function () {
        this.value = 1;
        this.x = null;
    };
    var big_array = new Array(10000000).fill(1).map((x, index)=> {
        p = new P();
        if (index > 5000000) {
            p.x = "some_string";
        }

        return p;
    });
    big_array.reduce((sum, p)=> sum + p.value, 0);
}


(function perform(){
    var start = performance.now();
    f1();
    var duration = performance.now() - start;

    console.log('duration of f1  ' + duration);


    start = performance.now();
    f2();
    duration = performance.now() - start;

    console.log('duration of f2 ' + duration);
})()

Este es el resultado para Chrome y Firefox.

       FireFox     Chrome
 --------------------------
 f1      6,400      11,400
 f2      1,700       9,600   

Como podemos ver, las mejoras de rendimiento son muy diferentes entre los dos.

Ser consistente en el uso de números

Si el motor puede predecir correctamente que está usando un tipo pequeño específico para sus valores, podrá optimizar el código ejecutado.

En este ejemplo, usaremos esta función trivial sumando los elementos de una matriz y generando el tiempo que tomó:

// summing properties
var sum = (function(arr){
        var start = process.hrtime();
        var sum = 0;
        for (var i=0; i<arr.length; i++) {
                sum += arr[i];
        }
        var diffSum = process.hrtime(start);
        console.log(`Summing took ${diffSum[0] * 1e9 + diffSum[1]} nanoseconds`);
        return sum;
})(arr);

Hagamos una matriz y sumemos los elementos:

var     N = 12345,
        arr = [];
for (var i=0; i<N; i++) arr[i] = Math.random();

Resultado:

Summing took 384416 nanoseconds

Ahora, hagamos lo mismo pero solo con enteros:

var     N = 12345,
        arr = [];
for (var i=0; i<N; i++) arr[i] = Math.round(1000*Math.random());

Resultado:

Summing took 180520 nanoseconds

Sumar enteros tomó la mitad del tiempo aquí.

Los motores no usan los mismos tipos que tienes en JavaScript. Como probablemente sepa, todos los números en JavaScript son números de punto flotante IEEE754 de doble precisión, no hay una representación específica disponible para los enteros. Pero los motores, cuando pueden predecir que solo usen enteros, pueden usar una representación más compacta y más rápida, por ejemplo, enteros cortos.

Este tipo de optimización es especialmente importante para aplicaciones de computación o uso intensivo de datos.



Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow