Ricerca…


introduzione

JavaScript, come qualsiasi altra lingua, ci impone di essere giudiziosi nell'uso di alcune funzionalità linguistiche. L'uso eccessivo di alcune funzionalità può ridurre le prestazioni, mentre alcune tecniche possono essere utilizzate per aumentare le prestazioni.

Osservazioni

Ricorda che l'ottimizzazione prematura è la radice di tutto il male. Scrivi prima un codice chiaro e corretto, quindi se hai problemi di prestazioni, usa un profiler per cercare aree specifiche da migliorare. Non perdere tempo a ottimizzare il codice che non influisce in modo significativo sulle prestazioni complessive.

Misura, misura, misura. Le prestazioni possono spesso essere controintuitive e cambiano nel tempo. Ciò che è più veloce ora potrebbe non esserlo in futuro e può dipendere dal tuo caso d'uso. Assicurati che le ottimizzazioni apportate migliorino effettivamente, non danneggino le prestazioni e che il cambiamento sia utile.

Evita di provare / catturare le funzioni critiche per le prestazioni

Alcuni motori JavaScript (ad esempio, la versione corrente di Node.js e versioni precedenti di Chrome prima di Ignition + turbofan) non eseguono l'ottimizzatore su funzioni che contengono un blocco try / catch.

Se è necessario gestire le eccezioni nel codice critico delle prestazioni, in alcuni casi può essere più veloce mantenere il try / catch in una funzione separata. Ad esempio, questa funzione non sarà ottimizzata da alcune implementazioni:

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

Tuttavia, puoi refactoring per spostare il codice lento in una funzione separata (che può essere ottimizzata) e chiamarla dall'interno del blocco 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);
    }
}

Ecco un benchmark jsPerf che mostra la differenza: https://jsperf.com/try-catch-deoptimization . Nella versione attuale della maggior parte dei browser, non dovrebbe esserci molta differenza se esiste, ma nelle versioni meno recenti di Chrome e Firefox, o IE, la versione che chiama una funzione di supporto all'interno del try / catch è probabilmente più veloce.

Nota che le ottimizzazioni come questa dovrebbero essere fatte con attenzione e con prove reali basate sulla profilazione del tuo codice. Man mano che i motori JavaScript migliorano, potrebbe finire per danneggiare le prestazioni anziché aiutare o non fare alcuna differenza (ma complicare il codice senza motivo). Se aiuta, fa male o non fa differenza può dipendere da molti fattori, quindi valuta sempre gli effetti sul tuo codice. Questo vale per tutte le ottimizzazioni, ma soprattutto per le micro-ottimizzazioni come questa che dipendono dai dettagli di basso livello del compilatore / runtime.

Utilizzare un memoizzatore per le funzioni di elaborazione intensiva

Se si sta costruendo una funzione che potrebbe essere pesante sul processore (lato client o server), si consiglia di prendere in considerazione un memoizer che è una cache delle esecuzioni delle funzioni precedenti e dei relativi valori restituiti . Ciò consente di verificare se i parametri di una funzione sono stati passati prima. Ricordate, le funzioni pure sono quelle che ricevono un input, restituiscono un output univoco corrispondente e non causano effetti collaterali al di fuori del loro scope, quindi non dovreste aggiungere memoizers a funzioni imprevedibili o dipendenti da risorse esterne (come le chiamate AJAX o in modo casuale valori restituiti).

Diciamo che ho una funzione fattoriale ricorsiva:

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

Se, ad esempio, passassi piccoli valori da 1 a 100, non ci sarebbero problemi, ma una volta che avremo iniziato ad andare più in profondità, potremmo far saltare lo stack delle chiamate o rendere il processo un po 'doloroso per il motore Javascript in cui lo stiamo facendo, specialmente se il motore non conta un'ottimizzazione di chiamata di coda (anche se Douglas Crockford afferma che l'ES6 nativo ha incluso l'ottimizzazione di coda).

Potremmo codificare con difficoltà il nostro dizionario da 1 a dio-sa-che numero con i loro fattoriali corrispondenti ma, non sono sicuro se lo consiglio! Creiamo un memoizer, dovremmo?

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; 
}());

Ora possiamo iniziare a usarlo:

Questo è come è fatto

Ora che inizio a riflettere su quello che ho fatto, se dovessi incrementare da 1 invece di decrement da num , avrei potuto memorizzare tutti i fattoriali da 1 a num nella cache in modo ricorsivo, ma lo lascerò per te.

Questo è grandioso, ma se avessimo più parametri ? Questo è un problema? Non proprio, possiamo fare alcuni trucchi come usare JSON.stringify () sull'array degli argomenti o anche un elenco di valori da cui dipenderà la funzione (per gli approcci orientati agli oggetti). Questo viene fatto per generare una chiave univoca con tutti gli argomenti e le dipendenze inclusi.

