requestIdleCallback'i kullanma

Paul Lewis

Birçok site ve uygulamada, yürütülecek çok sayıda komut dosyası bulunur. JavaScript'inizin genellikle mümkün olan en kısa sürede çalıştırılması gerekir, ancak aynı zamanda kullanıcının da çalışmasını da istemezsiniz. Analiz verilerini, kullanıcı sayfayı kaydırırken gönderirseniz veya düğmelere dokunurken DOM'ye öğeler eklerseniz web uygulamanız yanıt vermeyebilir ve bu da kötü bir kullanıcı deneyimine neden olabilir.

Gerekli olmayan işleri planlamak için requestIdleCallback'i kullanma.

Neyse ki artık bu konuda size yardımcı olabilecek bir API var: requestIdleCallback. requestAnimationFrame uygulamasını kullanmak, animasyonları düzgün bir şekilde planlamamıza ve 60 fps'ye ulaşma şansımızı en üst düzeye çıkarmamıza olanak tanıdığı gibi, requestIdleCallback bir karenin sonunda boş zaman olduğunda veya kullanıcı etkin olmadığında çalışmayı planlar. Bu, çalışmalarınızı kullanıcıya engellemeden yapma fırsatının olduğu anlamına gelir. Chrome 47 sürümünden itibaren kullanılabilir, böylece Chrome Canary'yi kullanarak hemen deneyebilirsiniz! Bu, deneysel bir özelliktir ve spesifikasyon değişmeye devam ettiği için gelecekte bazı şeyler değişebilir.

requestIdleCallback'i neden kullanmalıyım?

Zorunlu olmayan işleri kendi başınıza planlamak oldukça zordur. requestAnimationFrame geri çağırması yürütüldükten sonra stil hesaplamaları, düzen, boyama ve çalıştırılması gereken diğer tarayıcı dahili öğeleri olduğu için tam olarak ne kadar kare süresinin kaldığını öğrenmek mümkün değildir. Evde gösterilen reklamlar bunların hiçbirini hesaba katmaz. Kullanıcının herhangi bir şekilde etkileşimde olmadığından emin olmak için, işlevsellik için ihtiyaç duymasanız bile her tür etkileşim etkinliğine (scroll, touch, click) işleyiciler eklemeniz gerekir. Böylece kullanıcının etkileşim kurmadığından kesinlikle emin olabilirsiniz. Öte yandan tarayıcı, karenin sonunda tam olarak ne kadar süre kaldığını ve kullanıcının etkileşime geçip geçmediğini bilir. Bu nedenle requestIdleCallback aracılığıyla boş zamanı mümkün olan en verimli şekilde değerlendirmemizi sağlayan bir API elde ederiz.

Şimdi, biraz daha ayrıntılı bir şekilde inceleyelim ve bundan nasıl yararlanabileceğimizi görelim.

requestIdleCallback kontrol ediliyor

requestIdleCallback daha yolun başında olduğu için kullanmaya başlamadan önce kullanılabilir olup olmadığını kontrol etmelisiniz:

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

Ayrıca, davranışını değiştirerek de setTimeout işlevini geri alabilirsiniz:

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

requestIdleCallback gibi boşta kalma süresini bilmediğinden setTimeout kullanmak çok iyi bir fikir değildir. Ancak, requestIdleCallback kullanılamıyorsa işlevinizi doğrudan çağıracağınız için bu şekilde devam etmeniz çok daha iyi olmaz. requestIdleCallback için dolgu kullanıldığında, sesli aramalarınız sessizce yönlendirilir ve bu iyi bir gelişmedir.

Ama şimdilik bunun var olduğunu varsayalım.

requestIdleCallback Kullanma

requestIdleCallback çağrısı, ilk parametresi olarak bir geri çağırma işlevi alması açısından requestAnimationFrame ile çok benzerdir:

requestIdleCallback(myNonEssentialWork);

myNonEssentialWork çağrıldığında, kendisine bir deadline nesnesi verilir. Bu nesne, çalışmanız için ne kadar zaman kaldığını belirten bir sayı döndüren bir işlev içerir:

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

timeRemaining işlevi, en son değeri almak için çağrılabilir. timeRemaining() sıfır döndürdüğünde, yapılacak başka işleriniz varsa bir requestIdleCallback daha planlayabilirsiniz:

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

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

İşlevinizin garanti edilmesi

İşler gerçekten çok yoğunsa ne yaparsınız? Geri aramanızın hiçbir zaman aranmayabileceği konusunda endişeleniyor olabilirsiniz. requestIdleCallback, requestAnimationFrame özelliğine benzese de isteğe bağlı ikinci bir parametre, yani zaman aşımı özelliğine sahip seçenekler nesnesi olması bakımından farklıdır. Bu zaman aşımı ayarlanırsa tarayıcıya geri çağırmayı yürütmesi gereken süreyi milisaniye cinsinden verir:

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

Geri çağırma işleminiz, zaman aşımı tetiklenmesi nedeniyle yürütülürse iki şey görürsünüz:

  • timeRemaining(), sıfır değerini döndürür.
  • deadline nesnesinin didTimeout özelliği doğru olur.

