Een 400% sneller prestatiepaneel door perf-ceptie

Andrés Olivares
Andrés Olivares
Nancy Li
Nancy Li

Ongeacht welk type applicatie u ontwikkelt, het optimaliseren van de prestaties en het garanderen dat deze snel laadt en soepele interacties biedt, is van cruciaal belang voor de gebruikerservaring en het succes van de applicatie. Eén manier om dit te doen is door de activiteit van een applicatie te inspecteren met behulp van profileringstools om te zien wat er onder de motorkap gebeurt terwijl deze gedurende een bepaalde periode wordt uitgevoerd. Het Prestatiepaneel in DevTools is een geweldige profileringstool om de prestaties van webapplicaties te analyseren en optimaliseren. Als uw app in Chrome draait, krijgt u een gedetailleerd visueel overzicht van wat de browser doet terwijl uw applicatie wordt uitgevoerd. Als u deze activiteit begrijpt, kunt u patronen, knelpunten en prestatieproblemen identificeren waarop u actie kunt ondernemen om de prestaties te verbeteren.

Het volgende voorbeeld begeleidt u bij het gebruik van het paneel Prestaties .

Ons profileringsscenario opzetten en opnieuw creëren

Onlangs hebben we ons ten doel gesteld om het prestatiepaneel beter te laten presteren. We wilden vooral dat grote hoeveelheden prestatiegegevens sneller zouden worden geladen. Dit is bijvoorbeeld het geval bij het profileren van langlopende of complexe processen of bij het vastleggen van zeer gedetailleerde gegevens. Om dit te bereiken was eerst inzicht nodig in hoe de applicatie presteerde en waarom deze zo presteerde. Dit werd bereikt door gebruik te maken van een profileringstool.

Zoals u wellicht weet, is DevTools zelf een webapplicatie. Als zodanig kan het worden geprofileerd met behulp van het paneel Prestaties . Om dit paneel zelf te profileren, kunt u DevTools openen en vervolgens een ander DevTools-exemplaar openen dat eraan is gekoppeld. Bij Google staat deze opstelling bekend als DevTools-on-DevTools .

Als de opstelling klaar is, moet het te profileren scenario opnieuw worden gemaakt en vastgelegd. Om verwarring te voorkomen, wordt naar het oorspronkelijke DevTools-venster verwezen als de " eerste DevTools-instantie", en naar het venster dat de eerste instantie inspecteert, wordt verwezen als de " tweede DevTools-instantie".

Een screenshot van een DevTools-instantie die de elementen in DevTools zelf inspecteert.
DevTools-on-DevTools: DevTools inspecteren met DevTools.

Op de tweede DevTools-instantie observeert het Prestatiepaneel (dat vanaf nu het perf-paneel zal worden genoemd) de eerste DevTools-instantie om het scenario opnieuw te creëren, waardoor een profiel wordt geladen.

Bij de tweede instantie van DevTools wordt een live-opname gestart, terwijl bij de eerste instantie een profiel wordt geladen vanuit een bestand op schijf. Er wordt een groot bestand geladen om de prestaties bij het verwerken van grote invoer nauwkeurig te profileren. Wanneer beide instanties klaar zijn met laden, worden de prestatieprofileringsgegevens (gewoonlijk een trace genoemd) gezien in de tweede DevTools-instantie van het perf-paneel dat een profiel laadt.

De uitgangssituatie: het identificeren van mogelijkheden voor verbetering

Nadat het laden was voltooid, werd het volgende op onze tweede perf-paneelinstantie waargenomen in de volgende schermafbeelding. Concentreer u op de activiteit van de hoofdthread, die zichtbaar is onder de track met het label Main . Het is te zien dat er vijf grote activiteitsgroepen in de vlammenkaart zijn. Deze bestaan ​​uit de taken waarbij het laden de meeste tijd kost. De totale tijd van deze taken bedroeg ongeveer 10 seconden . In de volgende schermafbeelding wordt het prestatiepaneel gebruikt om zich op elk van deze activiteitsgroepen te concentreren om te zien wat er te vinden is.

Een schermafbeelding van het prestatiepaneel in DevTools dat het laden van een prestatietracering in het prestatiepaneel van een ander DevTools-exemplaar inspecteert. Het laden van het profiel duurt ongeveer 10 seconden. Deze tijd is grotendeels verdeeld over vijf hoofdgroepen activiteiten.

Eerste activiteitsgroep: onnodig werk

Het werd duidelijk dat de eerste groep activiteiten bestaande code betrof die nog steeds actief was, maar niet echt nodig was. Kortom, alles onder het groene blokje met de naam processThreadEvents was verspilde moeite. Die was een snelle overwinning. Het verwijderen van die functieaanroep bespaarde ongeveer 1,5 seconde tijd. Koel!

Tweede activiteitengroep

Bij de tweede activiteitengroep was de oplossing niet zo eenvoudig als bij de eerste. De buildProfileCalls duurden ongeveer 0,5 seconde, en die taak kon niet worden vermeden.

Een schermafbeelding van het prestatiepaneel in DevTools dat een ander exemplaar van het prestatiepaneel inspecteert. Een taak die is gekoppeld aan de functie buildProfileCalls duurt ongeveer 0,5 seconde.

Uit nieuwsgierigheid hebben we de optie Geheugen in het perf-paneel ingeschakeld om verder onderzoek te doen, en we zagen dat de buildProfileCalls -activiteit ook veel geheugen gebruikte. Hier kunt u zien hoe de blauwe lijngrafiek plotseling verspringt rond de tijd dat buildProfileCalls wordt uitgevoerd, wat duidt op een mogelijk geheugenlek.

Een screenshot van de geheugenprofiler in DevTools die het geheugenverbruik van het prestatiepaneel beoordeelt. De inspecteur suggereert dat de functie buildProfileCalls verantwoordelijk is voor een geheugenlek.

Om dit vermoeden op te volgen, hebben we het Memory-paneel (een ander paneel in DevTools, anders dan de Memory-lade in het perf-paneel) gebruikt om dit te onderzoeken. Binnen het paneel Geheugen werd het profileringstype "Allocation sampling" geselecteerd, dat de heap-snapshot registreerde voor het perf-paneel dat het CPU-profiel laadde.

Een screenshot van de beginstatus van de geheugenprofiler. De optie 'allocation sampling' is gemarkeerd met een rood vakje en geeft aan dat deze optie het beste is voor JavaScript-geheugenprofilering.

De volgende schermafbeelding toont de heap-momentopname die is verzameld.

Een schermafbeelding van de geheugenprofiler, waarbij een geheugenintensieve set-gebaseerde bewerking is geselecteerd.

Uit deze heap-snapshot is gebleken dat de klasse Set veel geheugen in beslag nam. Door de handpunten te controleren, bleek dat we onnodig eigenschappen van het type Set toekenden aan objecten die in grote volumes waren gemaakt. Deze kosten liepen op en er werd veel geheugen verbruikt, tot het punt dat het gebruikelijk was dat de applicatie crashte bij grote invoer.

Sets zijn handig voor het opslaan van unieke items en bieden bewerkingen die gebruik maken van het unieke karakter van hun inhoud, zoals het ontdubbelen van datasets en het bieden van efficiëntere zoekacties. Deze functies waren echter niet nodig omdat de opgeslagen gegevens gegarandeerd uniek waren ten opzichte van de bron. Als zodanig waren sets in de eerste plaats niet nodig. Om de geheugentoewijzing te verbeteren, is het eigenschapstype gewijzigd van een Set in een gewone array. Na het toepassen van deze wijziging werd nog een heap-snapshot gemaakt en werd een verminderde geheugentoewijzing waargenomen. Ondanks dat er met deze wijziging geen aanzienlijke snelheidsverbeteringen werden bereikt, was het secundaire voordeel dat de applicatie minder vaak crashte.

Een screenshot van de geheugenprofiler. De voorheen geheugenintensieve Set-gebaseerde bewerking is gewijzigd om een ​​gewone array te gebruiken, waardoor de geheugenkosten aanzienlijk zijn verlaagd.

Derde activiteitsgroep: afwegingen tussen datastructuren

Het derde gedeelte is eigenaardig: je kunt in de vlammengrafiek zien dat het bestaat uit smalle maar hoge kolommen, die diepe functieaanroepen aanduiden, en in dit geval diepe recursies. In totaal duurde dit gedeelte ongeveer 1,4 seconden. Door naar de onderkant van deze sectie te kijken, werd het duidelijk dat de breedte van deze kolommen werd bepaald door de duur van één functie: appendEventAtLevel , wat suggereerde dat dit een knelpunt zou kunnen zijn

Bij de implementatie van de functie appendEventAtLevel viel één ding op. Voor elke afzonderlijke gegevensinvoer in de invoer (die in de code bekend staat als de "gebeurtenis") werd een item toegevoegd aan een kaart die de verticale positie van de tijdlijninvoeren bijhield. Dit was problematisch, omdat de hoeveelheid spullen die werd opgeslagen erg groot was. Kaarten zijn snel voor op sleutels gebaseerde zoekopdrachten, maar dit voordeel is niet gratis. Naarmate een kaart groter wordt, kan het toevoegen van gegevens eraan bijvoorbeeld duur worden door het opnieuw bewerken ervan. Deze kosten worden merkbaar wanneer grote hoeveelheden items achtereenvolgens aan de kaart worden toegevoegd.

/**
 * Adds an event to the flame chart data at a defined vertical level.
 */
function appendEventAtLevel (event, level) {
  // ...

  const index = data.length;
  data.push(event);
  this.indexForEventMap.set(event, index);

  // ...
}

