Met requestIdleCallback

Veel sites en apps moeten veel scripts uitvoeren. Vaak moet uw JavaScript zo snel mogelijk worden uitgevoerd, maar tegelijkertijd wilt u niet dat het de gebruiker in de weg zit. Als u analysegegevens verzendt wanneer de gebruiker door de pagina scrollt, of als u elementen aan de DOM toevoegt terwijl deze op de knop tikt, reageert uw webapp mogelijk niet meer, wat resulteert in een slechte gebruikerservaring.

RequestIdleCallback gebruiken om niet-essentieel werk te plannen.

Het goede nieuws is dat er nu een API is die kan helpen: requestIdleCallback . Op dezelfde manier waarop requestAnimationFrame ons in staat stelde animaties goed te plannen en onze kansen op 60 fps te maximaliseren, plant requestIdleCallback werk wanneer er vrije tijd is aan het einde van een frame, of wanneer de gebruiker inactief is. Dit betekent dat er een mogelijkheid is om uw werk te doen zonder de gebruiker in de weg te staan. Het is beschikbaar vanaf Chrome 47, dus je kunt er vandaag nog mee experimenteren door Chrome Canary te gebruiken! Het is een experimentele functie en de specificaties zijn nog steeds in beweging, dus er kunnen in de toekomst dingen veranderen.

Waarom zou ik requestIdleCallback gebruiken?

Zelf niet-essentiële werkzaamheden inplannen is erg lastig. Het is onmogelijk om precies te achterhalen hoeveel frametijd er nog overblijft, omdat nadat requestAnimationFrame -callbacks zijn uitgevoerd, er stijlberekeningen, lay-out, verf en andere browser-internals moeten worden uitgevoerd. Een thuisgerolde oplossing kan daar geen rekening mee houden. Om er zeker van te zijn dat een gebruiker op de een of andere manier geen interactie heeft , moet je ook luisteraars koppelen aan elk soort interactiegebeurtenis ( scroll , touch , click ), zelfs als je ze niet nodig hebt voor de functionaliteit, alleen maar zodat u kunt er absoluut zeker van zijn dat de gebruiker geen interactie heeft. De browser daarentegen weet precies hoeveel tijd er aan het einde van het frame beschikbaar is en of de gebruiker interactie heeft, en dus krijgen we via requestIdleCallback een API waarmee we zo veel mogelijk gebruik kunnen maken van onze vrije tijd. efficiënte manier mogelijk.

Laten we er wat gedetailleerder naar kijken en kijken hoe we er gebruik van kunnen maken.

Controleren op requestIdleCallback

Het is een vroege dag voor requestIdleCallback , dus voordat u het gebruikt, moet u controleren of het beschikbaar is voor gebruik:

if ('requestIdleCallback' in window) {
    // Use requestIdleCallback to schedule work.
} else {
    // Do what you’d do today.
}

Je kunt het gedrag ook opvullen, waarvoor je terug moet vallen op setTimeout :

window.requestIdleCallback =
    window.requestIdleCallback ||
    function (cb) {
    var start = Date.now();
    return setTimeout(function () {
        cb({
        didTimeout: false,
        timeRemaining: function () {
            return Math.max(0, 50 - (Date.now() - start));
        }
        });
    }, 1);
    }

window.cancelIdleCallback =
    window.cancelIdleCallback ||
    function (id) {
    clearTimeout(id);
    }

Het gebruik van setTimeout is niet geweldig omdat het niets weet van inactieve tijd zoals requestIdleCallback dat doet, maar omdat je je functie direct zou aanroepen als requestIdleCallback niet beschikbaar was, ben je niet slechter af als je op deze manier aan het shimmen bent. Als requestIdleCallback beschikbaar is, worden uw oproepen met de shim stil doorgestuurd, wat geweldig is.

Maar laten we voorlopig aannemen dat het bestaat.

Met requestIdleCallback

Het aanroepen van requestIdleCallback lijkt sterk op requestAnimationFrame , omdat het een callback-functie als eerste parameter gebruikt:

requestIdleCallback(myNonEssentialWork);

Wanneer myNonEssentialWork wordt aangeroepen, krijgt het een deadline object dat een functie bevat die een getal retourneert dat aangeeft hoeveel tijd er nog over is voor uw werk:

function myNonEssentialWork (deadline) {
    while (deadline.timeRemaining() > 0)
    doWorkIfNeeded();
}

De timeRemaining functie kan worden aangeroepen om de nieuwste waarde op te halen. Wanneer timeRemaining() nul retourneert, kunt u nog een requestIdleCallback plannen als u nog meer werk te doen heeft:

function myNonEssentialWork (deadline) {
    while (deadline.timeRemaining() > 0 && tasks.length > 0)
    doWorkIfNeeded();

    if (tasks.length > 0)
    requestIdleCallback(myNonEssentialWork);
}

Het garanderen van je functie heet

Wat doe je als het echt druk is? Het kan zijn dat u zich zorgen maakt dat u nooit wordt teruggebeld. Hoewel requestIdleCallback lijkt op requestAnimationFrame , verschilt het ook doordat er een optionele tweede parameter voor nodig is: een optieobject met een time- outeigenschap. Deze time-out, indien ingesteld, geeft de browser een tijd in milliseconden waarbinnen de callback moet worden uitgevoerd:

// Wait at most two seconds before processing events.
requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });

Als uw callback wordt uitgevoerd vanwege de time-out, zult u twee dingen opmerken:

  • timeRemaining() retourneert nul.
  • De eigenschap didTimeout van het deadline object zal waar zijn.

Als je ziet dat de didTimeout waar is, wil je waarschijnlijk gewoon het werk uitvoeren en er klaar mee zijn:

function myNonEssentialWork (deadline) {

    // Use any remaining time, or, if timed out, just run through the tasks.
    while ((deadline.timeRemaining() > 0 || deadline.didTimeout) &&
            tasks.length > 0)
    doWorkIfNeeded();

    if (tasks.length > 0)
    requestIdleCallback(myNonEssentialWork);
}

Vanwege de potentiële verstoring die deze time-out voor uw gebruikers kan veroorzaken (het werk kan ervoor zorgen dat uw app niet meer reageert of janky wordt), moet u voorzichtig zijn met het instellen van deze parameter. Laat waar mogelijk de browser beslissen wanneer de callback moet worden opgeroepen.

RequestIdleCallback gebruiken voor het verzenden van analysegegevens

Laten we eens kijken met requestIdleCallback om analysegegevens te verzenden. In dit geval willen we waarschijnlijk een gebeurtenis volgen, bijvoorbeeld door op een navigatiemenu te tikken. Omdat ze normaal gesproken op het scherm animeren, willen we echter voorkomen dat deze gebeurtenis onmiddellijk naar Google Analytics wordt verzonden. We zullen een reeks gebeurtenissen maken die we kunnen verzenden en verzoeken dat ze op een bepaald moment in de toekomst worden verzonden:

var eventsToSend = [];

function onNavOpenClick () {

    // Animate the menu.
    menu.classList.add('open');

    // Store the event for later.
    eventsToSend.push(
    {
        category: 'button',
        action: 'click',
        label: 'nav',
        value: 'open'
    });

    schedulePendingEvents();
}

Nu zullen we requestIdleCallback moeten gebruiken om eventuele openstaande gebeurtenissen te verwerken:

function schedulePendingEvents() {

    // Only schedule the rIC if one has not already been set.
    if (isRequestIdleCallbackScheduled)
    return;

    isRequestIdleCallbackScheduled = true;

    if ('requestIdleCallback' in window) {
    // Wait at most two seconds before processing events.
    requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });
    } else {
    processPendingAnalyticsEvents();
    }
}

Hier kunt u zien dat ik een time-out van 2 seconden heb ingesteld, maar deze waarde is afhankelijk van uw toepassing. Voor analytische gegevens is het logisch dat een time-out wordt gebruikt om ervoor te zorgen dat gegevens binnen een redelijk tijdsbestek worden gerapporteerd, in plaats van alleen maar op een bepaald moment in de toekomst.

Ten slotte moeten we de functie schrijven die requestIdleCallback zal uitvoeren.

function processPendingAnalyticsEvents (deadline) {

    // Reset the boolean so future rICs can be set.
    isRequestIdleCallbackScheduled = false;

    // If there is no deadline, just run as long as necessary.
    // This will be the case if requestIdleCallback doesn’t exist.
    if (typeof deadline === 'undefined')
    deadline = { timeRemaining: function () { return Number.MAX_VALUE } };

    // Go for as long as there is time remaining and work to do.
    while (deadline.timeRemaining() > 0 && eventsToSend.length > 0) {
    var evt = eventsToSend.pop();

    ga('send', 'event',
        evt.category,
        evt.action,
        evt.label,
        evt.value);
    }

    // Check if there are more events still to send.
    if (eventsToSend.length > 0)
    schedulePendingEvents();
}

Voor dit voorbeeld ging ik ervan uit dat als requestIdleCallback niet bestond, de analysegegevens onmiddellijk moesten worden verzonden. In een productietoepassing is het echter waarschijnlijk beter om het verzenden uit te stellen met een time-out om ervoor te zorgen dat dit niet conflicteert met eventuele interacties en jank veroorzaakt.

RequestIdleCallback gebruiken om DOM-wijzigingen aan te brengen

Een andere situatie waarin requestIdleCallback de prestaties echt kan helpen, is wanneer u niet-essentiële DOM-wijzigingen moet aanbrengen, zoals het toevoegen van items aan het einde van een steeds groter wordende, lui geladen lijst. Laten we eens kijken hoe requestIdleCallback daadwerkelijk in een typisch frame past.

Een typisch kader.

Het is mogelijk dat de browser het te druk heeft om callbacks in een bepaald frame uit te voeren, dus u moet niet verwachten dat er aan het einde van een frame enige vrije tijd is om nog meer werk te doen. Dat maakt het anders dan zoiets als setImmediate , dat wel per frame draait.

Als de callback aan het einde van het frame wordt geactiveerd, wordt deze gepland nadat het huidige frame is vastgelegd, wat betekent dat stijlwijzigingen zijn toegepast en, belangrijker nog, de lay-out is berekend. Als we DOM-wijzigingen aanbrengen binnen de inactieve callback, worden die lay-outberekeningen ongeldig. Als er enige vorm van lay-out wordt gelezen in het volgende frame, bijvoorbeeld getBoundingClientRect , clientWidth , enz., zal de browser een Forced Synchronous Layout moeten uitvoeren, wat een potentieel prestatieknelpunt is.

Een andere reden waarom geen DOM-wijzigingen in de inactieve callback worden geactiveerd, is dat de tijdsimpact van het wijzigen van de DOM onvoorspelbaar is, en als zodanig kunnen we gemakkelijk voorbij de deadline gaan die de browser heeft opgegeven.

De beste praktijk is om alleen DOM-wijzigingen aan te brengen binnen een requestAnimationFrame -callback, omdat deze door de browser wordt gepland met dat soort werk in gedachten. Dat betekent dat onze code een documentfragment moet gebruiken, dat vervolgens kan worden toegevoegd aan de volgende requestAnimationFrame -callback. Als u een VDOM-bibliotheek gebruikt, zou u requestIdleCallback gebruiken om wijzigingen aan te brengen, maar u zou de DOM-patches toepassen in de volgende requestAnimationFrame -callback, en niet in de inactieve callback.

Laten we, met dat in gedachten, eens naar de code kijken:

function processPendingElements (deadline) {

    // If there is no deadline, just run as long as necessary.
    if (typeof deadline === 'undefined')
    deadline = { timeRemaining: function () { return Number.MAX_VALUE } };

    if (!documentFragment)
    documentFragment = document.createDocumentFragment();

    // Go for as long as there is time remaining and work to do.
    while (deadline.timeRemaining() > 0 && elementsToAdd.length > 0) {

    // Create the element.
    var elToAdd = elementsToAdd.pop();
    var el = document.createElement(elToAdd.tag);
    el.textContent = elToAdd.content;

    // Add it to the fragment.
    documentFragment.appendChild(el);

    // Don't append to the document immediately, wait for the next
    // requestAnimationFrame callback.
    scheduleVisualUpdateIfNeeded();
    }

    // Check if there are more events still to send.
    if (elementsToAdd.length > 0)
    scheduleElementCreation();
}

Hier maak ik het element en gebruik ik de eigenschap textContent om het in te vullen, maar de kans is groot dat de code voor het maken van je element meer betrokken zou zijn! Na het maken van het element wordt scheduleVisualUpdateIfNeeded aangeroepen, waardoor een enkele requestAnimationFrame -callback wordt ingesteld die op zijn beurt het documentfragment aan de hoofdtekst toevoegt:

function scheduleVisualUpdateIfNeeded() {

    if (isVisualUpdateScheduled)
    return;

    isVisualUpdateScheduled = true;

    requestAnimationFrame(appendDocumentFragment);
}

function appendDocumentFragment() {
    // Append the fragment and reset.
    document.body.appendChild(documentFragment);
    documentFragment = null;
}

Als alles goed gaat, zullen we nu veel minder fouten zien bij het toevoegen van items aan de DOM. Uitstekend!