Possiamo anche creare una funzione che "memoizes" altre funzioni, usando lo stesso concetto di scope di prima (restituendo una nuova funzione che utilizza l'originale e ha accesso all'oggetto cache):

ATTENZIONE: sintassi ES6, se non ti piace, sostituisci ... con niente e usa il var args = Array.prototype.slice.call(null, arguments); trucco; sostituisci const e let con var, e le altre cose che già conosci.

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
}

Ora si noti che questo funzionerà per più argomenti, ma non sarà di grande utilità in metodi orientati agli oggetti, penso, potrebbe essere necessario un oggetto aggiuntivo per le dipendenze. Inoltre, func.apply(null, args) può essere sostituito con func(...args) poiché la destrutturazione dell'array li invierà separatamente anziché come una matrice. Inoltre, solo per riferimento, passare un array come argomento per func non funzionerà a meno che non si usi Function.prototype.apply come ho fatto io.

Per utilizzare il metodo sopra è sufficiente:

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"

Benchmarking del codice - misurazione del tempo di esecuzione

La maggior parte dei suggerimenti per le prestazioni dipende molto dallo stato attuale dei motori JS e dovrebbe essere rilevante solo in un dato momento. La legge fondamentale dell'ottimizzazione delle prestazioni è che devi prima misurare prima di provare a ottimizzare e misurare di nuovo dopo una presunta ottimizzazione.

Per misurare il tempo di esecuzione del codice, puoi utilizzare diversi strumenti di misurazione del tempo come:

Interfaccia delle prestazioni che rappresenta le informazioni sulla performance relative alla tempistica per la pagina specificata (disponibile solo nei browser).

process.hrtime su Node.js fornisce informazioni sulla tempistica come tuple [secondi, nanosecondi]. Chiamato senza argomento restituisce un tempo arbitrario ma chiamato con un valore restituito come argomento restituisce la differenza tra le due esecuzioni.

Console temporizzatori console.time("labelName") avvia un timer che è possibile utilizzare per tenere traccia di quanto dura un'operazione. Assegnate a ciascun timer un nome di etichetta univoco e possono avere fino a 10.000 timer in esecuzione su una determinata pagina. Quando si chiama console.timeEnd("labelName") con lo stesso nome, il browser terminerà il timer per il nome specificato e produrrà il tempo in millisecondi, che è trascorso dall'avvio del timer. Le stringhe passate a time () e timeEnd () devono corrispondere altrimenti il ​​timer non finirà.

Date.now funzione Date.now() ritorna corrente Timestamp in millisecondi, che è un numero rappresentazione del tempo dal 1 gennaio 1970 00:00:00 GMT fino ad ora. Il metodo now () è un metodo statico di Date, quindi lo usi sempre come Date.now ().

Esempio 1 usando: performance.now()

In questo esempio calcoleremo il tempo trascorso per l'esecuzione della nostra funzione e useremo il metodo Performance.now () che restituisce un DOMHighResTimeStamp , misurato in millisecondi, preciso fino a un millesimo di millisecondo.

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));

Il risultato in console sarà simile a questo:

Elapsed time: 0.10000000009313226

L'utilizzo di performance.now() ha la massima precisione nei browser con precisione fino a un millesimo di millisecondo, ma la compatibilità più bassa.

Esempio 2 usando: Date.now()

In questo esempio calcoleremo il tempo trascorso per l'inizializzazione di un grande array (1 milione di valori), e useremo il metodo 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

Esempio 3 utilizzando: console.time("label") e console.timeEnd("label")

In questo esempio stiamo facendo lo stesso compito dell'Esempio 2, ma useremo i metodi console.time("label") e 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

Esempio 4 utilizzando process.hrtime()

Nei programmi Node.js questo è il modo più preciso per misurare il tempo trascorso.

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

Preferisci le variabili locali a globali, attributi e valori indicizzati

I motori Javascript cercano prima le variabili all'interno dell'ambito locale prima di estendere la loro ricerca a ambiti più ampi. Se la variabile è un valore indicizzato in un array o un attributo in un array associativo, cercherà prima l'array parent prima di trovare il contenuto.

Ciò ha implicazioni quando si lavora con codice critico per le prestazioni. Prendiamo ad esempio un ciclo for comune:

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

Per ogni iterazione for ciclo, il motore occhiata items , ricerca il length attributo all'interno di articoli, di ricerca items ancora, ricercare il valore di indice i di items , e infine occhiata global_variable , primo tentativo di applicazione locale prima di controllare la portata globale.

Una riscrittura performante della funzione di cui sopra è:

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

Per ogni iterazione nel ciclo riscritto for , il motore cercherà li , items ricerca, cerca il valore all'indice i , e local_variable , questa volta solo per controllare l'ambito locale.