We hebben met een andere aanpak geëxperimenteerd, waarbij we niet voor elk item in de vlammenkaart een item op een kaart hoefden toe te voegen. De verbetering was aanzienlijk en bevestigde dat het knelpunt inderdaad verband hield met de overhead die werd opgelopen door het toevoegen van alle gegevens aan de kaart. De tijd die de activiteitengroep nodig had, kromp van ongeveer 1,4 seconden naar ongeveer 200 milliseconden.

Voor:

Een schermafbeelding van het prestatiepaneel voordat er optimalisaties werden aangebracht aan de functie appendEventAtLevel. De totale tijd voor het uitvoeren van de functie was 1.372,51 milliseconden.

Na:

Een schermafbeelding van het prestatiepaneel nadat er optimalisaties zijn aangebracht aan de functie appendEventAtLevel. De totale tijd voor het uitvoeren van de functie was 207,2 milliseconden.

Vierde activiteitsgroep: het uitstellen van niet-kritiek werk en het cachen van gegevens om dubbel werk te voorkomen

Als u inzoomt op dit venster, ziet u dat er twee vrijwel identieke blokken met functieaanroepen zijn. Door naar de naam van de aangeroepen functies te kijken, kun je concluderen dat deze blokken bestaan ​​uit code die bomen bouwt (bijvoorbeeld met namen als refreshTree of buildChildren ). In feite is de bijbehorende code degene die de boomweergaven in de onderste la van het paneel creëert. Wat interessant is, is dat deze boomstructuurweergaven niet direct na het laden worden weergegeven. In plaats daarvan moet de gebruiker een boomstructuur selecteren (de tabbladen "Bottom-up", "Call Tree" en "Event Log" in de lade) om de bomen weer te geven. Bovendien werd, zoals u uit de schermafbeelding kunt zien, het proces voor het bouwen van de boom tweemaal uitgevoerd.

Een screenshot van het prestatiepaneel met verschillende, repetitieve taken die worden uitgevoerd, zelfs als ze niet nodig zijn. Deze taken kunnen worden uitgesteld om op verzoek te worden uitgevoerd, in plaats van van tevoren.

Er zijn twee problemen die we met deze afbeelding hebben geïdentificeerd:

  1. Een niet-kritieke taak belemmerde de uitvoering van de laadtijd. Gebruikers hebben niet altijd de uitvoer ervan nodig. Als zodanig is de taak niet kritisch voor het laden van het profiel.
  2. Het resultaat van deze taken is niet in de cache opgeslagen. Daarom zijn de bomen twee keer berekend, ondanks dat de gegevens niet veranderden.

We zijn begonnen met het uitstellen van de boomberekening tot het moment waarop de gebruiker de boomstructuur handmatig opende. Alleen dan is het de moeite waard om de prijs te betalen voor het maken van deze bomen. De totale tijd om dit twee keer uit te voeren was ongeveer 3,4 seconden, dus het uitstellen ervan maakte een aanzienlijk verschil in de laadtijd. We onderzoeken nog steeds of we dit soort taken ook in de cache kunnen opslaan.

Vijfde activiteitsgroep: vermijd waar mogelijk complexe oproephiërarchieën

Als we goed naar deze groep keken, werd het duidelijk dat een bepaalde oproepketen herhaaldelijk werd aangeroepen. Hetzelfde patroon verscheen 6 keer op verschillende plaatsen in de vlammenkaart, en de totale duur van dit venster was ongeveer 2,4 seconden!

Een screenshot van het prestatiepaneel met zes afzonderlijke functieaanroepen voor het genereren van dezelfde traceerminimap, die elk diepe call-stacks hebben.

De gerelateerde code die meerdere keren wordt aangeroepen, is het deel dat de gegevens verwerkt die moeten worden weergegeven op de "minimap" (het overzicht van de tijdlijnactiviteit bovenaan het paneel). Het was niet duidelijk waarom het meerdere keren gebeurde, maar het hoefde zeker geen 6 keer te gebeuren! In feite zou de uitvoer van de code actueel moeten blijven als er geen ander profiel wordt geladen. In theorie zou de code slechts één keer moeten worden uitgevoerd.

Bij onderzoek bleek dat de gerelateerde code werd aangeroepen als gevolg van het feit dat meerdere delen in de laadpijplijn direct of indirect de functie aanroepen die de minimap berekent. Dit komt omdat de complexiteit van de aanroepgrafiek van het programma in de loop van de tijd is geëvolueerd en er onbewust meer afhankelijkheden aan deze code zijn toegevoegd. Er bestaat geen snelle oplossing voor dit probleem. De manier om dit op te lossen hangt af van de architectuur van de codebase in kwestie. In ons geval moesten we de complexiteit van de oproephiërarchie een beetje verminderen en een controle toevoegen om te voorkomen dat de code werd uitgevoerd als de invoergegevens ongewijzigd bleven. Nadat we dit hadden geïmplementeerd, kregen we dit beeld van de tijdlijn:

Een schermafbeelding van het prestatiepaneel waarin de zes afzonderlijke functieaanroepen voor het genereren van dezelfde traceerminimap zijn teruggebracht tot slechts twee keer.

Houd er rekening mee dat de uitvoering van de minimap-rendering twee keer plaatsvindt, en niet één keer. Dit komt omdat er voor elk profiel twee minimaps worden getekend: één voor het overzicht bovenaan het paneel, en één voor het vervolgkeuzemenu dat het momenteel zichtbare profiel uit de geschiedenis selecteert (elk item in dit menu bevat een overzicht van het geselecteerde profiel). Niettemin hebben deze twee exact dezelfde inhoud, dus de een zou voor de ander moeten kunnen worden hergebruikt.

Omdat deze minimaps beide afbeeldingen zijn die op een canvas zijn getekend, was het een kwestie van het drawImage canvas-hulpprogramma gebruiken en vervolgens de code slechts één keer uitvoeren om wat extra tijd te besparen. Als gevolg van deze inspanning werd de duur van de groep teruggebracht van 2,4 seconden naar 140 milliseconden.

Conclusie

Nadat al deze verbeteringen waren aangebracht (en hier en daar nog een paar kleinere), zag de wijziging van de laadtijdlijn van het profiel er als volgt uit:

Voor:

Een schermafbeelding van het prestatiepaneel waarin het laden van sporen vóór optimalisaties wordt weergegeven. Het proces duurde ongeveer tien seconden.

Na:

Een schermafbeelding van het prestatiepaneel waarin het laden van sporen na optimalisaties wordt weergegeven. Het proces duurt nu ongeveer twee seconden.

De laadtijd na de verbeteringen bedroeg 2 seconden, wat betekent dat met relatief weinig inspanning een verbetering van ongeveer 80% werd bereikt, aangezien het grootste deel van wat werd gedaan uit snelle oplossingen bestond. Natuurlijk was het van cruciaal belang om in eerste instantie goed te identificeren wat te doen, en het perf-panel was hiervoor het juiste hulpmiddel.

Het is ook belangrijk om te benadrukken dat deze cijfers specifiek zijn voor een profiel dat als studieonderwerp wordt gebruikt. Het profiel was voor ons interessant omdat het bijzonder groot was. Omdat de verwerkingspijplijn echter voor elk profiel hetzelfde is, geldt de aanzienlijke verbetering die wordt bereikt voor elk profiel dat in het perf-paneel wordt geladen.

Afhaalrestaurants

Er zijn enkele lessen die u uit deze resultaten kunt trekken op het gebied van prestatie-optimalisatie van uw applicatie:

1. Maak gebruik van profileringstools om prestatiepatronen tijdens runtime te identificeren

Profileringstools zijn ongelooflijk handig om te begrijpen wat er in uw applicatie gebeurt terwijl deze actief is, vooral om mogelijkheden te identificeren om de prestaties te verbeteren. Het prestatiepaneel in Chrome DevTools is een geweldige optie voor webapplicaties, omdat het de eigen webprofileringstool in de browser is en actief wordt onderhouden om up-to-date te zijn met de nieuwste webplatformfuncties. Bovendien gaat het nu aanzienlijk sneller! 😉

Gebruik voorbeelden die kunnen worden gebruikt als representatieve werklasten en kijk wat u kunt vinden!

2. Vermijd complexe oproephiërarchieën

Zorg er indien mogelijk voor dat uw oproepgrafiek niet te ingewikkeld wordt. Met complexe oproephiërarchieën is het gemakkelijk om prestatieregressies te introduceren en moeilijk te begrijpen waarom uw code werkt zoals deze is, waardoor het moeilijk wordt om verbeteringen aan te brengen.

3. Identificeer onnodig werk

Het komt vaak voor dat verouderde codebases code bevatten die niet langer nodig is. In ons geval nam verouderde en onnodige code een aanzienlijk deel van de totale laadtijd in beslag. Het verwijderen ervan was het laaghangende fruit.

4. Gebruik datastructuren op de juiste manier

Gebruik datastructuren om de prestaties te optimaliseren, maar begrijp ook de kosten en afwegingen die elk type datastructuur met zich meebrengt bij het beslissen welke u wilt gebruiken. Dit is niet alleen de ruimtecomplexiteit van de datastructuur zelf, maar ook de tijdcomplexiteit van de toepasselijke bewerkingen.

5. Resultaten in de cache opslaan om dubbel werk voor complexe of repetitieve bewerkingen te voorkomen

Als de uitvoering kostbaar is, is het zinvol om de resultaten op te slaan voor de volgende keer dat deze nodig zijn. Het is ook zinvol om dit te doen als de operatie vele malen wordt uitgevoerd, zelfs als elke afzonderlijke keer niet bijzonder kostbaar is.

6. Stel niet-kritiek werk uit

Als de uitvoer van een taak niet onmiddellijk nodig is en de uitvoering van de taak het kritieke pad verlengt, overweeg dan om de taak uit te stellen door deze lui aan te roepen wanneer de uitvoer daadwerkelijk nodig is.

7. Gebruik efficiënte algoritmen voor grote invoer

Voor grote inputs worden algoritmen voor optimale tijdcomplexiteit cruciaal. We hebben in dit voorbeeld niet naar deze categorie gekeken, maar het belang ervan kan nauwelijks worden overschat.

8. Bonus: benchmark uw pijpleidingen

Om ervoor te zorgen dat uw evoluerende code snel blijft, is het verstandig om het gedrag te monitoren en te vergelijken met standaarden. Op deze manier identificeert u proactief regressies en verbetert u de algehele betrouwbaarheid, waardoor u succes op de lange termijn kunt behalen.

,

Andrés Olivares
Andrés Olivares
Nancy Li
Nancy Li

Ongeacht welk type applicatie u ontwikkelt, het optimaliseren van de prestaties en het garanderen dat deze snel laadt en soepele interacties biedt, is van cruciaal belang voor de gebruikerservaring en het succes van de applicatie. Eén manier om dit te doen is door de activiteit van een applicatie te inspecteren met behulp van profileringstools om te zien wat er onder de motorkap gebeurt terwijl deze gedurende een bepaalde periode wordt uitgevoerd. Het Prestatiepaneel in DevTools is een geweldige profileringstool om de prestaties van webapplicaties te analyseren en optimaliseren. Als uw app in Chrome draait, krijgt u een gedetailleerd visueel overzicht van wat de browser doet terwijl uw applicatie wordt uitgevoerd. Als u deze activiteit begrijpt, kunt u patronen, knelpunten en prestatieproblemen identificeren waarop u actie kunt ondernemen om de prestaties te verbeteren.

Het volgende voorbeeld begeleidt u bij het gebruik van het paneel Prestaties .

Ons profileringsscenario opzetten en opnieuw creëren

Onlangs hebben we ons ten doel gesteld om het prestatiepaneel beter te laten presteren. We wilden vooral dat grote hoeveelheden prestatiegegevens sneller zouden worden geladen. Dit is bijvoorbeeld het geval bij het profileren van langlopende of complexe processen of bij het vastleggen van zeer gedetailleerde gegevens. Om dit te bereiken was eerst inzicht nodig in hoe de applicatie presteerde en waarom deze zo presteerde. Dit werd bereikt door gebruik te maken van een profileringstool.

Zoals u wellicht weet, is DevTools zelf een webapplicatie. Als zodanig kan het worden geprofileerd met behulp van het paneel Prestaties . Om dit paneel zelf te profileren, kunt u DevTools openen en vervolgens een ander DevTools-exemplaar openen dat eraan is gekoppeld. Bij Google staat deze opstelling bekend als DevTools-on-DevTools .

Als de opstelling klaar is, moet het te profileren scenario opnieuw worden gemaakt en vastgelegd. Om verwarring te voorkomen, wordt naar het oorspronkelijke DevTools-venster verwezen als de " eerste DevTools-instantie", en naar het venster dat de eerste instantie inspecteert, wordt verwezen als de " tweede DevTools-instantie".

Een screenshot van een DevTools-instantie die de elementen in DevTools zelf inspecteert.
DevTools-on-DevTools: DevTools inspecteren met DevTools.

Op de tweede DevTools-instantie observeert het Prestatiepaneel (dat vanaf nu het perf-paneel zal worden genoemd) de eerste DevTools-instantie om het scenario opnieuw te creëren, waardoor een profiel wordt geladen.

Bij de tweede instantie van DevTools wordt een live-opname gestart, terwijl bij de eerste instantie een profiel wordt geladen vanuit een bestand op schijf. Er wordt een groot bestand geladen om de prestaties bij het verwerken van grote invoer nauwkeurig te profileren. Wanneer beide instanties klaar zijn met laden, worden de prestatieprofileringsgegevens (gewoonlijk een trace genoemd) gezien in de tweede DevTools-instantie van het perf-paneel dat een profiel laadt.

De uitgangssituatie: het identificeren van mogelijkheden voor verbetering

Nadat het laden was voltooid, werd het volgende op onze tweede perf-paneelinstantie waargenomen in de volgende schermafbeelding. Concentreer u op de activiteit van de hoofdthread, die zichtbaar is onder de track met het label Main . Het is te zien dat er vijf grote activiteitsgroepen in de vlammenkaart zijn. Deze bestaan ​​uit de taken waarbij het laden de meeste tijd kost. De totale tijd van deze taken bedroeg ongeveer 10 seconden . In de volgende schermafbeelding wordt het prestatiepaneel gebruikt om zich op elk van deze activiteitsgroepen te concentreren om te zien wat er te vinden is.

Een schermafbeelding van het prestatiepaneel in DevTools dat het laden van een prestatietracering in het prestatiepaneel van een ander DevTools-exemplaar inspecteert. Het laden van het profiel duurt ongeveer 10 seconden. Deze tijd is grotendeels verdeeld over vijf hoofdgroepen activiteiten.

Eerste activiteitsgroep: onnodig werk

Het werd duidelijk dat de eerste groep activiteiten bestaande code betrof die nog steeds actief was, maar niet echt nodig was. Kortom, alles onder het groene blokje met de naam processThreadEvents was verspilde moeite. Die was een snelle overwinning. Het verwijderen van die functieaanroep bespaarde ongeveer 1,5 seconde tijd. Koel!

Tweede activiteitengroep

Bij de tweede activiteitengroep was de oplossing niet zo eenvoudig als bij de eerste. De buildProfileCalls duurden ongeveer 0,5 seconde, en die taak kon niet worden vermeden.

Een schermafbeelding van het prestatiepaneel in DevTools dat een ander exemplaar van het prestatiepaneel inspecteert. Een taak die is gekoppeld aan de functie buildProfileCalls duurt ongeveer 0,5 seconde.

Uit nieuwsgierigheid hebben we de optie Geheugen in het perf-paneel ingeschakeld om verder onderzoek te doen, en we zagen dat de buildProfileCalls -activiteit ook veel geheugen gebruikte. Hier kunt u zien hoe de blauwe lijngrafiek plotseling verspringt rond de tijd dat buildProfileCalls wordt uitgevoerd, wat duidt op een mogelijk geheugenlek.

Een screenshot van de geheugenprofiler in DevTools die het geheugenverbruik van het prestatiepaneel beoordeelt. De inspecteur suggereert dat de functie buildProfileCalls verantwoordelijk is voor een geheugenlek.

Om dit vermoeden op te volgen, hebben we het Memory-paneel (een ander paneel in DevTools, anders dan de Memory-lade in het perf-paneel) gebruikt om dit te onderzoeken. Binnen het paneel Geheugen werd het profileringstype "Allocation sampling" geselecteerd, dat de heap-snapshot registreerde voor het perf-paneel dat het CPU-profiel laadde.

Een screenshot van de beginstatus van de geheugenprofiler. De optie 'allocation sampling' is gemarkeerd met een rood vakje en geeft aan dat deze optie het beste is voor JavaScript-geheugenprofilering.

De volgende schermafbeelding toont de heap-momentopname die is verzameld.

Een schermafbeelding van de geheugenprofiler, waarbij een geheugenintensieve set-gebaseerde bewerking is geselecteerd.

Uit deze heap-snapshot is gebleken dat de klasse Set veel geheugen in beslag nam. Door de handpunten te controleren, bleek dat we onnodig eigenschappen van het type Set toekenden aan objecten die in grote volumes waren gemaakt. Deze kosten liepen op en er werd veel geheugen verbruikt, tot het punt dat het gebruikelijk was dat de applicatie crashte bij grote invoer.

Sets zijn handig voor het opslaan van unieke items en bieden bewerkingen die gebruik maken van het unieke karakter van hun inhoud, zoals het ontdubbelen van datasets en het bieden van efficiëntere zoekacties. Deze functies waren echter niet nodig omdat de opgeslagen gegevens gegarandeerd uniek waren ten opzichte van de bron. Als zodanig waren sets in de eerste plaats niet nodig. Om de geheugentoewijzing te verbeteren, is het eigenschapstype gewijzigd van een Set in een gewone array. Na het toepassen van deze wijziging werd nog een heap-snapshot gemaakt en werd een verminderde geheugentoewijzing waargenomen. Ondanks dat er met deze wijziging geen aanzienlijke snelheidsverbeteringen werden bereikt, was het secundaire voordeel dat de applicatie minder vaak crashte.

Een screenshot van de geheugenprofiler. De voorheen geheugenintensieve Set-gebaseerde bewerking is gewijzigd om een ​​gewone array te gebruiken, waardoor de geheugenkosten aanzienlijk zijn verlaagd.

Derde activiteitsgroep: afwegingen tussen datastructuren

Het derde gedeelte is eigenaardig: je kunt in de vlammengrafiek zien dat het bestaat uit smalle maar hoge kolommen, die diepe functieaanroepen aanduiden, en in dit geval diepe recursies. In totaal duurde dit gedeelte ongeveer 1,4 seconden. Door naar de onderkant van deze sectie te kijken, werd het duidelijk dat de breedte van deze kolommen werd bepaald door de duur van één functie: appendEventAtLevel , wat suggereerde dat dit een knelpunt zou kunnen zijn