didTimeout maddesinin doğru olduğunu görürseniz büyük olasılıkla çalışmayı yürütüp süreci tamamlamak isteyeceksinizdir:

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

Bu zaman aşımının kullanıcılarınıza neden olabileceği olası kesintiler nedeniyle (bu işlem uygulamanızın yanıt vermemesine veya titremesine neden olabilir) bu parametreyi ayarlarken dikkatli olun. Mümkünse tarayıcının, geri çağırma işlemini ne zaman yapacağına karar vermesine izin verin.

Analiz verilerini göndermek için requestIdleCallback kullanma

Analiz verilerini göndermek için requestIdleCallback hizmetinin nasıl kullanıldığına göz atalım. Bu durumda, muhtemelen gezinme menüsüne dokunma gibi bir etkinliği izlemek isteriz. Ancak, normalde ekranda canlandırıldığı için bu etkinliğin Google Analytics'e hemen gönderilmesini istemeyiz. Gönderilecek ve ileride gönderilmesini isteyeceğimiz bir etkinlik dizisi oluşturacağız:

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

Şimdi beklemedeki etkinlikleri işlemek için requestIdleCallback kullanmamız gerekiyor:

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

Burada 2 saniyelik bir zaman aşımı belirlediğimi görebilirsiniz, ancak bu değer uygulamanıza bağlı olacaktır. Analiz verileri söz konusu olduğunda, verilerin yalnızca gelecekteki bir zaman yerine makul bir zaman aralığında raporlandığından emin olmak için bir zaman aşımının kullanılması mantıklıdır.

Son olarak, requestIdleCallback tarafından çalıştırılacak fonksiyonu yazmamız gerekir.

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

Bu örnekte, requestIdleCallback olmasaydı analiz verilerinin hemen gönderilmesi gerektiğini düşündüm. Ancak bir üretim uygulamasında, herhangi bir etkileşimle çakışmaması ve soruna neden olmaması için gönderme zamanını zaman aşımıyla geciktirmek muhtemelen daha iyi olacaktır.

DOM değişiklikleri yapmak için requestIdleCallback kullanma

requestIdleCallback ürününün performansa gerçekten yardımcı olabileceği bir başka durum da, sürekli büyüyen, geç yüklenen bir listenin sonuna öğe eklemek gibi gerekli olmayan DOM değişikliklerinin yapılmasıdır. requestIdleCallback öğesinin tipik bir kareye nasıl sığdığına bakalım.

Sıradan bir kare.

Tarayıcı belirli bir çerçevede geri çağırma yapamayacak kadar meşgul olabilir. Bu nedenle, bir karenin sonunda başka iş yapmak için hiç boş bir zaman olmasını beklememelisiniz. Bu, kare başına çalışan setImmediate gibi bir değerden farklıdır.

Geri çağırma, çerçevenin sonunda tetiklenirse mevcut kare kaydedildikten sonra gerçekleşecek şekilde planlanır. Bu, stil değişikliklerinin uygulandığı ve daha da önemlisi, düzenin hesaplanacağı anlamına gelir. Boşta geri çağırmanın içinde DOM değişiklikleri yaparsak bu düzen hesaplamaları geçersiz hale gelir. Bir sonraki karede herhangi bir düzen okuması varsa (ör. getBoundingClientRect, clientWidth vb. için tarayıcının Zorunlu Senkronize Düzen'i gerçekleştirmesi gerekir. Bu durum, performans sorunu yaşanmasına yol açabilir.

Boşta geri çağırma işlevinde DOM değişikliklerini tetiklememenin bir diğer nedeni de, DOM'yi değiştirmenin zaman üzerindeki etkisinin öngörülememesidir. Böylece, tarayıcının sağladığı son tarihi kolayca aşabiliriz.

En iyi uygulama, DOM değişikliklerinin yalnızca bir requestAnimationFrame geri çağırması içinde yapılmasıdır. Çünkü işlem, tarayıcı tarafından bu tür işler göz önünde bulundurularak planlanmıştır. Bu, kodumuzun sonraki requestAnimationFrame geri çağırmasına eklenebilecek bir doküman parçası kullanması gerektiği anlamına gelir. VDOM kitaplığı kullanıyorsanız değişiklik yapmak için requestIdleCallback uzantısını kullanırsınız. Ancak DOM yamalarını, sonraki requestAnimationFrame geri çağırmasında (boşta geri çağırmada değil) uygulamanız gerekir.

Bunu göz önünde bulundurarak koda göz atalım:

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

Burada öğeyi oluşturuyor ve doldurmak için textContent özelliğini kullanıyorum ancak muhtemelen öğe oluşturma kodunuz daha fazla işin içine girecektir. scheduleVisualUpdateIfNeeded öğesi oluşturulduktan sonra çağrılır. Bu işlem, doküman parçasını gövdeye ekleyecek tek bir requestAnimationFrame geri çağırması oluşturur:

function scheduleVisualUpdateIfNeeded() {

    if (isVisualUpdateScheduled)
    return;

    isVisualUpdateScheduled = true;

    requestAnimationFrame(appendDocumentFragment);
}

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

Her şey yolundaysa artık DOM'ye öğe eklerken çok daha az sorun olacağını göreceğiz. Mükemmel!

SSS

  • Çoklu dolgu var mı? Maalesef değil, ancak setTimeout öğesine şeffaf bir yönlendirme yapmak istiyorsanız bir dolgu vardır. Bu API'nin var olmasının nedeni, web platformunda büyük bir boşluğu kapatmasıdır. Etkinlik eksikliğinin olduğunu tahmin etmek zordur, ancak çerçevenin sonundaki boş süreyi belirleyecek JavaScript API'si yoktur, bu yüzden en iyi tahminlerde bulunmanız gerekir. setTimeout, setInterval veya setImmediate gibi API'ler işi planlamak için kullanılabilir, ancak bunlar, requestIdleCallback gibi kullanıcı etkileşimini önlemek için zaman aşımına uğramaz.
  • Son tarihi aşarsam ne olur? timeRemaining() değeri sıfır döndürürse ancak daha uzun süre çalışmayı seçerseniz tarayıcının çalışmayı durdurmasından korkmadan bunu yapabilirsiniz. Ancak tarayıcı, kullanıcılarınıza sorunsuz bir deneyim sunmanız için size bir son tarih verir. Dolayısıyla, çok iyi bir neden olmadığı sürece her zaman son tarihe uymalısınız.
  • timeRemaining() öğesinin döndüreceği maksimum değer var mı? Evet, şu anda 50 ms. Duyarlı uygulama sunmaya devam ederken kullanıcı etkileşimlerine verilen tüm yanıtlar 100 ms'nin altında tutulmalıdır. Kullanıcı 50 ms. penceresiyle etkileşime geçerse çoğu durumda boşta geri çağırmanın tamamlanmasına ve tarayıcının kullanıcı etkileşimlerine yanıt vermesine izin vermelidir. Arka arkaya planlanan birden fazla boşta geri çağırma alabilirsiniz (tarayıcı, bunları çalıştırmak için yeterli zaman olduğunu belirlerse).
  • requestIdleCallback üzerinde yapmam gereken herhangi bir çalışma var mı? İdeal olarak, yaptığınız işi nispeten tahmin edilebilir özelliklere sahip küçük parçalar (mikro görevler) yapmalısınız. Örneğin, özellikle DOM'nin değiştirilmesi stil hesaplamalarını, düzeni, boyama ve birleştirmeyi tetikleyeceğinden öngörülemeyen yürütme süreleri olacaktır. Bu nedenle, yukarıda önerildiği gibi yalnızca bir requestAnimationFrame geri çağırmasında DOM değişiklikleri yapmanız gerekir. Dikkatli olunması gereken bir diğer nokta da vaatlerin çözüme kavuşturulması (veya reddedilmesidir.) çünkü geri çağırmalar, geri arama süresi kalmasa bile boşta kalma geri çağırması bittikten hemen sonra yürütülür.
  • Bir karenin sonunda her zaman requestIdleCallback olacak mı? Hayır, her zaman değil. Tarayıcı, bir karenin sonunda boş zaman olduğunda veya kullanıcının etkin olmadığı dönemlerde geri çağırmayı planlar. Geri çağırmanın kare başına çağrılmasını beklememeniz gerekir. Geri çağırmanın belirli bir zaman aralığında çalıştırılmasını istiyorsanız zaman aşımından faydalanmanız gerekir.
  • Birden fazla requestIdleCallback geri aramam olabilir mi? Evet, dilerseniz birden fazla requestAnimationFrame geri araması yapabilirsiniz. Ancak ilk geri arama işleminiz, geri arama sırasında kalan süreyi kullanırsa diğer geri çağırma işlemleri için başka zaman kalmayacağını hatırlatmak isteriz. Bu durumda diğer geri çağırma işlemlerinin çalıştırılabilmesi için tarayıcının bir sonraki boşta olana kadar beklemesi gerekir. Yapmaya çalıştığınız işe bağlı olarak, boşta bir geri çağırma olması ve işi oraya bölmesi daha iyi olabilir. Alternatif olarak, geri çağırmaların uzun süre kalmaması için zaman aşımından yararlanabilirsiniz.
  • Başka bir anahtarın içinde yeni bir boşta geri çağırma yaparsam ne olur? Yeni boşta geri çağırma, sonraki kareden (geçerli kare yerine) başlayarak mümkün olan en kısa sürede çalışacak şekilde planlanır.

Boşta kalma açık!

requestIdleCallback, kullanıcınıza engel olmadan kodunuzu çalıştırabilmenizi sağlamanın harika bir yoludur. Kullanımı kolaydır ve çok esnektir. Henüz çok erken ve spesifikasyonlar henüz netleşmedi. Bu nedenle geri bildirimlerinizi bekliyoruz.

Chrome Canary'de göz atın, projelerinizde değişiklikler yapın ve işlerin nasıl gittiğini bize bildirin!