Veelgestelde vragen

  • Is er een polyfill? Helaas niet, maar er is een vulstuk als je een transparante omleiding naar setTimeout wilt hebben. De reden dat deze API bestaat, is omdat deze een zeer reële leemte in het webplatform opvult. Het is moeilijk om een ​​gebrek aan activiteit af te leiden, maar er bestaan ​​geen JavaScript-API's om de hoeveelheid vrije tijd aan het einde van het frame te bepalen, dus in het beste geval moet je gokken. API's zoals setTimeout , setInterval of setImmediate kunnen worden gebruikt om werk te plannen, maar ze zijn niet getimed om gebruikersinteractie te voorkomen op de manier waarop requestIdleCallback dat doet.
  • Wat gebeurt er als ik de deadline overschrijd? Als timeRemaining() nul retourneert, maar u ervoor kiest langer te blijven werken, kunt u dit doen zonder bang te hoeven zijn dat de browser uw werk zal onderbreken. De browser geeft u echter de deadline om te proberen een soepele ervaring voor uw gebruikers te garanderen, dus tenzij er een hele goede reden is, moet u zich altijd aan de deadline houden.
  • Is er een maximale waarde die timeRemaining() zal retourneren? Ja, momenteel is het 50 ms. Wanneer u probeert een responsieve applicatie te onderhouden, moeten alle reacties op gebruikersinteracties onder de 100 ms blijven. Als de gebruiker interactie heeft, zou het venster van 50 ms in de meeste gevallen de inactieve callback moeten voltooien en de browser moeten laten reageren op de interacties van de gebruiker. Mogelijk krijgt u meerdere inactieve callbacks achter elkaar gepland (als de browser bepaalt dat er voldoende tijd is om ze uit te voeren).
  • Is er enig werk dat ik niet zou moeten doen in een requestIdleCallback? Idealiter bestaat het werk dat u doet uit kleine stukjes (microtaken) die relatief voorspelbare kenmerken hebben. Het wijzigen van de DOM in het bijzonder zal bijvoorbeeld onvoorspelbare uitvoeringstijden hebben, omdat dit stijlberekeningen, lay-out, schilderen en composities zal veroorzaken. Daarom moet u alleen DOM-wijzigingen aanbrengen in een requestAnimationFrame -callback, zoals hierboven voorgesteld. Een ander ding waar u op moet letten is het oplossen (of afwijzen) van beloften, aangezien de callbacks onmiddellijk worden uitgevoerd nadat de inactieve callback is voltooid, zelfs als er geen tijd meer over is.
  • Krijg ik altijd een requestIdleCallback aan het einde van een frame? Nee, niet altijd. De browser plant het terugbellen wanneer er vrije tijd is aan het einde van een frame, of in perioden waarin de gebruiker inactief is. U moet niet verwachten dat de callback per frame wordt aangeroepen, en als u wilt dat deze binnen een bepaald tijdsbestek wordt uitgevoerd, moet u gebruik maken van de time-out.
  • Kan ik meerdere requestIdleCallback callbacks hebben? Ja, dat kan, net zoals u meerdere requestAnimationFrame -callbacks kunt hebben. Het is echter de moeite waard om te onthouden dat als uw eerste terugbelactie de resterende tijd tijdens het terugbellen opgebruikt, er geen tijd meer over is voor andere terugbelgesprekken. De andere callbacks moeten dan wachten tot de browser weer inactief is voordat ze kunnen worden uitgevoerd. Afhankelijk van het werk dat u probeert gedaan te krijgen, kan het beter zijn om slechts één inactieve callback te hebben en het werk daarin te verdelen. Als alternatief kunt u gebruik maken van de time-out om ervoor te zorgen dat terugbellen geen tijd kost.
  • Wat gebeurt er als ik een nieuwe inactieve callback in een andere instel? De nieuwe inactieve callback zal zo snel mogelijk worden uitgevoerd, beginnend bij het volgende frame (in plaats van het huidige frame).

Rust aan!

requestIdleCallback is een geweldige manier om ervoor te zorgen dat u uw code kunt uitvoeren, maar zonder de gebruiker in de weg te lopen. Het is eenvoudig te gebruiken en zeer flexibel. Het is echter nog een beginperiode en de specificaties zijn nog niet volledig geregeld, dus alle feedback die je hebt is welkom.

Bekijk het eens in Chrome Canary, geef er een draai aan voor uw projecten en laat ons weten hoe het met u gaat!