Bij de implementatie van de functie appendEventAtLevel viel één ding op. Voor elke afzonderlijke gegevensinvoer in de invoer (die in de code bekend staat als de "gebeurtenis") werd een item toegevoegd aan een kaart die de verticale positie van de tijdlijninvoeren bijhield. Dit was problematisch, omdat de hoeveelheid spullen die werd opgeslagen erg groot was. Kaarten zijn snel voor op sleutels gebaseerde zoekopdrachten, maar dit voordeel is niet gratis. Naarmate een kaart groter wordt, kan het toevoegen van gegevens eraan bijvoorbeeld duur worden door het opnieuw bewerken ervan. Deze kosten worden merkbaar wanneer grote hoeveelheden items achtereenvolgens aan de kaart worden toegevoegd.

/**
 * Adds an event to the flame chart data at a defined vertical level.
 */
function appendEventAtLevel (event, level) {
  // ...

  const index = data.length;
  data.push(event);
  this.indexForEventMap.set(event, index);

  // ...
}

We hebben met een andere aanpak geëxperimenteerd, waarbij we niet voor elk item in de vlammenkaart een item op een kaart hoefden toe te voegen. De verbetering was aanzienlijk en bevestigde dat het knelpunt inderdaad verband hield met de overhead die werd opgelopen door het toevoegen van alle gegevens aan de kaart. De tijd die de activiteitengroep nodig had, kromp van ongeveer 1,4 seconden naar ongeveer 200 milliseconden.

Voor:

Een schermafbeelding van het prestatiepaneel voordat er optimalisaties werden aangebracht aan de functie appendEventAtLevel. De totale tijd voor het uitvoeren van de functie was 1.372,51 milliseconden.

Na:

Een schermafbeelding van het prestatiepaneel nadat er optimalisaties zijn aangebracht aan de functie appendEventAtLevel. De totale tijd voor het uitvoeren van de functie was 207,2 milliseconden.

Vierde activiteitsgroep: het uitstellen van niet-kritiek werk en het cachen van gegevens om dubbel werk te voorkomen

Als u inzoomt op dit venster, ziet u dat er twee vrijwel identieke blokken met functieaanroepen zijn. Door naar de naam van de aangeroepen functies te kijken, kun je concluderen dat deze blokken bestaan ​​uit code die bomen bouwt (bijvoorbeeld met namen als refreshTree of buildChildren ). In feite is de bijbehorende code degene die de boomweergaven in de onderste la van het paneel creëert. Wat interessant is, is dat deze boomstructuurweergaven niet direct na het laden worden weergegeven. In plaats daarvan moet de gebruiker een boomstructuur selecteren (de tabbladen "Bottom-up", "Call Tree" en "Event Log" in de lade) om de bomen weer te geven. Bovendien werd, zoals u uit de schermafbeelding kunt zien, het proces voor het bouwen van de boom tweemaal uitgevoerd.

Een screenshot van het prestatiepaneel met verschillende, repetitieve taken die worden uitgevoerd, zelfs als ze niet nodig zijn. Deze taken kunnen worden uitgesteld om op verzoek te worden uitgevoerd, in plaats van van tevoren.

Er zijn twee problemen die we met deze afbeelding hebben geïdentificeerd:

  1. Een niet-kritieke taak belemmerde de uitvoering van de laadtijd. Gebruikers hebben niet altijd de uitvoer ervan nodig. Als zodanig is de taak niet kritisch voor het laden van het profiel.
  2. Het resultaat van deze taken is niet in de cache opgeslagen. Daarom zijn de bomen twee keer berekend, ondanks dat de gegevens niet veranderden.

We zijn begonnen met het uitstellen van de boomberekening tot het moment waarop de gebruiker de boomstructuur handmatig opende. Alleen dan is het de moeite waard om de prijs te betalen voor het maken van deze bomen. De totale tijd om dit twee keer uit te voeren was ongeveer 3,4 seconden, dus het uitstellen ervan maakte een aanzienlijk verschil in de laadtijd. We onderzoeken nog steeds of we dit soort taken ook in de cache kunnen opslaan.

Vijfde activiteitsgroep: vermijd waar mogelijk complexe oproephiërarchieën

Als we goed naar deze groep keken, werd het duidelijk dat een bepaalde oproepketen herhaaldelijk werd aangeroepen. Hetzelfde patroon verscheen 6 keer op verschillende plaatsen in de vlammenkaart, en de totale duur van dit venster was ongeveer 2,4 seconden!

Een screenshot van het prestatiepaneel met zes afzonderlijke functieaanroepen voor het genereren van dezelfde traceerminimap, die elk diepe call-stacks hebben.

De gerelateerde code die meerdere keren wordt aangeroepen, is het deel dat de gegevens verwerkt die moeten worden weergegeven op de "minimap" (het overzicht van de tijdlijnactiviteit bovenaan het paneel). Het was niet duidelijk waarom het meerdere keren gebeurde, maar het hoefde zeker geen 6 keer te gebeuren! In feite zou de uitvoer van de code actueel moeten blijven als er geen ander profiel wordt geladen. In theorie zou de code slechts één keer moeten worden uitgevoerd.

Bij onderzoek bleek dat de gerelateerde code werd aangeroepen als gevolg van het feit dat meerdere delen in de laadpijplijn direct of indirect de functie aanroepen die de minimap berekent. Dit komt omdat de complexiteit van de aanroepgrafiek van het programma in de loop van de tijd is geëvolueerd en er onbewust meer afhankelijkheden aan deze code zijn toegevoegd. Er bestaat geen snelle oplossing voor dit probleem. De manier om dit op te lossen hangt af van de architectuur van de codebase in kwestie. In ons geval moesten we de complexiteit van de oproephiërarchie een beetje verminderen en een controle toevoegen om te voorkomen dat de code werd uitgevoerd als de invoergegevens ongewijzigd bleven. Nadat we dit hadden geïmplementeerd, kregen we dit beeld van de tijdlijn:

Een schermafbeelding van het prestatiepaneel waarin de zes afzonderlijke functieaanroepen voor het genereren van dezelfde traceerminimap zijn teruggebracht tot slechts twee keer.

Houd er rekening mee dat de uitvoering van de minimap-rendering twee keer plaatsvindt, en niet één keer. Dit komt omdat er voor elk profiel twee minimaps worden getekend: één voor het overzicht bovenaan het paneel, en één voor het vervolgkeuzemenu dat het momenteel zichtbare profiel uit de geschiedenis selecteert (elk item in dit menu bevat een overzicht van het geselecteerde profiel). Niettemin hebben deze twee exact dezelfde inhoud, dus de een zou voor de ander moeten kunnen worden hergebruikt.

Omdat deze minimaps beide afbeeldingen zijn die op een canvas zijn getekend, was het een kwestie van het drawImage canvas-hulpprogramma gebruiken en vervolgens de code slechts één keer uitvoeren om wat extra tijd te besparen. Als gevolg van deze inspanning werd de duur van de groep teruggebracht van 2,4 seconden naar 140 milliseconden.

Conclusie

Nadat al deze verbeteringen waren aangebracht (en hier en daar nog een paar kleinere), zag de wijziging van de laadtijdlijn van het profiel er als volgt uit:

Voor:

Een schermafbeelding van het prestatiepaneel waarin het laden van sporen vóór optimalisaties wordt weergegeven. Het proces duurde ongeveer tien seconden.

Na:

Een schermafbeelding van het prestatiepaneel waarin het laden van sporen na optimalisaties wordt weergegeven. Het proces duurt nu ongeveer twee seconden.

De laadtijd na de verbeteringen bedroeg 2 seconden, wat betekent dat met relatief weinig inspanning een verbetering van ongeveer 80% werd bereikt, aangezien het grootste deel van wat werd gedaan uit snelle oplossingen bestond. Natuurlijk was het van cruciaal belang om in eerste instantie goed te identificeren wat te doen, en het perf-panel was hiervoor het juiste hulpmiddel.

Het is ook belangrijk om te benadrukken dat deze cijfers specifiek zijn voor een profiel dat als studieonderwerp wordt gebruikt. Het profiel was voor ons interessant omdat het bijzonder groot was. Omdat de verwerkingspijplijn echter voor elk profiel hetzelfde is, geldt de aanzienlijke verbetering die wordt bereikt voor elk profiel dat in het perf-paneel wordt geladen.

Afhaalrestaurants

Er zijn enkele lessen die u uit deze resultaten kunt trekken op het gebied van prestatie-optimalisatie van uw applicatie:

1. Maak gebruik van profileringstools om prestatiepatronen tijdens runtime te identificeren

Profileringstools zijn ongelooflijk nuttig om te begrijpen wat er in uw applicatie gebeurt terwijl deze actief is, vooral om kansen te identificeren om de prestaties te verbeteren. Het prestatiepaneel in Chrome Devtools is een geweldige optie voor webapplicaties, omdat het de native webprofileringstool in de browser is en het actief wordt onderhouden om up-to-date te zijn met de nieuwste webplatformfuncties. Het is nu ook aanzienlijk sneller! 😉

Gebruik monsters die kunnen worden gebruikt als representatieve werklast en kijk wat u kunt vinden!

2. Vermijd complexe oproephiërarchieën

Vermijd indien mogelijk uw oproepgrafiek te ingewikkeld. Met complexe oproephiërarchieën is het gemakkelijk om prestatieregressies te introduceren en moeilijk te begrijpen waarom uw code wordt uitgevoerd zoals het is, waardoor het moeilijk is om verbeteringen te landen.

3. Identificeer onnodig werk

Het is gebruikelijk dat verouderende codebases code bevatten die niet langer nodig is. In ons geval namen Legacy en onnodige code een aanzienlijk deel van de totale laadtijd aan. Het verwijderen was het laagsthangende fruit.

4. Gebruik gegevensstructuren op de juiste manier

Gebruik gegevensstructuren om de prestaties te optimaliseren, maar begrijp ook de kosten en afwegingen die elk type gegevensstructuur met zich meebrengt bij het beslissen welke te gebruiken. Dit is niet alleen de ruimtecomplexiteit van de gegevensstructuur zelf, maar ook de tijdcomplexiteit van de toepasselijke bewerkingen.

5. Cache -resultaten om dubbele werk te voorkomen voor complexe of repetitieve bewerkingen

Als de bewerking kostbaar is om uit te voeren, is het logisch om zijn resultaten op te slaan voor de volgende keer dat deze nodig is. Het is ook logisch om dit te doen als de bewerking vele malen wordt gedaan - zelfs als elke individuele tijd niet bijzonder duur is.

6. Stel niet-kritisch werk uit

Als de uitvoer van een taak niet onmiddellijk nodig is en de taakuitvoering het kritieke pad uitbreidt, overweeg dan om het uit te stellen door het lui te bellen wanneer de uitvoer daadwerkelijk nodig is.

7. Gebruik efficiënte algoritmen op grote inputs

Voor grote input worden optimale tijdcomplexiteitsalgoritmen cruciaal. We hebben in dit voorbeeld niet in deze categorie gekeken, maar hun belang kan nauwelijks worden overschat.

8. Bonus: Benchmark uw pijpleidingen

Om ervoor te zorgen dat uw evoluerende code snel blijft, is het verstandig om het gedrag te controleren en te vergelijken met normen. Op deze manier identificeert u proactief regressies en verbetert u de algehele betrouwbaarheid, waardoor u op langetermijnsucces wordt ingesteld.

,

Andrés Olivares
Andrés Olivares
Nancy Li
Nancy Li

Ongeacht wat voor soort applicatie u ontwikkelt, de prestaties van de prestaties te optimaliseren en ervoor te zorgen dat deze snel wordt geladen en soepele interacties biedt, is van cruciaal belang voor de gebruikerservaring en het succes van de toepassing. Een manier om dit te doen is om de activiteit van een applicatie te inspecteren door profileringstools te gebruiken om te zien wat er onder de motorkap gebeurt terwijl deze draait tijdens een tijdvenster. Het prestatiepaneel in Devtools is een geweldig profileringstool om de prestaties van webtoepassingen te analyseren en te optimaliseren. Als uw app in Chrome wordt uitgevoerd, geeft het u een gedetailleerd visueel overzicht van wat de browser doet als uw applicatie wordt uitgevoerd. Inzicht in deze activiteit kan u helpen patronen, knelpunten en prestatiehotspots te identificeren waarop u kunt handelen om de prestaties te verbeteren.

Het volgende voorbeeld loopt u echter met behulp van het uitvoeringspaneel .

Ons profileringscenario opzetten en opnieuw maken

Onlangs hebben we een doel gesteld om het prestatiepaneel performanter te maken. In het bijzonder wilden we dat het grote hoeveelheden prestatiegegevens sneller zou laden. Dit is bijvoorbeeld het geval bij het profileren van langlopende of complexe processen of het vastleggen van gegevens met hoge granulariteit. Om dit te bereiken, was een begrip van hoe de toepassing presteerde en waarom deze op die manier presteerde eerst nodig was, wat werd bereikt door een profileringstool te gebruiken.

Zoals u misschien weet, is Devtools zelf een webtoepassing. Als zodanig kan het worden geprofileerd met behulp van het prestatiepaneel . Om dit paneel zelf te profileren, kunt u Devtools openen en vervolgens een andere DevTools -instantie openen die eraan is gekoppeld. Bij Google staat deze opstelling bekend als DevTools-on-Devtools .

Met de opstelling klaar, moet het te profileren scenario worden nagebouwd en opgenomen. Om verwarring te voorkomen, wordt het originele devTools -venster de " eerste devtools -instantie" genoemd, en het venster dat de eerste instantie inspecteert, wordt de " tweede devtools -instantie" genoemd.

Een screenshot van een DevTools -exemplaar die de elementen in Devtools zelf inspecteert.
DevTools-on-Devtools: Devtools inspecteren met DevTools.

In het tweede DevTools -exemplaar wordt het prestatiepaneel - dat vanaf hier het perf -paneel zal worden genoemd - de eerste DevTools -instantie om het scenario opnieuw te maken, dat een profiel laadt.

In de tweede DevTools -instantie wordt een live -opname gestart, terwijl in eerste instantie een profiel wordt geladen vanuit een bestand op schijf. Een groot bestand wordt geladen om de prestaties van het verwerken van grote ingangen nauwkeurig te profileren. Wanneer beide instanties het laden voltooien, worden de prestatieprofileringsgegevens - meestal een spoor genoemd - gezien in het tweede DevTools -exemplaar van het Perf -paneel dat een profiel laadt.

De initiële staat: kansen voor verbetering identificeren

Nadat het laden is voltooid, werd het volgende op onze tweede Perf -paneelinstantie waargenomen in de volgende screenshot. Focus op de activiteit van de hoofdthread, die zichtbaar is onder de baan met het label Main . Het is te zien dat er vijf grote groepen activiteiten in de vlamgrafiek zijn. Deze bestaan ​​uit de taken waarbij het laden de meeste tijd duurt. De totale tijd van deze taken was ongeveer 10 seconden . In de volgende screenshot wordt het prestatiepaneel gebruikt om zich op elk van deze activiteitengroepen te concentreren om te zien wat te vinden is.

Een screenshot van het prestatiepaneel in Devtools die het laden van een prestatietrace in het uitvoeringspaneel van een andere DevTools -instantie inspecteren. Het profiel duurt ongeveer 10 seconden om te laden. Deze keer wordt meestal verdeeld over vijf hoofdgroepen activiteiten.

Eerste activiteitsgroep: onnodig werk

Het werd duidelijk dat de eerste groep activiteiten legacy code was die nog steeds liep, maar niet echt nodig was. Kortom, alles onder het Green Block Labeled processThreadEvents was verspilde inspanning. Die was een snelle overwinning. Het verwijderen van die functieoproep die ongeveer 1,5 seconden tijd is opgeslagen. Koel!

Tweede activiteitsgroep

In de tweede activiteitsgroep was de oplossing niet zo eenvoudig als bij de eerste. De buildProfileCalls duurden ongeveer 0,5 seconden, en die taak was niet iets dat kon worden vermeden.

Een screenshot van het prestatiepaneel in Devtools die een ander exemplaar van het prestatiepaneel inspecteert. Een taak geassocieerd met de functie BuildProfilecalls duurt ongeveer 0,5 seconden.

Uit nieuwsgierigheid hebben we de geheugenoptie in het Perf -paneel ingeschakeld om verder te onderzoeken en zagen we dat de buildProfileCalls -activiteit ook veel geheugen gebruikte. Hier kunt u zien hoe de blauwe lijngrafiek plotseling rond de tijd dat buildProfileCalls wordt uitgevoerd, wordt uitgevoerd, wat een potentieel geheugenlek suggereert.

Een screenshot van de geheugenprofiler in Devtools die geheugenverbruik van het prestatiepaneel beoordelen. De inspecteur suggereert dat de functie BuildProfilecalls verantwoordelijk is voor een geheugenlek.

Om dit vermoeden op te volgen, gebruikten we het geheugenpaneel (een ander paneel in Devtools, anders dan de geheugenlade in het Perf -paneel) om te onderzoeken. Binnen het geheugenpaneel werd het "toewijzingsbemonstering" -profileringstype geselecteerd, dat de heap -snapshot registreerde voor het Perf -paneel dat het CPU -profiel laadde.

Een screenshot van de initiële status van de geheugenprofiler. De optie 'Allocatie bemonstering' wordt gemarkeerd met een rode doos en het geeft aan dat deze optie het beste is voor JavaScript -geheugenprofilering.

De volgende screenshot toont de verzameling van de heap die is verzameld.

Een screenshot van de geheugenprofiler, met een geheugenintensieve set-gebaseerde bewerking geselecteerd.

Uit deze heap -momentopname werd opgemerkt dat de Set veel geheugen consumeerde. Door de oproeppunten te controleren, werd vastgesteld dat we onnodig eigenschappen van het type Set waren aan objecten die in grote volumes werden gemaakt. Deze kosten waren opgeteld en er werd veel geheugen geconsumeerd, tot het punt dat het gebruikelijk was dat de applicatie op grote input crashte.

Sets zijn handig voor het opslaan van unieke items en bieden bewerkingen die het unieke van hun inhoud gebruiken, zoals deduplicerende datasets en het bieden van efficiëntere opzoekingen. Die functies waren echter niet nodig, omdat de opgeslagen gegevens van de bron gegarandeerd waren. Als zodanig waren sets in de eerste plaats niet nodig. Om de geheugentoewijzing te verbeteren, werd het eigenschapstype gewijzigd van een Set in een gewone array. Na het toepassen van deze wijziging werd een andere hoop -momentopname gemaakt en werd verminderde geheugentoewijzing waargenomen. Ondanks het feit dat het geen aanzienlijke snelheidsverbeteringen met deze wijziging heeft bereikt, was het secundaire voordeel dat de applicatie minder vaak crashte.

Een screenshot van de geheugenprofiler. De eerder geheugenintensieve set-gebaseerde bewerking werd gewijzigd om een ​​gewone array te gebruiken, die de geheugenkosten aanzienlijk heeft verlaagd.

Derde activiteitengroep: afwegingen van gegevensstructuur wegen

Het derde deel is eigenaardig: u kunt in de vlamgrafiek zien dat het bestaat uit smalle maar hoge kolommen, die in dit geval diepe functieaanroepen en diepe recursies aangeven. In totaal duurde dit gedeelte ongeveer 1,4 seconden. Door naar de onderkant van deze sectie te kijken, was het duidelijk dat de breedte van deze kolommen werd bepaald door de duur van één functie: appendEventAtLevel , die suggereerde dat het een knelpunt kon zijn

In de implementatie van de functie appendEventAtLevel viel één ding op. Voor elke gegevensinvoer in de invoer (die bekend staat in code als de "gebeurtenis"), werd een item toegevoegd aan een kaart die de verticale positie van de tijdlijninvoer volgde. Dit was problematisch, omdat de hoeveelheid items die werden opgeslagen erg groot was. Kaarten zijn snel voor belangrijke opzoekingen, maar dit voordeel komt niet gratis. Naarmate een kaart groter wordt, kan het toevoegen van gegevens eraan bijvoorbeeld duur worden door het herhalen. Deze kosten worden merkbaar wanneer grote hoeveelheden items achtereenvolgens aan de kaart worden toegevoegd.

/**
 * Adds an event to the flame chart data at a defined vertical level.
 */
function appendEventAtLevel (event, level) {
  // ...

  const index = data.length;
  data.push(event);
  this.indexForEventMap.set(event, index);

  // ...
}

We hebben geëxperimenteerd met een andere aanpak waarvoor we niet nodig waren om een ​​item in een kaart toe te voegen voor elk item in de vlamgrafiek. De verbetering was aanzienlijk, wat bevestigde dat het knelpunt inderdaad gerelateerd was aan de overhead die werd opgelopen door alle gegevens aan de kaart toe te voegen. De tijd dat de activiteitengroep ongeveer 1,4 seconden tot ongeveer 200 milliseconden kromp.

Voor:

Een screenshot van het prestatiepaneel voordat optimalisaties werden gemaakt naar de functie ApendEventatEn -niveau. De totale tijd om de functie te laten draaien was 1.372,51 milliseconden.

Na:

Een screenshot van het prestatiepaneel nadat optimalisaties werden gemaakt naar de functie ApendEventatEn -niveau. De totale tijd om de functie te laten draaien was 207,2 milliseconden.

Vierde activiteitsgroep: gegevens over niet-kritiek werk en cachegegevens uitstellen om dubbele werkzaamheden te voorkomen

Op dit venster inzoomen is te zien dat er twee bijna identieke blokken functieblokken zijn. Door te kijken naar de naam van de genoemde functies, kunt u concluderen dat deze blokken bestaan ​​uit code die bomen bouwen (bijvoorbeeld met namen als refreshTree of buildChildren ). In feite is de gerelateerde code degene die de boomaanzichten in de onderste lade van het paneel creëert. Wat interessant is, is dat deze boomweergaven niet direct na het laden worden getoond. In plaats daarvan moet de gebruiker een boomweergave selecteren (de tabbladen "Bottom-Up", "Call Tree" en "Event Log" in de lade) voor de bomen die worden getoond. Bovendien, zoals u kunt zien aan de screenshot, werd het boombouwproces twee keer uitgevoerd.

Een screenshot van het prestatiepaneel met verschillende repetitieve taken die uitvoeren, zelfs als ze niet nodig zijn. Deze taken kunnen worden uitgesteld om op aanvraag uit te voeren, in plaats van van tevoren.

Er zijn twee problemen die we met deze foto hebben geïdentificeerd:

  1. Een niet-kritische taak belemmerde de prestaties van de laadtijd. Gebruikers hebben niet altijd de uitvoer nodig. Als zodanig is de taak niet van cruciaal belang voor het laden van profiel.
  2. Het resultaat van deze taken was niet in de cache. Daarom werden de bomen twee keer berekend, ondanks dat de gegevens niet veranderden.

We zijn begonnen met het uitstellen van de boomberekening tot wanneer de gebruiker de boomweergave handmatig opende. Alleen dan is het de moeite waard om de prijs te betalen van het maken van deze bomen. De totale tijd om dit twee keer te draaien was ongeveer 3,4 seconden, dus het uitstelde dat het een significant verschil maakte in de laadtijd. We onderzoeken nog steeds naar het cachen van dit soort taken.

Fifth Activity Group: vermijd complexe oproephiërarchieën waar mogelijk

Het was duidelijk dat het goed keek naar deze groep, dat een bepaalde call -keten herhaaldelijk werd ingeroepen. Hetzelfde patroon verscheen 6 keer op verschillende plaatsen in de vlamgrafiek en de totale duur van dit venster was ongeveer 2,4 seconden!

Een screenshot van het prestatiepaneel met zes afzonderlijke functie vereist het genereren van dezelfde trace -minimap, die elk diepe call -stacks hebben.

De gerelateerde code die meerdere keren wordt opgeroepen, is het deel dat de gegevens verwerkt die moeten worden weergegeven op de "minimap" (het overzicht van de tijdlijnactiviteit bovenaan het paneel). Het was niet duidelijk waarom het meerdere keren gebeurde, maar het hoefde zeker niet 6 keer te gebeuren! In feite moet de uitvoer van de code actueel blijven als er geen ander profiel wordt geladen. In theorie mag de code slechts één keer worden uitgevoerd.

Bij onderzoek werd vastgesteld dat de gerelateerde code werd genoemd als een gevolg van meerdere onderdelen in de laadpijplijn direct of indirect de functie aanroepen die de minimap berekent. Dit komt omdat de complexiteit van de oproepgrafiek van het programma in de loop van de tijd evolueerde en meer afhankelijkheden van deze code onbewust werden toegevoegd. Er is geen snelle oplossing voor dit probleem. De manier om het op te lossen, hangt af van de architectuur van de betreffende codebase. In ons geval moesten we de complexiteit van de oproephiërarchie een beetje verminderen en een controle toevoegen om de uitvoering van de code te voorkomen als de invoergegevens ongewijzigd bleven. Na dit te hebben geïmplementeerd, kregen we deze vooruitzichten van de tijdlijn:

Een screenshot van het prestatiepaneel dat de zes afzonderlijke functie toont, vraagt ​​om het genereren van dezelfde trace -minimap die slechts twee keer wordt verlaagd.

Merk op dat de uitvoering van de minimap twee keer optreedt, niet eenmaal. Dit komt omdat er voor elk profiel twee minimap worden getekend: een voor het overzicht bovenop het paneel, en een andere voor het vervolgkeuzemenu dat het momenteel zichtbare profiel uit de geschiedenis selecteert (elk item in dit menu bevat een overzicht van het profiel dat het selecteert). Desalniettemin hebben deze twee exact dezelfde inhoud, dus de een moet voor de ander kunnen hergebruikt.

Omdat deze minimap beide afbeeldingen op een canvas zijn, was het een kwestie van het gebruik van het drawImage canvas -hulpprogramma's en vervolgens slechts één keer de code uitvoeren om wat extra tijd te besparen. Als gevolg van deze inspanning werd de duur van de groep verlaagd van 2,4 seconden tot 140 milliseconden.

Conclusie

Na al deze fixes te hebben toegepast (en een paar andere kleinere hier en daar), zag de verandering van de tijdlijn van het profielbelasting er als volgt uit:

Voor:

Een screenshot van het prestatiepaneel met trace -laden vóór optimalisaties. Het proces duurde ongeveer tien seconden.

Na:

Een screenshot van het prestatiepaneel met trace -laden na optimalisaties. Het proces duurt nu ongeveer twee seconden.

De laadtijd na de verbeteringen was 2 seconden, wat betekent dat een verbetering van ongeveer 80% werd bereikt met relatief lage inspanning, omdat het meeste van wat werd gedaan uit snelle fixes bestond. Natuurlijk was het goed om in eerste instantie goed te identificeren wat te doen, en het PERF -paneel was hiervoor het juiste hulpmiddel.

Het is ook belangrijk om te benadrukken dat deze cijfers specifiek zijn voor een profiel dat wordt gebruikt als onderwerp van studie. Het profiel was interessant voor ons omdat het bijzonder groot was. Aangezien de verwerkingspijplijn voor elk profiel hetzelfde is, is de significante verbetering echter van toepassing op elk profiel dat in het Perf -paneel is geladen.

Afhaalmaaltijden

Er zijn enkele lessen om deze resultaten weg te nemen in termen van prestatie -optimalisatie van uw applicatie:

1. Maak gebruik van profileringstools om runtime -prestatiepatronen te identificeren

Profileringstools zijn ongelooflijk nuttig om te begrijpen wat er in uw applicatie gebeurt terwijl deze actief is, vooral om kansen te identificeren om de prestaties te verbeteren. Het prestatiepaneel in Chrome Devtools is een geweldige optie voor webapplicaties, omdat het de native webprofileringstool in de browser is en het actief wordt onderhouden om up-to-date te zijn met de nieuwste webplatformfuncties. Het is nu ook aanzienlijk sneller! 😉

Gebruik monsters die kunnen worden gebruikt als representatieve werklast en kijk wat u kunt vinden!

2. Vermijd complexe oproephiërarchieën

Vermijd indien mogelijk uw oproepgrafiek te ingewikkeld. Met complexe oproephiërarchieën is het gemakkelijk om prestatieregressies te introduceren en moeilijk te begrijpen waarom uw code wordt uitgevoerd zoals het is, waardoor het moeilijk is om verbeteringen te landen.

3. Identificeer onnodig werk

Het is gebruikelijk dat verouderende codebases code bevatten die niet langer nodig is. In ons geval namen Legacy en onnodige code een aanzienlijk deel van de totale laadtijd aan. Het verwijderen was het laagsthangende fruit.

4. Gebruik gegevensstructuren op de juiste manier

Gebruik gegevensstructuren om de prestaties te optimaliseren, maar begrijp ook de kosten en afwegingen die elk type gegevensstructuur met zich meebrengt bij het beslissen welke te gebruiken. Dit is niet alleen de ruimtecomplexiteit van de gegevensstructuur zelf, maar ook de tijdcomplexiteit van de toepasselijke bewerkingen.

5. Cache -resultaten om dubbele werk te voorkomen voor complexe of repetitieve bewerkingen

Als de bewerking kostbaar is om uit te voeren, is het logisch om zijn resultaten op te slaan voor de volgende keer dat deze nodig is. Het is ook logisch om dit te doen als de bewerking vele malen wordt gedaan - zelfs als elke individuele tijd niet bijzonder duur is.

6. Stel niet-kritisch werk uit

Als de uitvoer van een taak niet onmiddellijk nodig is en de taakuitvoering het kritieke pad uitbreidt, overweeg dan om het uit te stellen door het lui te bellen wanneer de uitvoer daadwerkelijk nodig is.

7. Gebruik efficiënte algoritmen op grote inputs

Voor grote input worden optimale tijdcomplexiteitsalgoritmen cruciaal. We hebben in dit voorbeeld niet in deze categorie gekeken, maar hun belang kan nauwelijks worden overschat.

8. Bonus: Benchmark uw pijpleidingen

Om ervoor te zorgen dat uw evoluerende code snel blijft, is het verstandig om het gedrag te controleren en te vergelijken met normen. Op deze manier identificeert u proactief regressies en verbetert u de algehele betrouwbaarheid, waardoor u op langetermijnsucces wordt ingesteld.

,

Andrés Olivares
Andrés Olivares
Nancy Li
Nancy Li

Ongeacht wat voor soort applicatie u ontwikkelt, de prestaties van de prestaties te optimaliseren en ervoor te zorgen dat deze snel wordt geladen en soepele interacties biedt, is van cruciaal belang voor de gebruikerservaring en het succes van de toepassing. Een manier om dit te doen is om de activiteit van een applicatie te inspecteren door profileringstools te gebruiken om te zien wat er onder de motorkap gebeurt terwijl deze draait tijdens een tijdvenster. Het prestatiepaneel in Devtools is een geweldig profileringstool om de prestaties van webtoepassingen te analyseren en te optimaliseren. Als uw app in Chrome wordt uitgevoerd, geeft het u een gedetailleerd visueel overzicht van wat de browser doet als uw applicatie wordt uitgevoerd. Inzicht in deze activiteit kan u helpen patronen, knelpunten en prestatiehotspots te identificeren waarop u kunt handelen om de prestaties te verbeteren.

Het volgende voorbeeld loopt u echter met behulp van het uitvoeringspaneel .

Ons profileringscenario opzetten en opnieuw maken

Onlangs hebben we een doel gesteld om het prestatiepaneel performanter te maken. In het bijzonder wilden we dat het grote hoeveelheden prestatiegegevens sneller zou laden. Dit is bijvoorbeeld het geval bij het profileren van langlopende of complexe processen of het vastleggen van gegevens met hoge granulariteit. Om dit te bereiken, was een begrip van hoe de toepassing presteerde en waarom deze op die manier presteerde eerst nodig was, wat werd bereikt door een profileringstool te gebruiken.

Zoals u misschien weet, is Devtools zelf een webtoepassing. Als zodanig kan het worden geprofileerd met behulp van het prestatiepaneel . Om dit paneel zelf te profileren, kunt u Devtools openen en vervolgens een andere DevTools -instantie openen die eraan is gekoppeld. Bij Google staat deze opstelling bekend als DevTools-on-Devtools .

Met de opstelling klaar, moet het te profileren scenario worden nagebouwd en opgenomen. Om verwarring te voorkomen, wordt het originele devTools -venster de " eerste devtools -instantie" genoemd, en het venster dat de eerste instantie inspecteert, wordt de " tweede devtools -instantie" genoemd.

Een screenshot van een DevTools -exemplaar die de elementen in Devtools zelf inspecteert.
DevTools-on-Devtools: Devtools inspecteren met DevTools.

In het tweede DevTools -exemplaar wordt het prestatiepaneel - dat vanaf hier het perf -paneel zal worden genoemd - de eerste DevTools -instantie om het scenario opnieuw te maken, dat een profiel laadt.

In de tweede DevTools -instantie wordt een live -opname gestart, terwijl in eerste instantie een profiel wordt geladen vanuit een bestand op schijf. Een groot bestand wordt geladen om de prestaties van het verwerken van grote ingangen nauwkeurig te profileren. Wanneer beide instanties het laden voltooien, worden de prestatieprofileringsgegevens - meestal een spoor genoemd - gezien in het tweede DevTools -exemplaar van het Perf -paneel dat een profiel laadt.

De initiële staat: kansen voor verbetering identificeren

Nadat het laden is voltooid, werd het volgende op onze tweede Perf -paneelinstantie waargenomen in de volgende screenshot. Focus op de activiteit van de hoofdthread, die zichtbaar is onder de baan met het label Main . Het is te zien dat er vijf grote groepen activiteiten in de vlamgrafiek zijn. Deze bestaan ​​uit de taken waarbij het laden de meeste tijd duurt. De totale tijd van deze taken was ongeveer 10 seconden . In de volgende screenshot wordt het prestatiepaneel gebruikt om zich op elk van deze activiteitengroepen te concentreren om te zien wat te vinden is.

Een screenshot van het prestatiepaneel in Devtools die het laden van een prestatietrace in het uitvoeringspaneel van een andere DevTools -instantie inspecteren. Het profiel duurt ongeveer 10 seconden om te laden. Deze keer wordt meestal verdeeld over vijf hoofdgroepen activiteiten.

Eerste activiteitsgroep: onnodig werk

Het werd duidelijk dat de eerste groep activiteiten legacy code was die nog steeds liep, maar niet echt nodig was. Kortom, alles onder het Green Block Labeled processThreadEvents was verspilde inspanning. Die was een snelle overwinning. Het verwijderen van die functieoproep die ongeveer 1,5 seconden tijd is opgeslagen. Koel!

Tweede activiteitsgroep

In de tweede activiteitsgroep was de oplossing niet zo eenvoudig als bij de eerste. De buildProfileCalls duurden ongeveer 0,5 seconden, en die taak was niet iets dat kon worden vermeden.

Een screenshot van het prestatiepaneel in Devtools die een ander exemplaar van het prestatiepaneel inspecteert. Een taak geassocieerd met de functie BuildProfilecalls duurt ongeveer 0,5 seconden.

Uit nieuwsgierigheid hebben we de geheugenoptie in het Perf -paneel ingeschakeld om verder te onderzoeken en zagen we dat de buildProfileCalls -activiteit ook veel geheugen gebruikte. Hier kunt u zien hoe de blauwe lijngrafiek plotseling rond de tijd dat buildProfileCalls wordt uitgevoerd, wordt uitgevoerd, wat een potentieel geheugenlek suggereert.

Een screenshot van de geheugenprofiler in Devtools die geheugenverbruik van het prestatiepaneel beoordelen. De inspecteur suggereert dat de functie BuildProfilecalls verantwoordelijk is voor een geheugenlek.

Om dit vermoeden op te volgen, gebruikten we het geheugenpaneel (een ander paneel in Devtools, anders dan de geheugenlade in het Perf -paneel) om te onderzoeken. Binnen het geheugenpaneel werd het "toewijzingsbemonstering" -profileringstype geselecteerd, dat de heap -snapshot registreerde voor het Perf -paneel dat het CPU -profiel laadde.

Een screenshot van de initiële status van de geheugenprofiler. De optie 'Allocatie bemonstering' wordt gemarkeerd met een rode doos en het geeft aan dat deze optie het beste is voor JavaScript -geheugenprofilering.

De volgende screenshot toont de verzameling van de heap die is verzameld.

Een screenshot van de geheugenprofiler, met een geheugenintensieve set-gebaseerde bewerking geselecteerd.

Uit deze heap -momentopname werd opgemerkt dat de Set veel geheugen consumeerde. Door de oproeppunten te controleren, werd vastgesteld dat we onnodig eigenschappen van het type Set waren aan objecten die in grote volumes werden gemaakt. Deze kosten waren opgeteld en er werd veel geheugen geconsumeerd, tot het punt dat het gebruikelijk was dat de applicatie op grote input crashte.

Sets zijn handig voor het opslaan van unieke items en bieden bewerkingen die het unieke van hun inhoud gebruiken, zoals deduplicerende datasets en het bieden van efficiëntere opzoekingen. Die functies waren echter niet nodig, omdat de opgeslagen gegevens van de bron gegarandeerd waren. Als zodanig waren sets in de eerste plaats niet nodig. Om de geheugentoewijzing te verbeteren, werd het eigenschapstype gewijzigd van een Set in een gewone array. Na het toepassen van deze wijziging werd een andere hoop -momentopname gemaakt en werd verminderde geheugentoewijzing waargenomen. Ondanks het feit dat het geen aanzienlijke snelheidsverbeteringen met deze wijziging heeft bereikt, was het secundaire voordeel dat de applicatie minder vaak crashte.

Een screenshot van de geheugenprofiler. De eerder geheugenintensieve set-gebaseerde bewerking werd gewijzigd om een ​​gewone array te gebruiken, die de geheugenkosten aanzienlijk heeft verlaagd.

Derde activiteitengroep: afwegingen van gegevensstructuur wegen

Het derde deel is eigenaardig: u kunt in de vlamgrafiek zien dat het bestaat uit smalle maar hoge kolommen, die in dit geval diepe functieaanroepen en diepe recursies aangeven. In totaal duurde dit gedeelte ongeveer 1,4 seconden. Door naar de onderkant van deze sectie te kijken, was het duidelijk dat de breedte van deze kolommen werd bepaald door de duur van één functie: appendEventAtLevel , die suggereerde dat het een knelpunt kon zijn

In de implementatie van de functie appendEventAtLevel viel één ding op. Voor elke gegevensinvoer in de invoer (die bekend staat in code als de "gebeurtenis"), werd een item toegevoegd aan een kaart die de verticale positie van de tijdlijninvoer volgde. Dit was problematisch, omdat de hoeveelheid items die werden opgeslagen erg groot was. Kaarten zijn snel voor belangrijke opzoekingen, maar dit voordeel komt niet gratis. Naarmate een kaart groter wordt, kan het toevoegen van gegevens eraan bijvoorbeeld duur worden door het herhalen. Deze kosten worden merkbaar wanneer grote hoeveelheden items achtereenvolgens aan de kaart worden toegevoegd.

/**
 * Adds an event to the flame chart data at a defined vertical level.
 */
function appendEventAtLevel (event, level) {
  // ...

  const index = data.length;
  data.push(event);
  this.indexForEventMap.set(event, index);

  // ...
}

We hebben geëxperimenteerd met een andere aanpak waarvoor we niet nodig waren om een ​​item in een kaart toe te voegen voor elk item in de vlamgrafiek. De verbetering was aanzienlijk, wat bevestigde dat het knelpunt inderdaad gerelateerd was aan de overhead die werd opgelopen door alle gegevens aan de kaart toe te voegen. De tijd dat de activiteitengroep ongeveer 1,4 seconden tot ongeveer 200 milliseconden kromp.

Voor:

Een screenshot van het prestatiepaneel voordat optimalisaties werden gemaakt naar de functie ApendEventatEn -niveau. De totale tijd om de functie te laten draaien was 1.372,51 milliseconden.

Na:

Een screenshot van het prestatiepaneel nadat optimalisaties werden gemaakt naar de functie ApendEventatEn -niveau. De totale tijd om de functie te laten draaien was 207,2 milliseconden.

Vierde activiteitsgroep: gegevens over niet-kritiek werk en cachegegevens uitstellen om dubbele werkzaamheden te voorkomen

Op dit venster inzoomen is te zien dat er twee bijna identieke blokken functieblokken zijn. Door te kijken naar de naam van de genoemde functies, kunt u concluderen dat deze blokken bestaan ​​uit code die bomen bouwen (bijvoorbeeld met namen als refreshTree of buildChildren ). In feite is de gerelateerde code degene die de boomaanzichten in de onderste lade van het paneel creëert. Wat interessant is, is dat deze boomweergaven niet direct na het laden worden getoond. In plaats daarvan moet de gebruiker een boomweergave selecteren (de tabbladen "Bottom-Up", "Call Tree" en "Event Log" in de lade) voor de bomen die worden getoond. Bovendien, zoals u kunt zien aan de screenshot, werd het boombouwproces twee keer uitgevoerd.

Een screenshot van het prestatiepaneel met verschillende repetitieve taken die uitvoeren, zelfs als ze niet nodig zijn. Deze taken kunnen worden uitgesteld om op aanvraag uit te voeren, in plaats van van tevoren.

Er zijn twee problemen die we met deze foto hebben geïdentificeerd:

  1. Een niet-kritische taak belemmerde de prestaties van de laadtijd. Gebruikers hebben niet altijd de uitvoer nodig. Als zodanig is de taak niet van cruciaal belang voor het laden van profiel.
  2. Het resultaat van deze taken was niet in de cache. Daarom werden de bomen twee keer berekend, ondanks dat de gegevens niet veranderden.

We zijn begonnen met het uitstellen van de boomberekening tot wanneer de gebruiker de boomweergave handmatig opende. Alleen dan is het de moeite waard om de prijs te betalen van het maken van deze bomen. De totale tijd om dit twee keer te draaien was ongeveer 3,4 seconden, dus het uitstelde dat het een significant verschil maakte in de laadtijd. We onderzoeken nog steeds naar het cachen van dit soort taken.

Fifth Activity Group: vermijd complexe oproephiërarchieën waar mogelijk

Het was duidelijk dat het goed keek naar deze groep, dat een bepaalde call -keten herhaaldelijk werd ingeroepen. Hetzelfde patroon verscheen 6 keer op verschillende plaatsen in de vlamgrafiek en de totale duur van dit venster was ongeveer 2,4 seconden!

Een screenshot van het prestatiepaneel met zes afzonderlijke functie vereist het genereren van dezelfde trace -minimap, die elk diepe call -stacks hebben.

De gerelateerde code die meerdere keren wordt opgeroepen, is het deel dat de gegevens verwerkt die moeten worden weergegeven op de "minimap" (het overzicht van de tijdlijnactiviteit bovenaan het paneel). Het was niet duidelijk waarom het meerdere keren gebeurde, maar het hoefde zeker niet 6 keer te gebeuren! In feite moet de uitvoer van de code actueel blijven als er geen ander profiel wordt geladen. In theorie mag de code slechts één keer worden uitgevoerd.

Bij onderzoek werd vastgesteld dat de gerelateerde code werd genoemd als een gevolg van meerdere onderdelen in de laadpijplijn direct of indirect de functie aanroepen die de minimap berekent. Dit komt omdat de complexiteit van de oproepgrafiek van het programma in de loop van de tijd evolueerde en meer afhankelijkheden van deze code onbewust werden toegevoegd. Er is geen snelle oplossing voor dit probleem. De manier om het op te lossen, hangt af van de architectuur van de betreffende codebase. In ons geval moesten we de complexiteit van de oproephiërarchie een beetje verminderen en een controle toevoegen om de uitvoering van de code te voorkomen als de invoergegevens ongewijzigd bleven. After implementing this, we got this outlook of the timeline:

A screenshot of the performance panel showing the six separate function calls for generating the same trace minimap reduced to only two times.

Note that the minimap rendering execution occurs twice, not once. This is because there are two minimaps being drawn for every profile: one for the overview on top of the panel, and another for the drop-down menu that selects the currently visible profile from the history (every item in this menu contains an overview of the profile it selects). Nonetheless, these two have the exact same content, so one should be able to reused for the other.

Since these minimaps are both images drawn on a canvas, it was a matter of using the drawImage canvas utility , and subsequently running the code only once to save some extra time. As a result of this effort, the group's duration was reduced from 2.4 seconds to 140 milliseconds.

Conclusie

After having applied all these fixes (and a couple of other smaller ones here and there), the change of the profile loading timeline looked as follows:

Voor:

A screenshot of the performance panel showing trace loading before optimizations. The process took approximately ten seconds.

Na:

A screenshot of the performance panel showing trace loading after optimizations. The process now takes approximately two seconds.

The load time after the improvements was 2 seconds, meaning that an improvement of about 80% was achieved with relatively low effort, since most of what was done consisted of quick fixes. Of course, properly identifying what to do initially was key, and the perf panel was the right tool for this.

It's also important to highlight that these numbers are particular to a profile being used as a subject of study. The profile was interesting to us because it was particularly large. Nonetheless, since the processing pipeline is the same for every profile, the significant improvement achieved applies to every profile loaded in the perf panel.

Takeaways

There are some lessons to take away from these results in terms of performance optimization of your application:

1. Make use of profiling tools to identify runtime performance patterns

Profiling tools are incredibly useful to understand what's happening in your application while it's running, especially to identify opportunities to improve performance. The Performance panel in Chrome DevTools is a great option for web applications since it's the native web profiling tool in the browser, and it's actively maintained to be up-to-date with the latest web platform features. Also, it's now significantly faster! 😉

Use samples that can be used as representative workloads and see what you can find!

2. Avoid complex call hierarchies

When possible, avoid making your call graph too complicated. With complex call hierarchies, it's easy to introduce performance regressions and hard to understand why your code is running the way it is, making it hard to land improvements.

3. Identify unnecessary work

It's common for aging codebases to contain code that's no longer needed. In our case, legacy and unnecessary code was taking a significant portion of the total loading time. Removing it was the lowest-hanging fruit.

4. Use data structures appropriately

Use data structures to optimize performance, but also understand the costs and trade-offs each type of data structure brings when deciding which ones to use. This isn't only the space complexity of the data structure itself, but also time complexity of the applicable operations.

5. Cache results to avoid duplicate work for complex or repetitive operations

If the operation is costly to execute, it makes sense to store its results for the next time it's needed. It also makes sense to do this if the operation is done many times—even if each individual time isn't particularly costly.

6. Defer non-critical work

If the output of a task isn't needed immediately and the task execution is extending the critical path, consider deferring it by lazily calling it when its output is actually needed.

7. Use efficient algorithms on large inputs

For large inputs, optimal time complexity algorithms become crucial. We didn't look into this category in this example, but their importance can hardly be overstated.

8. Bonus: benchmark your pipelines

To make sure your evolving code remains fast, it's wise to monitor the behavior and compare it against standards. This way, you proactively identify regressions and improve the overall reliability, setting you up for long-term success.