Gebruik planner.yield() om lange taken op te splitsen

Brendan Kenny
Brendan Kenny

Gepubliceerd: 6 maart 2025

Browser Support

  • Chroom: 129.
  • Rand: 129.
  • Firefox: 142.
  • Safari: niet ondersteund.

Source

Een pagina voelt traag en reageert niet goed wanneer lange taken de hoofdthread bezig houden, waardoor deze geen ander belangrijk werk kan doen, zoals reageren op gebruikersinvoer. Hierdoor kunnen zelfs ingebouwde formulierbesturingselementen voor gebruikers kapot lijken – alsof de pagina vastgelopen is – laat staan ​​complexere, aangepaste componenten.

scheduler.yield() is een manier om de hoofdthread te ondersteunen, waardoor de browser al het werk met hoge prioriteit kan uitvoeren en de uitvoering kan voortzetten waar deze was gebleven. Dit zorgt ervoor dat een pagina responsiever blijft en draagt ​​op zijn beurt bij aan een betere Interaction to Next Paint (INP) .

scheduler.yield biedt een ergonomische API die precies doet wat hij belooft: de uitvoering van de functie die wordt aangeroepen, pauzeert bij de await scheduler.yield() expressie en geeft deze door aan de hoofdthread, waardoor de taak wordt afgebroken. De uitvoering van de rest van de functie – de zogenaamde voortzetting van de functie – wordt gepland voor een nieuwe event-loop-taak.

async function respondToUserClick() {
  giveImmediateFeedback();
  await scheduler.yield(); // Yield to the main thread.
  slowerComputation();
}

Het specifieke voordeel van scheduler.yield is dat de voortzetting na de yield gepland wordt uitgevoerd vóór andere soortgelijke taken die door de pagina in de wachtrij zijn geplaatst. Het geeft prioriteit aan het voortzetten van een taak boven het starten van nieuwe taken.

Functies zoals setTimeout of scheduler.postTask kunnen ook worden gebruikt om taken op te splitsen, maar deze voortzettingen worden doorgaans uitgevoerd na reeds in de wachtrij geplaatste nieuwe taken, waardoor er mogelijk lange vertragingen kunnen ontstaan ​​tussen het overdragen aan de hoofdthread en het voltooien van hun werk.

Geprioriteerde voortzettingen na het opgeven

scheduler.yield maakt deel uit van de API voor het plannen van geprioriteerde taken . Als webontwikkelaars praten we doorgaans niet over de volgorde waarin de event loop taken uitvoert in termen van expliciete prioriteiten, maar de relatieve prioriteiten zijn er altijd , zoals een requestIdleCallback -callback die wordt uitgevoerd na alle setTimeout -callbacks in de wachtrij, of een geactiveerde invoergebeurtenislistener die meestal wordt uitgevoerd vóór een taak die in de wachtrij is geplaatst met setTimeout(callback, 0) .

Met geprioriteerde taakplanning wordt dit explicieter gemaakt, waardoor het eenvoudiger wordt om te bepalen welke taak vóór een andere wordt uitgevoerd. Bovendien kunt u de prioriteiten aanpassen om de uitvoeringsvolgorde indien nodig te wijzigen.

Zoals gezegd krijgt het voortzetten van een functie na het opgeven met scheduler.yield() een hogere prioriteit dan het starten van andere taken. Het uitgangspunt is dat de voortzetting van een taak eerst moet worden uitgevoerd, voordat andere taken worden gestart. Als de taak goed gedragende code is die periodiek opgeeft zodat de browser andere belangrijke taken kan uitvoeren (zoals reageren op gebruikersinvoer), mag deze niet worden bestraft voor het opgeven door voorrang te krijgen na andere, vergelijkbare taken.

Hier is een voorbeeld: twee functies, in de wachtrij geplaatst om in verschillende taken te worden uitgevoerd met behulp van setTimeout .

setTimeout(myJob);
setTimeout(someoneElsesJob);

In dit geval staan ​​de twee setTimeout aanroepen direct naast elkaar, maar op een echte pagina kunnen ze op compleet verschillende plekken worden aangeroepen. Bijvoorbeeld een script van de eerste partij en een script van de derde partij die onafhankelijk van elkaar werk instellen om uit te voeren, of het kunnen twee taken van afzonderlijke componenten zijn die diep in de scheduler van uw framework worden geactiveerd.

Zo zou dat werk er in DevTools uit kunnen zien:

Twee taken worden weergegeven in het prestatiepaneel van Chrome DevTools. Beide worden aangegeven als lange taken, waarbij de functie 'myJob' de volledige uitvoering van de eerste taak in beslag neemt en 'someoneElsesJob' de volledige uitvoering van de tweede taak.

myJob is gemarkeerd als een lange taak, waardoor de browser niets anders kan doen terwijl de taak actief is. Ervan uitgaande dat het afkomstig is van een first-party script, kunnen we het opsplitsen:

function myJob() {
  // Run part 1.
  myJobPart1();
  // Yield with setTimeout() to break up long task, then run part2.
  setTimeout(myJobPart2, 0);
}

Omdat myJobPart2 gepland is om te worden uitgevoerd met setTimeout in myJob , maar die planning wordt uitgevoerd nadat someoneElsesJob al is gepland, ziet de uitvoering er als volgt uit:

Drie taken worden weergegeven in het prestatiepaneel van Chrome DevTools. De eerste is het uitvoeren van de functie 'myJobPart1', de tweede is een lange taak die 'someoneElsesJob' uitvoert, en de derde taak is het uitvoeren van 'myJobPart2'.

We hebben de taak opgedeeld met setTimeout , zodat de browser responsief kan zijn tijdens het midden van myJob , maar nu wordt het tweede deel van myJob pas uitgevoerd nadat someoneElsesJob klaar is.

In sommige gevallen kan dat prima zijn, maar meestal is dit niet optimaal. myJob gaf voorrang aan de hoofdthread om ervoor te zorgen dat de pagina responsief bleef op gebruikersinvoer, en niet om de hoofdthread volledig op te geven. In gevallen waarin someoneElsesJob bijzonder traag is, of waarin naast someoneElsesJob ook veel andere taken zijn gepland, kan het lang duren voordat de tweede helft van myJob wordt uitgevoerd. Dat was waarschijnlijk niet de bedoeling van de ontwikkelaar toen ze die setTimeout aan myJob toevoegden.

Voer scheduler.yield() in, waardoor de voortzetting van elke functie die deze aanroept in een wachtrij met een iets hogere prioriteit wordt geplaatst dan het starten van andere vergelijkbare taken. Als myJob wordt gewijzigd om deze functie te gebruiken:

async function myJob() {
  // Run part 1.
  myJobPart1();
  // Yield with scheduler.yield() to break up long task, then run part2.
  await scheduler.yield();
  myJobPart2();
}

De uitvoering ziet er nu als volgt uit:

Twee taken worden weergegeven in het prestatiepaneel van Chrome DevTools. Beide worden aangegeven als lange taken, waarbij de functie 'myJob' de volledige uitvoering van de eerste taak in beslag neemt en 'someoneElsesJob' de volledige uitvoering van de tweede taak.

De browser kan nog steeds responsief zijn, maar de voortzetting van de myJob -taak krijgt nu voorrang boven het starten van de nieuwe taak someoneElsesJob . myJob is dus voltooid voordat someoneElsesJob begint. Dit komt veel dichter in de buurt van de verwachting om de responsiviteit te behouden door de hoofdthread te laten meedraaien, en niet door de hoofdthread volledig op te geven.

Prioritaire overerving

Als onderdeel van de grotere API voor prioritaire taakplanning (Prioritized Task Scheduling API) werkt scheduler.yield() goed samen met de expliciete prioriteiten die beschikbaar zijn in scheduler.postTask() . Zonder een expliciet ingestelde prioriteit zal een scheduler.yield() binnen een scheduler.postTask() -callback in principe hetzelfde werken als in het vorige voorbeeld.

Als er echter een prioriteit is ingesteld, bijvoorbeeld door een lage 'background' prioriteit te gebruiken:

async function lowPriorityJob() {
  part1();
  await scheduler.yield();
  part2();
}

scheduler.postTask(lowPriorityJob, {priority: 'background'});

De voortzetting wordt gepland met een prioriteit die hoger is dan andere 'background' Hierbij krijgt de voortzetting de verwachte prioriteit vóór alle in behandeling zijnde 'background' . De prioriteit is echter nog steeds lager dan andere standaardtaken of taken met een hoge prioriteit. Het blijft 'background' .

Dit betekent dat als u werk met lage prioriteit plant met een 'background' scheduler.postTask() (of met requestIdleCallback ), de voortzetting na een scheduler.yield() daarin ook wacht totdat de meeste andere taken voltooid zijn en de hoofdthread inactief is om te worden uitgevoerd. Dit is precies wat u wilt van het opleveren van een taak met lage prioriteit.

Hoe de API te gebruiken

Momenteel is scheduler.yield() alleen beschikbaar in Chromium-gebaseerde browsers. Om het te gebruiken moet u de functie detecteren gebruiken en voor andere browsers terugvallen op een secundaire manier van yielden.

scheduler-polyfill is een kleine polyfill voor scheduler.postTask en scheduler.yield die intern een combinatie van methoden gebruikt om veel van de kracht van de planning-API's in andere browsers na te bootsen (hoewel scheduler.yield() prioriteitsovererving niet wordt ondersteund).

Voor degenen die een polyfill willen vermijden, is een methode om via setTimeout() te yielden en het verlies van een geprioriteerde voortzetting te accepteren, of zelfs helemaal niet te yielden in niet-ondersteunde browsers als dat niet acceptabel is. Zie de documentatie van scheduler.yield() in Optimaliseer lange taken voor meer informatie .

De typen wicg-task-scheduling kunnen ook worden gebruikt om typecontrole en IDE-ondersteuning te krijgen als u scheduler.yield() gebruikt voor functiedetectie en zelf een fallback toevoegt.

Meer informatie

Voor meer informatie over de API en hoe deze samenwerkt met taakprioriteiten en scheduler.postTask() raadpleegt u de documentatie over scheduler.yield() en Prioritized Task Scheduling op MDN.

Als u meer wilt weten over lange taken, hoe ze de gebruikerservaring beïnvloeden en wat u eraan kunt doen, lees dan over het optimaliseren van lange taken .