Sök…


Introduktion

JavaScript, som alla språk, kräver att vi är försiktiga när vi använder vissa språkfunktioner. Överanvändning av vissa funktioner kan minska prestandan, medan vissa tekniker kan användas för att öka prestandan.

Anmärkningar

Kom ihåg att för tidig optimering är roten till allt ont. Skriv först tydlig, korrekt kod, sedan om du har prestandaproblem, använd en profiler för att leta efter specifika områden att förbättra. Slösa inte tid på att optimera koden som inte påverkar det totala resultatet på ett meningsfullt sätt.

Mät, mät, mät. Prestanda kan ofta vara motsatt och förändras över tid. Vad som är snabbare nu kanske inte kommer i framtiden och kan bero på ditt användningsfall. Se till att alla optimeringar du gör faktiskt förbättrar, inte skadar prestandan och att förändringen är värdefull.

Undvik försök / fångst i prestandakritiska funktioner

Vissa JavaScript-motorer (till exempel den aktuella versionen av Node.js och äldre versioner av Chrome före Ignition + turbofan) kör inte optimeringsprogrammet på funktioner som innehåller ett test / fångst-block.

Om du behöver hantera undantag i prestandakritisk kod kan det i vissa fall vara snabbare att hålla försök / fångst i en separat funktion. Till exempel kommer denna funktion inte att optimeras av vissa implementationer:

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

Du kan emellertid refactor för att flytta den långsamma koden till en separat funktion (som kan optimeras) och ringa den inifrån 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);
    }
}

Här är ett jsPerf-riktmärke som visar skillnaden: https://jsperf.com/try-catch-deoptimization . I den aktuella versionen av de flesta webbläsare borde det inte vara någon stor skillnad, men i mindre senaste versioner av Chrome och Firefox, eller IE, kommer den versionen som kallar en hjälpfunktion i try / catch förmodligen att bli snabbare.

Observera att optimeringar som denna bör göras noggrant och med faktiska bevis baserat på profilering av din kod. När JavaScript-motorer blir bättre kan det leda till att det skadar prestanda istället för att hjälpa eller göra någon skillnad alls (men komplicera koden utan anledning). Oavsett om det hjälper, gör ont eller gör ingen skillnad kan bero på många faktorer, så mät alltid effekterna på din kod. Det är sant för alla optimeringar, men särskilt mikrooptimeringar som denna som beror på detaljer på låg nivå i kompilatorn / runtime.

Använd en memoizer för tunga datorfunktioner

Om du bygger en funktion som kan vara tung på processorn (antingen klientsidan eller serversidan) kanske du vill överväga en memoizer som är en cache från tidigare funktionsutföranden och deras returnerade värden . Detta gör att du kan kontrollera om parametrarna för en funktion har gått tidigare. Kom ihåg att rena funktioner är de som ges en inmatning, returnerar en motsvarande unik utgång och inte orsakar biverkningar utanför deras omfattning, så du bör inte lägga till memoizers till funktioner som är oförutsägbara eller beror på externa resurser (som AJAX-samtal eller slumpmässigt) returnerade värden).

Låt oss säga att jag har en rekursiv faktorfunktion:

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

Om jag till exempel överför små värden från 1 till 100 skulle det inte vara några problem, men när vi börjar gå djupare kan vi kanske spränga samtalstacken eller göra processen lite smärtsam för Javascript-motorn vi gör detta i, speciellt om motorn inte räknas med optimering av svängsamtal (även om Douglas Crockford säger att den ursprungliga ES6 har optimering av svanssamtal ingår).

Vi kan hårt koda vår egen ordlista från 1 till gud-vet-vilket nummer med deras motsvarande faktorer men jag är inte säker på om jag rekommenderar det! Låt oss skapa en memoizer, ska vi?

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

Nu kan vi börja använda det:

Så här är det gjort

Nu när jag börjar reflektera över vad jag gjorde, om jag skulle öka från 1 istället för att minska från num , kunde jag ha cacherat alla faktorer från 1 till num i cachen rekursivt, men jag kommer att lämna det åt dig.

Det här är bra men vad händer om vi har flera parametrar ? Det här är ett problem? Inte riktigt, vi kan göra några trevliga trick som att använda JSON.stringify () på argumentmatrisen eller till och med en lista över värden som funktionen kommer att bero på (för objektorienterade tillvägagångssätt). Detta görs för att skapa en unik nyckel med alla argument och beroenden inkluderade.

Vi kan också skapa en funktion som "memorerar" andra funktioner med samma omfattningskoncept som tidigare (returnerar en ny funktion som använder originalet och har åtkomst till cacheobjektet):

VARNING: ES6-syntax, om du inte gillar det, byt ut ... mot ingenting och använd var args = Array.prototype.slice.call(null, arguments); lura; byt ut const och låt med var och andra saker du redan vet.

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
}

Lägg nu märke till att detta kommer att fungera för flera argument men kommer inte att vara till stor nytta i objektorienterade metoder tror jag, du kan behöva ett extra objekt för beroenden. Dessutom kan func.apply(null, args) ersättas med func(...args) eftersom array-förstörande skickar dem separat istället för som en arrayform. Bara för referens, att en matris som argument till func fungerar fungerar inte om du använder Function.prototype.apply som jag gjorde.

För att använda metoden ovan:

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"

Justera din kod - mäta körningstiden

De flesta prestandatips är mycket beroende av JS-motorernas nuvarande tillstånd och förväntas endast vara relevanta vid en given tidpunkt. Den grundläggande lagen för prestationsoptimering är att du måste mäta först innan du försöker optimera och mäta igen efter en antagd optimering.

För att mäta kodkörningstid kan du använda olika tidsmätverktyg som:

Prestationsgränssnitt som representerar tidsrelaterad prestationsinformation för den givna sidan (endast tillgänglig i webbläsare).

process.hrtime på Node.js ger dig tidsinformation som [sekunder, nanosekunder] tuples. Kallas utan argument returnerar det en godtycklig tid men kallas med ett tidigare returnerat värde som argument returnerar det skillnaden mellan de två avrättningarna.

Console timers console.time("labelName") startar en timer som du kan använda för att spåra hur lång tid en operation tar. Du ger varje timer ett unikt etikettnamn och kan ha upp till 10 000 tidtagare som körs på en given sida. När du ringer console.timeEnd("labelName") med samma namn kommer webbläsaren att avsluta timern för givet namn och mata ut tiden i millisekunder, som förflutit sedan timern startades. Strängarna som gick till tiden () och timeEnd () måste matcha annars kommer timern inte att slutföras.

Date.now- funktionen Date.now() returnerar aktuell tidsstämpel i millisekunder, vilket är en nummerrepresentation av tiden sedan 1 januari 1970 00:00:00 UTC fram till nu. Metoden nu () är en statisk metod för Date, därför använder du den alltid som Date.now ().

Exempel 1 med: performance.now()

I det här exemplet kommer vi att beräkna den förflutna tiden för utförandet av vår funktion, och vi kommer att använda metoden Performance.now () som returnerar en DOMHighResTimeStamp , mätt i millisekunder, exakt till en tusendels millisekund.

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

Resultatet i konsolen ser ut så här:

Elapsed time: 0.10000000009313226

Användning av performance.now() har högsta precision i webbläsare med noggrannhet till en tusendels millisekund, men den lägsta kompatibiliteten .

Exempel 2 med: Date.now()

I det här exemplet kommer vi att beräkna den förflutna tiden för initieringen av en stor matris (1 miljon värden), och vi kommer att använda 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

Exempel 3 med användning av: console.time("label") & console.timeEnd("label")

I det här exemplet utför vi samma uppgift som i exempel 2, men vi kommer att använda console.time("label") & console.timeEnd("label") -metoderna

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

Exempel 4 med process.hrtime()

I Node.js-program är detta det mest exakta sättet att mäta förbrukad tid.

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

Föredra lokala variabler framför globaler, attribut och indexerade värden

Javascript-motorer letar först efter variabler inom det lokala tillämpningsområdet innan de utvidgar sin sökning till större omfattningar. Om variabeln är ett indexerat värde i en matris eller ett attribut i en associerande matris, kommer den först att leta efter överordnad array innan den hittar innehållet.

Detta har konsekvenser när du arbetar med prestandakritisk kod. Ta till exempel en vanlig for loop:

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

För varje iteration i for loop, kommer motorn lookup items , lookup den length attribut inom poster lookup items igen, slå upp värdet på index i av items , och slutligen lookup global_variable , först försöker den lokala omfattning innan du kontrollerar den globala räckvidd.

En performant omskrivning av funktionen ovan är:

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

För varje iteration i omskriven for slingan kommer motorn lookup li , lookup items , slå värdet vid index i och lookup local_variable , den här gången bara behöva kontrollera lokala omfattning.

Återanvända objekt snarare än att återskapa

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

Loop B är 4 (400%) gånger snabbare än Loop A

Det är mycket ineffektivt att skapa ett nytt objekt i prestandakod. Loop A- test() som returnerar ett nytt objekt varje samtal. Det skapade objektet kasseras varje iteration, Loop B kallar test1() som kräver att objektet returnerar att levereras. Den använder således samma objekt och undviker allokering av ett nytt objekt och överdrivna GC-träffar. (GC ingick inte i prestandatestet)

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

Loop B är 5 (500%) gånger snabbare än slinga A

Begränsa DOM-uppdateringar

Ett vanligt misstag i JavaScript när det körs i en webbläsarmiljö uppdaterar DOM ofta än nödvändigt.

Problemet här är att varje uppdatering i DOM-gränssnittet får webbläsaren att återge skärmen. Om en uppdatering ändrar layouten för ett element på sidan måste hela sidlayouten beräknas om, och det är mycket prestandatungt även i de enklaste fallen. Processen med att rita en sida igen kallas refow och kan göra att en webbläsare kör långsamt eller till och med blir svarsfri.

Konsekvensen av att uppdatera dokumentet för ofta illustreras med följande exempel på att lägga till objekt i en lista.

Tänk på följande dokument som innehåller ett <ul> -element:

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

Vi lägger till 5000 objekt i listan som loopar 5000 gånger (du kan prova detta med ett större antal på en kraftfull dator för att öka effekten).

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

I detta fall kan prestandan förbättras genom att samla alla 5000 ändringar i en enda DOM-uppdatering.

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

Funktionen document.createDocumentFragment() kan användas som en lättbehållare för HTML som skapas av slingan. Den här metoden är något snabbare än att ändra behållarelementets innerHTML egenskap (som visas nedan).

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

Initierar objektegenskaper med noll

Alla moderna JavaScript JIT-kompilatorer som försöker optimera koden baserat på förväntade objektstrukturer. Några tips från mdn .

Lyckligtvis är föremål och egenskaper ofta "förutsägbara", och i sådana fall kan deras underliggande struktur också vara förutsägbar. JIT: er kan lita på detta för att göra förutsägbara åtkomst snabbare.

Det bästa sättet att göra objekt förutsägbart är att definiera en hel struktur i en konstruktör. Så om du ska lägga till några extra egenskaper efter att objekt skapats, definiera dem i en konstruktör med null . Detta hjälper optimeringsprogrammet att förutsäga objektbeteende under hela sin livscykel. Alla kompilatorer har emellertid olika optimisatorer, och prestandaförhöjningen kan vara annorlunda, men totalt sett är det bra att definiera alla egenskaper i en konstruktör, även om deras värde ännu inte är känt.

Dags för några tester. I mitt test skapar jag ett stort utbud av vissa klassinstanser med en för-loop. Inom loopen tilldelar jag samma sträng till alla objekts "x" -egenskaper innan arrayinitiering. Om konstruktören initierar "x" -egenskap med noll, bearbetas matris alltid bättre även om det gör extra uttalande.

Detta är kod:

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

Detta är resultatet för Chrome och Firefox.

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

Som vi ser är prestandaförbättringarna mycket olika mellan de två.

Var konsekvent när du använder siffror

Om motorn kan korrekt förutsäga att du använder en specifik liten typ för dina värden, kommer den att kunna optimera den körda koden.

I det här exemplet använder vi den här triviala funktionen som summerar elementen i en matris och visar den tid det tog:

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

Låt oss göra en matris och summera elementen:

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

Resultat:

Summing took 384416 nanoseconds

Låt oss göra detsamma men med bara heltal:

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

Resultat:

Summing took 180520 nanoseconds

Sammanlagda heltal tog hälften av tiden här.

Motorer använder inte samma typer som du har i JavaScript. Som du antagligen vet är alla siffror i JavaScript IEEE754 med dubbla precision med flytande punktnummer, det finns ingen specifik tillgänglig representation för heltal. Men motorer, när de kan förutsäga att du bara använder heltal, kan använda ett mer kompakt och snabbare för att använda representation, till exempel korta heltal.

Den här typen av optimering är särskilt viktig för beräkningar eller datakrävande applikationer.



Modified text is an extract of the original Stack Overflow Documentation
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow