Zoeken…
Invoering
JavaScript, zoals elke taal, vereist dat we oordeelkundig zijn in het gebruik van bepaalde taalfuncties. Overmatig gebruik van sommige functies kan de prestaties verminderen, terwijl sommige technieken kunnen worden gebruikt om de prestaties te verbeteren.
Opmerkingen
Vergeet niet dat voortijdige optimalisatie de wortel is van alle kwaad. Schrijf eerst een duidelijke, correcte code en gebruik vervolgens een profiler om te zoeken naar specifieke te verbeteren gebieden als u prestatieproblemen heeft. Verspil geen tijd aan het optimaliseren van code die de algehele prestaties niet op een zinvolle manier beïnvloedt.
Meten, meten, meten. Prestaties kunnen vaak contra-intuïtief zijn en veranderen in de loop van de tijd. Wat nu sneller is, is mogelijk niet in de toekomst en kan afhankelijk zijn van uw gebruik. Zorg ervoor dat alle optimalisaties die u aanbrengt daadwerkelijk verbeteren, de prestaties niet schaden en dat de verandering de moeite waard is.
Vermijd proberen / vangen in kritieke functies
Sommige JavaScript-engines (bijvoorbeeld de huidige versie van Node.js en oudere versies van Chrome vóór Ignition + turbofan) voeren de optimizer niet uit op functies die een try / catch-blok bevatten.
Als u uitzonderingen in prestatie-kritische code moet verwerken, kan het in sommige gevallen sneller zijn om het proberen / vangen in een aparte functie te houden. Deze functie wordt bijvoorbeeld niet geoptimaliseerd door sommige implementaties:
function myPerformanceCriticalFunction() {
try {
// do complex calculations here
} catch (e) {
console.log(e);
}
}
U kunt echter weigeren om de langzame code naar een afzonderlijke functie te verplaatsen (die kan worden geoptimaliseerd) en deze vanuit het try
blok aan te roepen.
// 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);
}
}
Hier is een jsPerf-benchmark die het verschil toont: https://jsperf.com/try-catch-deoptimization . In de huidige versie van de meeste browsers zou er niet veel verschil moeten zijn, maar in minder recente versies van Chrome en Firefox, of IE, is de versie die een helperfunctie in de try / catch aanroept waarschijnlijk sneller.
Merk op dat dergelijke optimalisaties zorgvuldig en met feitelijk bewijs moeten worden gemaakt op basis van het profileren van uw code. Naarmate JavaScript-motoren beter worden, kan dit de prestaties nadelig beïnvloeden in plaats van helpen of helemaal geen verschil maken (maar de code zonder reden ingewikkeld maken). Of het helpt, pijn doet of geen verschil maakt, kan van veel factoren afhangen, dus meet altijd de effecten op uw code. Dat geldt voor alle optimalisaties, maar vooral micro-optimalisaties zoals deze die afhankelijk zijn van details op laag niveau van de compiler / runtime.
Gebruik een memoizer voor zware computerfuncties
Als u een functie bouwt die mogelijk zwaar is voor de processor (clientside of serveride), wilt u misschien een memoizer overwegen die een cache is van eerdere functie-uitvoeringen en hun geretourneerde waarden . Hiermee kunt u controleren of de parameters van een functie eerder zijn doorgegeven. Vergeet niet dat pure functies functies zijn die een invoer hebben gegeven, een overeenkomstige unieke uitvoer retourneren en geen bijwerkingen veroorzaken die buiten hun bereik vallen, dus u moet geen memoizers toevoegen aan functies die onvoorspelbaar zijn of afhankelijk zijn van externe bronnen (zoals AJAX-oproepen of willekeurig geretourneerde waarden).
Laten we zeggen dat ik een recursieve factorfunctie heb:
function fact(num) {
return (num === 0)? 1 : num * fact(num - 1);
}
Als ik bijvoorbeeld kleine waarden van 1 tot 100 doorgeef, zou er geen probleem zijn, maar zodra we dieper gaan, kunnen we de call-stack opblazen of het proces een beetje pijnlijk maken voor de Javascript-engine waarin we dit doen, vooral als de motor niet meetelt met tail-call-optimalisatie (hoewel Douglas Crockford zegt dat native ES6 tail-call-optimalisatie omvat).
We zouden ons eigen woordenboek hard kunnen coderen van 1 tot god-weet-welk nummer met hun bijbehorende faculteiten, maar ik weet niet zeker of ik dat adviseer! Laten we een memoizer maken, zullen we?
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 kunnen we het gaan gebruiken:
Nu ik beginnen na te denken over wat ik deed, als ik toename van 1 in plaats van verlagen van num, kon ik alle faculteiten hebben in de cache van 1 tot num in de cache recursief, maar ik laat dat voor u.
Dit is geweldig, maar wat als we meerdere parameters hebben ? Dit is een probleem? Niet helemaal, we kunnen een aantal leuke trucs doen, zoals JSON.stringify () gebruiken in de array argumenten of zelfs een lijst met waarden waarvan de functie afhankelijk is (voor objectgeoriënteerde benaderingen). Dit wordt gedaan om een unieke sleutel te genereren met alle argumenten en afhankelijkheden.
We kunnen ook een functie maken die andere functies "memoriseert", met hetzelfde scoopconcept als voorheen (een nieuwe functie retourneren die het origineel gebruikt en toegang heeft tot het cache-object):
WAARSCHUWING: ES6-syntaxis, als het u niet var args = Array.prototype.slice.call(null, arguments);
, vervang ... door niets en gebruik de var args = Array.prototype.slice.call(null, arguments);
truc; vervang const en laat met var, en de andere dingen die je al weet.
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
}
Merk nu op dat dit voor meerdere argumenten zal werken, maar niet van veel nut zal zijn in object-georiënteerde methoden denk ik, je hebt misschien een extra object nodig voor afhankelijkheden. Ook kan func.apply(null, args)
worden vervangen door func(...args)
omdat array-destructurering ze afzonderlijk verzendt in plaats van als array-formulier. Ook, alleen voor referentie, zal het doorgeven van een array als een argument aan func niet werken tenzij je Function.prototype.apply
zoals ik deed.
Om de bovenstaande methode te gebruiken:
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 van uw code - uitvoeringstijd meten
De meeste prestatietips zijn erg afhankelijk van de huidige staat van JS-motoren en zullen naar verwachting alleen op een bepaald moment relevant zijn. De fundamentele wet van prestatie-optimalisatie is dat u eerst moet meten voordat u probeert te optimaliseren en opnieuw meet na een veronderstelde optimalisatie.
Om de uitvoeringstijd van de code te meten, kunt u verschillende tijdmeetinstrumenten gebruiken, zoals:
Prestatie- interface die timing-gerelateerde prestatie-informatie voor de gegeven pagina vertegenwoordigt (alleen beschikbaar in browsers).
process.hrtime op Node.js geeft u timinginformatie als [seconden, nanoseconden] tupels. Zonder argument geretourneerd retourneert het een willekeurige tijd, maar met een eerder geretourneerde waarde als argument retourneert het het verschil tussen de twee uitvoeringen.
Console timers console.time("labelName")
start een timer die u kunt gebruiken om bij te houden hoe lang een operatie vindt. U geeft elke timer een unieke labelnaam en er kunnen maximaal 10.000 timers op een bepaalde pagina worden uitgevoerd. Wanneer u console.timeEnd("labelName")
met dezelfde naam console.timeEnd("labelName")
, zal de browser de timer voor de opgegeven naam voltooien en de tijd in milliseconden uitvoeren, die is verstreken sinds de timer is gestart. De tekenreeksen die zijn doorgegeven aan time () en timeEnd () moeten overeenkomen, anders zal de timer niet eindigen.
Date.now functie Date.now()
geeft de huidige tijdstempel in milliseconden, dat is een aantal representatie van de tijd vanaf 1 januari 1970 00:00:00 GMT tot nu toe. De methode now () is een statische methode van Date, daarom gebruikt u deze altijd als Date.now ().
Voorbeeld 1 met: performance.now()
In dit voorbeeld gaan we de verstreken tijd berekenen voor de uitvoering van onze functie, en we gaan de methode Performance.now () gebruiken die een DOMHighResTimeStamp retourneert , gemeten in milliseconden, nauwkeurig tot op een duizendste van een milliseconde.
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));
Het resultaat in console ziet er ongeveer zo uit:
Elapsed time: 0.10000000009313226
Gebruik van performance.now()
heeft de hoogste precisie in browsers met een nauwkeurigheid tot een duizendste van een milliseconde, maar de laagste compatibiliteit .
Voorbeeld 2 met: Date.now()
In dit voorbeeld gaan we de verstreken tijd berekenen voor de initialisatie van een grote array (1 miljoen waarden), en we gaan de methode Date.now()
gebruiken
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
Voorbeeld 3 met: console.time("label")
& console.timeEnd("label")
In dit voorbeeld doen we dezelfde taak als in Voorbeeld 2, maar we gaan de methoden console.time("label")
& console.timeEnd("label")
gebruiken
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
Voorbeeld 4 met behulp van process.hrtime()
In Node.js-programma's is dit de meest nauwkeurige manier om de bestede tijd te meten.
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
Geef de voorkeur aan lokale variabelen boven globalen, kenmerken en geïndexeerde waarden
Javascript-engines zoeken eerst naar variabelen binnen het lokale bereik voordat ze hun zoekopdracht uitbreiden naar grotere bereiken. Als de variabele een geïndexeerde waarde in een array is, of een kenmerk in een associatieve array, zoekt deze eerst naar de bovenliggende array voordat de inhoud wordt gevonden.
Dit heeft implicaties bij het werken met prestatie-kritische code. Neem bijvoorbeeld een gemeenschappelijke for
lus:
var global_variable = 0;
function foo(){
global_variable = 0;
for (var i=0; i<items.length; i++) {
global_variable += items[i];
}
}
Voor iedere iteratie in for
lus, zal de motor opzoeken items
, lookup de length
attribuut in posten, lookup items
opnieuw opzoeken van de waarde bij index i
van items
, en uiteindelijk lookup global_variable
eerst de lokale scope proberen voor het controleren van het globale bereik.
Een performante herschrijving van de bovenstaande functie is:
function foo(){
var local_variable = 0;
for (var i=0, li=items.length; i<li; i++) {
local_variable += items[i];
}
return local_variable;
}
Voor iedere iteratie in de herschreven for
lus, zal de motor opzoeken li
, lookup items
, lookup waarde bij index i
en lookup local_variable
, alleen deze keer nodig om de lokale scope controleren.
Hergebruik objecten in plaats van opnieuw te maken
Voorbeeld 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);
}
Lus B is 4 (400%) keer sneller dan Lus A
Het is erg inefficiënt om een nieuw object in prestatiecode te maken. Lus A roept functietest test()
die elke oproep een nieuw object retourneert. Het gemaakte object wordt elke iteratie genegeerd, Lus B roept test1()
die vereist dat het object wordt teruggestuurd. Het gebruikt dus hetzelfde object en vermijdt de toewijzing van een nieuw object en buitensporige GC-hits. (GC zijn niet opgenomen in de prestatietest)
Voorbeeld 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);
}
Lus B is 5 (500%) keer sneller dan lus A
Beperk DOM-updates
Een veel voorkomende fout in JavaScript wanneer deze in een browseromgeving wordt uitgevoerd, is het bijwerken van de DOM vaker dan nodig.
Het probleem hier is dat elke update in de DOM-interface ervoor zorgt dat de browser het scherm opnieuw rendert. Als een update de lay-out van een element op de pagina wijzigt, moet de hele paginalay-out opnieuw worden berekend, en dit is zeer prestatie-zwaar, zelfs in de eenvoudigste gevallen. Het opnieuw tekenen van een pagina staat bekend als opnieuw plaatsen en kan ertoe leiden dat een browser traag wordt of zelfs niet meer reageert.
Het gevolg van het te vaak bijwerken van het document wordt geïllustreerd met het volgende voorbeeld van het toevoegen van items aan een lijst.
Beschouw het volgende document met een <ul>
-element:
<!DOCTYPE html>
<html>
<body>
<ul id="list"></ul>
</body>
</html>
We voegen 5000
items toe aan de lijst die 5000 keer wordt herhaald (je kunt dit met een groter aantal op een krachtige computer proberen om het effect te vergroten).
var list = document.getElementById("list");
for(var i = 1; i <= 5000; i++) {
list.innerHTML += `<li>item ${i}</li>`; // update 5000 times
}
In dit geval kunnen de prestaties worden verbeterd door alle 5000 wijzigingen in één enkele DOM-update te groeperen.
var list = document.getElementById("list");
var html = "";
for(var i = 1; i <= 5000; i++) {
html += `<li>item ${i}</li>`;
}
list.innerHTML = html; // update once
De functie document.createDocumentFragment()
kan worden gebruikt als een lichtgewicht container voor de HTML die door de lus is gemaakt. Deze methode is iets sneller dan het wijzigen van de innerHTML
eigenschap van het containerelement (zoals hieronder weergegeven).
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);
Objecteigenschappen initialiseren met null
Alle moderne JavaScript JIT-compilers die code proberen te optimaliseren op basis van verwachte objectstructuren. Enkele tips van mdn .
Gelukkig zijn de objecten en eigenschappen vaak "voorspelbaar", en in dergelijke gevallen kan hun onderliggende structuur ook voorspelbaar zijn. JIT's kunnen hierop vertrouwen om voorspelbare toegang sneller te maken.
De beste manier om een object voorspelbaar te maken, is door een hele structuur in een constructor te definiëren. Dus als u na het maken van een object wat extra eigenschappen wilt toevoegen, definieer deze dan in een constructor met null
. Dit helpt de optimizer om het gedrag van objecten gedurende zijn hele levenscyclus te voorspellen. Alle compilers hebben echter verschillende optimizers en de prestatieverbetering kan verschillen, maar over het algemeen is het een goede gewoonte om alle eigenschappen in een constructor te definiëren, zelfs als hun waarde nog niet bekend is.
Tijd voor wat testen. In mijn test maak ik een groot aantal klasseninstanties met een for-lus. Binnen de lus wijs ik dezelfde string toe aan de eigenschap "x" van alle objecten vóór de initialisatie van de array. Als de constructor de eigenschap "x" met null initialiseert, wordt de array altijd beter, zelfs als deze een extra instructie uitvoert.
Dit is code:
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);
})()
Dit is het resultaat voor Chrome en Firefox.
FireFox Chrome
--------------------------
f1 6,400 11,400
f2 1,700 9,600
Zoals we kunnen zien, zijn de prestatieverbeteringen tussen de twee heel verschillend.
Wees consistent in het gebruik van Nummers
Als de engine correct kan voorspellen dat u een specifiek klein type voor uw waarden gebruikt, kan hij de uitgevoerde code optimaliseren.
In dit voorbeeld gebruiken we deze triviale functie die de elementen van een array opsomt en de benodigde tijd uitvoert:
// 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);
Laten we een array maken en de elementen samenvatten:
var N = 12345,
arr = [];
for (var i=0; i<N; i++) arr[i] = Math.random();
Resultaat:
Summing took 384416 nanoseconds
Laten we nu hetzelfde doen, maar dan met alleen gehele getallen:
var N = 12345,
arr = [];
for (var i=0; i<N; i++) arr[i] = Math.round(1000*Math.random());
Resultaat:
Summing took 180520 nanoseconds
Het optellen van gehele getallen duurde hier de helft van de tijd.
Motoren gebruiken niet dezelfde typen als u in JavaScript hebt. Zoals u waarschijnlijk weet, zijn alle getallen in JavaScript IEEE754-getallen met dubbele precisie met drijvende komma, er is geen specifieke beschikbare weergave voor gehele getallen. Maar motoren, wanneer ze kunnen voorspellen dat u alleen gehele getallen gebruikt, kunnen een compactere en sneller te gebruiken weergave gebruiken, bijvoorbeeld korte gehele getallen.
Dit soort optimalisatie is vooral belangrijk voor berekeningen of gegevensintensieve toepassingen.