Riutilizza gli oggetti piuttosto che ricreare

Esempio 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);
}

Il Loop B è 4 (400%) volte più veloce del Loop A

È molto inefficiente creare un nuovo oggetto nel codice delle prestazioni. Loop A chiama function test() che restituisce un nuovo oggetto ad ogni chiamata. L'oggetto creato viene scartato ogni iterazione, Loop B chiama test1() che richiede il ritorno dell'oggetto da fornire. Utilizza quindi lo stesso oggetto ed evita l'allocazione di un nuovo oggetto e gli eccessivi hit GC. (GC non sono stati inclusi nel test delle prestazioni)

Esempio 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);                
}

Il loop B è 5 (500%) volte più veloce del loop A

Limita gli aggiornamenti DOM

Un errore comune visto in JavaScript quando viene eseguito in un ambiente browser sta aggiornando il DOM più spesso del necessario.

Il problema qui è che ogni aggiornamento nell'interfaccia DOM fa sì che il browser riesegua il rendering dello schermo. Se un aggiornamento cambia il layout di un elemento nella pagina, l'intero layout della pagina deve essere ricalcolato, e questo è molto pesante anche nei casi più semplici. Il processo di ridisegnare una pagina è noto come reflow e può far sì che un browser funzioni lentamente o addirittura non risponda.

La conseguenza dell'aggiornamento troppo frequente del documento è illustrata nel seguente esempio di aggiunta di elementi a un elenco.

Considera il seguente documento contenente un elemento <ul> :

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

Aggiungiamo 5000 voci alla lista in loop di 5000 volte (puoi provare questo con un numero maggiore su un potente computer per aumentare l'effetto).

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

In questo caso, le prestazioni possono essere migliorate raggruppando tutte le 5000 modifiche in un singolo aggiornamento 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 funzione document.createDocumentFragment() può essere utilizzata come contenitore leggero per l'HTML creato dal loop. Questo metodo è leggermente più veloce della modifica della proprietà innerHTML dell'elemento contenitore (come mostrato di seguito).

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);

Inizializzazione delle proprietà dell'oggetto con null

Tutti i moderni compilatori JIT JavaScript che cercano di ottimizzare il codice in base alle strutture oggetto previste. Qualche consiglio da mdn .

Fortunatamente, gli oggetti e le proprietà sono spesso "prevedibili" e in tali casi la loro struttura sottostante può anche essere prevedibile. Le JIT possono fare affidamento su questo per rendere più veloci gli accessi prevedibili.

Il modo migliore per rendere prevedibile l'oggetto è definire un'intera struttura in un costruttore. Pertanto, se si aggiungono alcune proprietà aggiuntive dopo la creazione dell'oggetto, definirle in un costruttore con valore null . Ciò consentirà all'ottimizzatore di prevedere il comportamento degli oggetti per l'intero ciclo di vita. Tuttavia, tutti i compilatori hanno diversi ottimizzatori e l'aumento delle prestazioni può essere diverso, ma nel complesso è buona pratica definire tutte le proprietà in un costruttore, anche quando il loro valore non è ancora noto.

Tempo per alcuni test. Nel mio test, sto creando una vasta gamma di alcune istanze di classi con un ciclo for. All'interno del ciclo, sto assegnando la stessa stringa alla proprietà "x" di tutti gli oggetti prima dell'inizializzazione dell'array. Se il costruttore inizializza la proprietà "x" con null, array si elabora sempre meglio anche se sta eseguendo un'istruzione extra.

Questo è il codice:

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);
})()

Questo è il risultato per Chrome e Firefox.

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

Come possiamo vedere, i miglioramenti delle prestazioni sono molto diversi tra i due.

Sii coerente nell'uso dei numeri

Se il motore è in grado di prevedere correttamente che stai utilizzando uno specifico tipo piccolo per i tuoi valori, sarà in grado di ottimizzare il codice eseguito.

In questo esempio, useremo questa banale funzione sommando gli elementi di un array e emettendo il tempo impiegato:

// 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);

Facciamo un array e sommiamo gli elementi:

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

Risultato:

Summing took 384416 nanoseconds

Ora, facciamo lo stesso, ma con solo numeri interi:

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

Risultato:

Summing took 180520 nanoseconds

Sommando gli interi ci sono voluti metà del tempo.

I motori non usano gli stessi tipi che hai in JavaScript. Come probabilmente saprai, tutti i numeri in JavaScript sono numeri in virgola mobile a precisione doppia IEEE754, non esiste una specifica rappresentazione disponibile per i numeri interi. Ma i motori, quando possono prevedere solo gli interi, possono utilizzare una rappresentazione più compatta e più veloce da utilizzare, ad esempio interi brevi.

Questo tipo di ottimizzazione è particolarmente importante per le applicazioni di calcolo o di dati intensivi.



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow