Uzun görevleri optimize edin

"Ana iş parçacığını engellemeyin" ve "uzun görevlerinizi bölün" gibi öneriler aldınız. Peki bu önerileri uygulamak ne anlama geliyor?

JavaScript uygulamalarını hızlı tutmayla ilgili yaygın öneriler genellikle aşağıdakilere dayanır:

  • "Ana ileti dizisini engellemeyin."
  • "Uzun görevlerinizi bölün."

Bu harika bir tavsiye ama ne gibi çalışmalar gerektiriyor? Daha az JavaScript göndermek iyidir ancak bu durum otomatik olarak daha duyarlı kullanıcı arayüzleriyle eş değer midir? Belki, belki de değil.

JavaScript'te görevlerin nasıl optimize edileceğini anlamak için öncelikle görevlerin ne olduğunu ve tarayıcının bunları nasıl işlediğini bilmeniz gerekir.

Görev nedir?

Görev, tarayıcının yaptığı ayrı bir çalışma parçasıdır. Bu işlemler arasında oluşturma, HTML ve CSS'yi ayrıştırma, JavaScript çalıştırma ve doğrudan kontrol sahibi olamayacağınız diğer işlem türleri yer alır. Tüm bu kaynaklar arasında, yazdığınız JavaScript belki de en büyük görev kaynağıdır.

Chrome'un Geliştirici Araçları'ndaki performans profillerinde gösterildiği gibi bir görevin görselleştirilmesi. Görev, bir yığının en üstünde yer alır. Tıklama etkinliği işleyicisi, işlev çağrısı ve daha fazla öğe bu görevin altındadır. Görev, sağ taraftaki bazı oluşturma işlemlerini de içerir.
Chrome Geliştirici Araçları'nın performans profilleyicisinde gösterilen, click etkinlik işleyicisi tarafından başlatılan bir görev.

JavaScript ile ilişkili görevler performansı birkaç şekilde etkiler:

  • Bir tarayıcı, başlangıç sırasında bir JavaScript dosyası indirdiğinde, bu JavaScript'i daha sonra çalıştırabilmek için ayrıştırma ve derleme görevlerini sıraya ekler.
  • Sayfanın kullanım süresi boyunca, JavaScript'in çalıştığı diğer zamanlarda (ör. etkinlik işleyiciler aracılığıyla etkileşimler oluşturma, JavaScript destekli animasyonlar ve analiz toplama gibi arka plan etkinlikleri) görevler sıraya alınır.

Web çalışanları ve benzer API'ler hariç tüm bu işlemler ana iş parçacığında gerçekleşir.

Ana iş parçacığı nedir?

Ana iş parçacığı, tarayıcıda çoğu görevin çalıştığı ve yazdığınız neredeyse tüm JavaScript'in yürütüldüğü yerdir.

Ana iş parçacığı tek seferde yalnızca bir görevi işleyebilir. 50 milisaniyeden uzun süren tüm görevler uzun görev olarak kabul edilir. 50 milisaniyeyi aşan görevler için, görevin toplam süresinden 50 milisaniye çıkarılan değer, görevin engelleme süresi olarak bilinir.

Tarayıcı, herhangi bir uzunlukta bir görev çalışırken etkileşimlerin gerçekleşmesini engeller ancak görevler çok uzun süre çalışmadığı sürece bu durum kullanıcı tarafından fark edilmez. Ancak, kullanıcı çok sayıda uzun görev olduğunda bir sayfayla etkileşime geçmeye çalıştığında kullanıcı arayüzü yanıt vermez ve ana iş parçacığı çok uzun süre boyunca engellenirse hatta bozuk bile görünebilir.

Chrome'un Geliştirici Araçları'ndaki performans profilleyicisinde uzun bir görev. Görevin engelleyen kısmı (50 milisaniyeden uzun), kırmızı çapraz şeritlerden oluşan bir desenle gösterilir.
Chrome'un performans profilleyicisinde gösterildiği gibi uzun bir görev. Uzun görevler, köşesinde kırmızı üçgenle gösterilir. Görevin engelleyen kısmı, çapraz kırmızı şeritlerle doldurulur.

Ana iş parçacığının çok uzun süre boyunca engellenmesini önlemek için uzun bir görevi birkaç küçük göreve bölebilirsiniz.

Tek bir uzun görev ve aynı görevin daha kısa görevlere bölünmüş hali. Uzun görev tek bir büyük dikdörtgenken, parçalara ayrılmış görev, toplu olarak uzun görevle aynı genişliğe sahip beş küçük kutudan oluşur.
Tek bir uzun görevin görselleştirmesi ve aynı görevin beş kısa göreve bölünmüş hali.

Görevler bölündüğünde tarayıcı, kullanıcı etkileşimleri de dahil olmak üzere daha yüksek öncelikli işlere çok daha erken yanıt verebilir. Bu nedenle, görevler bölünmelidir. Ardından, kalan görevler tamamlanana kadar çalıştırılır. Böylece, başlangıçta sıraya eklediğiniz işin tamamlanması sağlanır.

Bir görevi bölmenin kullanıcı etkileşimini nasıl kolaylaştırabileceğini gösteren bir resim. Üstte, uzun bir görev, görev tamamlanana kadar bir etkinlik işleyicinin çalışmasını engeller. Alt kısımda, parçalara ayrılmış görev, etkinlik işleyicinin normalden daha erken çalışmasına olanak tanır.
Görevler çok uzun olduğunda ve tarayıcı etkileşimlere yeterince hızlı yanıt veremediğinde etkileşimlere ne olduğunun, uzun görevlerin daha küçük görevlere bölündüğünde ise ne olduğunun görselleştirilmesi.

Önceki şeklin üst kısmında, bir kullanıcı etkileşimi tarafından sıraya eklenen bir etkinlik işleyicinin başlamadan önce tek bir uzun görevi beklemesi gerekiyordu. Bu, etkileşimin gerçekleşmesini geciktirir. Bu senaryoda kullanıcı gecikmeyi fark etmiş olabilir. Alt kısımda, etkinlik işleyici daha erken çalışmaya başlayabilir ve etkileşim anında gerçekleşmiş gibi hissedilebilir.

Görevleri bölmenin neden önemli olduğunu öğrendiğinize göre, JavaScript'te bunu nasıl yapacağınızı öğrenebilirsiniz.

Görev yönetimi stratejileri

Yazılım mimarisinde sık verilen bir tavsiye, çalışmanızı daha küçük işlevlere ayırmaktır:

function saveSettings () {
  validateForm
();
  showSpinner
();
  saveToDatabase
();
  updateUI
();
  sendAnalytics
();
}

Bu örnekte, bir formu doğrulamak, bir döndürme çubuğu göstermek, uygulama arka ucuna veri göndermek, kullanıcı arayüzünü güncellemek ve analiz göndermek için beş işlevi çağıran saveSettings() adlı bir işlev vardır.

saveSettings(), kavramsal olarak iyi tasarlanmış. Bu işlevlerden birinde hata ayıklama yapmanız gerekirse her işlevin ne işe yaradığını öğrenmek için proje ağacında gezinebilirsiniz. Bu şekilde bölünmüş işler, projelerde gezinmeyi ve bakımı kolaylaştırır.

Ancak buradaki olası bir sorun, JavaScript'in bu işlevlerin her birini ayrı görevler olarak çalıştırmaması, çünkü saveSettings() işlevi içinde çalıştırılıyor olmalarıdır. Bu, beş işlevin de tek bir görev olarak çalışacağı anlamına gelir.

Chrome'un performans profilleyicisinde gösterilen saveSettings işlevi. Üst düzey işlev beş başka işlevi çağırırken tüm işlemler, ana iş parçacığının çalışmasını engelleyen tek bir uzun görevde gerçekleşir.
Beş işlevi çağıran tek bir işlev saveSettings(). Çalışma, tek bir uzun monolitik görev kapsamında çalıştırılır.

En iyi durumda, bu işlevlerden yalnızca biri bile görevin toplam uzunluğuna 50 milisaniye veya daha fazla katkıda bulunabilir. En kötü durumda, bu görevlerden daha fazlası özellikle kaynak kısıtlaması olan cihazlarda çok daha uzun süre çalışabilir.

Kod yürütmeyi manuel olarak erteleme

Geliştiricilerin görevleri daha küçük görevlere bölmek için kullandığı yöntemlerden biri setTimeout()'tür. Bu teknikte işlevi setTimeout()'e iletirsiniz. Bu işlem, 0 zaman aşımı belirtseniz bile geri çağırma işlevinin yürütülmesini ayrı bir göreve erteler.

function saveSettings () {
 
// Do critical work that is user-visible:
  validateForm
();
  showSpinner
();
  updateUI
();

 
// Defer work that isn't user-visible to a separate task:
  setTimeout
(() => {
    saveToDatabase
();
    sendAnalytics
();
 
}, 0);
}

Bu işleme üretme adı verilir ve sırayla çalıştırılması gereken bir dizi işlev için en iyi şekilde çalışır.

Ancak kodunuz her zaman bu şekilde düzenlenmeyebilir. Örneğin, döngü içinde işlenmesi gereken büyük miktarda veriniz olabilir ve çok sayıda iterasyon varsa bu görev çok uzun sürebilir.

function processData () {
 
for (const item of largeDataArray) {
   
// Process the individual item here.
 
}
}

Burada setTimeout() kullanmak, geliştirici ergonomisi nedeniyle sorunludur ve her iterasyon hızlı bir şekilde çalışsa bile veri dizisinin tamamının işlenmesi çok uzun sürebilir. Tüm bu nedenlerden dolayı setTimeout(), bu iş için doğru araç değildir (en azından bu şekilde kullanıldığında).

Getirdiğiniz geliri puan olarak göstermek için async/await simgesini kullanın

Kullanıcılara yönelik önemli görevlerin, önceliği daha düşük olan görevlerden önce yapılmasını sağlamak için tarayıcıya daha önemli görevleri çalıştırma fırsatı vermek amacıyla görev kuyruğunu kısa süreliğine kesintiye uğratarak ana iş parçacığına verebilirsiniz.

Daha önce açıklandığı gibi, setTimeout ana iş parçacığına yol vermek için kullanılabilir. Ancak kolaylık ve daha iyi okunabilirlik için Promise içinde setTimeout'ü çağırabilir ve geri çağırma işlevi olarak resolve yöntemini iletebilirsiniz.

function yieldToMain () {
 
return new Promise(resolve => {
    setTimeout
(resolve, 0);
 
});
}

yieldToMain() işlevinin avantajı, herhangi bir async işlevinde await kullanabilmenizdir. Önceki örnekten yola çıkarak, çalıştırılacak bir işlev dizisi oluşturabilir ve her biri çalıştıktan sonra ana iş parçacığına verebilirsiniz:

async function saveSettings () {
 
// Create an array of functions to run:
 
const tasks = [
    validateForm
,
    showSpinner
,
    saveToDatabase
,
    updateUI
,
    sendAnalytics
 
]

 
// Loop over the tasks:
 
while (tasks.length > 0) {
   
// Shift the first task off the tasks array:
   
const task = tasks.shift();

   
// Run the task:
    task
();

   
// Yield to the main thread:
    await yieldToMain
();
 
}
}

Sonuç olarak, bir zamanlar monolit olan görev artık ayrı görevlere bölünmüştür.

Chrome'un performans profilleyicisinde gösterilen saveSettings işlevinin yalnızca yield ile birlikte kullanılan hali. Sonuç olarak, bir zamanlar tek bir parça olan görev artık her işlev için bir tane olmak üzere beş ayrı göreve bölünmüştür.
saveSettings() işlevi artık alt işlevlerini ayrı görevler olarak yürütüyor.

Özel bir planlayıcı API'si

setTimeout, görevleri bölmenin etkili bir yoludur ancak bir dezavantajı olabilir: Kodu sonraki bir görevde çalıştırılmak üzere erteleyerek ana iş parçacığına verdiğinizde, söz konusu görev kuyruğun sonuna eklenir.

Sayfanızdaki tüm kodu kontrol ediyorsanız görevlere öncelik verme özelliğine sahip kendi planlayıcınızı oluşturabilirsiniz. Ancak üçüncü taraf komut dosyaları, planlayıcınızı kullanmaz. Bu tür ortamlarda işe öncelik veremezsiniz. Yalnızca parçalara ayırabilir veya kullanıcı etkileşimlerine açıkça yol verebilirsiniz.

Tarayıcı desteği

  • Chrome: 94.
  • Edge: 94.
  • Firefox: İşaretli.
  • Safari: Desteklenmez.

Kaynak

Planlayıcı API, görevlerin daha ayrıntılı bir şekilde planlanmasına olanak tanıyan postTask() işlevini sunar. Bu işlev, tarayıcının işe öncelik vermesine yardımcı olarak düşük öncelikli görevlerin ana iş parçacığına devredilmesini sağlar. postTask(), söz vermeyi kullanır ve üç priority ayarından birini kabul eder:

  • 'background' en düşük öncelikli görevler içindir.
  • 'user-visible' orta öncelikli görevler için. priority ayarlanmamışsa varsayılan değer budur.
  • 'user-blocking', yüksek öncelikli olarak çalıştırılması gereken kritik görevler için kullanılır.

Üç görevi mümkün olan en yüksek öncelikte, kalan iki görevi ise mümkün olan en düşük öncelikte çalıştırmak için postTask() API'nin kullanıldığı aşağıdaki kodu örnek olarak alın.

function saveSettings () {
 
// Validate the form at high priority
  scheduler
.postTask(validateForm, {priority: 'user-blocking'});

 
// Show the spinner at high priority:
  scheduler
.postTask(showSpinner, {priority: 'user-blocking'});

 
// Update the database in the background:
  scheduler
.postTask(saveToDatabase, {priority: 'background'});

 
// Update the user interface at high priority:
  scheduler
.postTask(updateUI, {priority: 'user-blocking'});

 
// Send analytics data in the background:
  scheduler
.postTask(sendAnalytics, {priority: 'background'});
};

Burada, görevlerin önceliği, tarayıcı tarafından öncelik verilen görevlerin (ör. kullanıcı etkileşimleri) gerektiğinde aralarına girebileceği şekilde planlanır.

Chrome'un performans profilleyicisinde gösterildiği gibi saveSettings işlevi, ancak postTask kullanılarak. postTask, saveSettings'in çalıştırdığı her işlevi böler ve kullanıcı etkileşiminin engellenmeden çalışma şansı olacak şekilde işlevlere öncelik verir.
saveSettings() çalıştırıldığında işlev, postTask()'i kullanarak işlevleri tek tek planlar. Kullanıcıya yönelik kritik işler yüksek öncelikli olarak planlanırken, kullanıcının bilmediği işler arka planda çalışacak şekilde planlanır. Bu sayede iş hem bölünür hem de uygun şekilde önceliklendirilir. Böylece kullanıcı etkileşimleri daha hızlı yürütülür.

Bu, postTask() değerinin nasıl kullanılabileceğini gösteren basit bir örnektir. Görevler arasında öncelikleri paylaşabilecek farklı TaskController nesneleri örneklemek mümkündür. Bu sayede, gerektiğinde farklı TaskController örnekleri için öncelikleri değiştirebilirsiniz.

scheduler.yield() API'yi kullanarak devam eden yerleşik getiri

Tarayıcı desteği

  • Chrome: 129.
  • Edge: 129.
  • Firefox: Desteklenmez.
  • Safari: Desteklenmez.

Kaynak

scheduler.yield(), tarayıcıdaki ana iş parçacığına yol vermek için özel olarak tasarlanmış bir API'dir. Kullanımı, bu kılavuzun daha önceki bölümlerinde gösterilen yieldToMain() işlevine benzer:

async function saveSettings () {
 
// Create an array of functions to run:
 
const tasks = [
    validateForm
,
    showSpinner
,
    saveToDatabase
,
    updateUI
,
    sendAnalytics
 
]

 
// Loop over the tasks:
 
while (tasks.length > 0) {
   
// Shift the first task off the tasks array:
   
const task = tasks.shift();

   
// Run the task:
    task
();

   
// Yield to the main thread with the scheduler
   
// API's own yielding mechanism:
    await scheduler
.yield();
 
}
}

Bu kod büyük oranda tanıdıktır ancak yieldToMain() yerine await scheduler.yield() kullanılmıştır.

Verimi olmayan, verimi olan ve verimi olan ve devam eden görevleri gösteren üç diyagram. Verimli olmayan uzun görevler vardır. Verimli bir şekilde çalışmanın avantajı, daha kısa olan ancak alakasız diğer görevler tarafından kesintiye uğrayabilecek daha fazla görev olmasıdır. Verme ve devam etme ile daha kısa görevler olsa da bunların yürütme sırası korunur.
scheduler.yield() kullanıldığında, görev yürütme, verim noktasından sonra bile kaldığı yerden devam eder.

scheduler.yield() değerinin avantajı devamlılıktır. Yani bir görev grubunun ortasında teslim olursanız planlanmış diğer görevler, teslim noktasından sonra aynı sırayla devam eder. Bu sayede üçüncü taraf komut dosyalarındaki kod, kendi kodunuzun yürütülme sırasını kesintiye uğratmaz.

isInputPending() kullanmayın

Tarayıcı desteği

  • Chrome: 87.
  • Edge: 87.
  • Firefox: Desteklenmez.
  • Safari: Desteklenmez.

Kaynak

isInputPending() API, kullanıcının bir sayfayla etkileşim kurmaya çalışıp çalışmadığını kontrol etmenin bir yolunu sağlar ve yalnızca bir giriş beklemedeyse sonuç verir.

Bu sayede JavaScript, bekleyen giriş yoksa görev kuyruğunun en sonuna gitmek yerine çalışmaya devam eder. Bu, aksi takdirde ana iş parçacığına geri dönmeyebilecek siteler için Gönderme Niyeti bölümünde ayrıntılı olarak açıklandığı gibi etkileyici performans iyileştirmelerine neden olabilir.

Ancak bu API'nin kullanıma sunulmasından bu yana, özellikle de INP'nin kullanıma sunulmasıyla birlikte, verim hakkındaki bilgimiz arttı. Artık bu API'yi kullanmanızı önermiyoruz. Bunun yerine, aşağıdaki nedenlerden dolayı giriş beklemede olup olmadığına bakılmaksızın vermeyi öneririz:

  • isInputPending(), kullanıcının bazı durumlarda etkileşimde bulunmasına rağmen yanlışlıkla false döndürebilir.
  • Görevlerin verim sağlaması gereken tek durum giriş değildir. Animasyonlar ve diğer düzenli kullanıcı arayüzü güncellemeleri, duyarlı bir web sayfası sağlamak için eşit derecede önemli olabilir.
  • O zamandan beri, scheduler.postTask() ve scheduler.yield() gibi verim sorunlarını ele alan daha kapsamlı verim API'leri kullanıma sunulmuştur.

Sonuç

Görevleri yönetmek zordur ancak bu sayede sayfanız kullanıcı etkileşimlerine daha hızlı yanıt verebilir. Görevleri yönetmek ve önceliklendirmek için tek bir tavsiye yoktur. Bunun yerine, çeşitli teknikler vardır. Görevleri yönetirken dikkate almanız gereken temel noktaları tekrar belirtmek isteriz:

  • Kullanıcılara yönelik kritik görevler için ana iş parçacığına verin.
  • postTask() ile görevlere öncelik verin.
  • scheduler.yield() ile denemeler yapmayı düşünebilirsiniz.
  • Son olarak, işlevlerinizde mümkün olduğunca az işlem yapın.

Bu araçlardan bir veya daha fazlasını kullanarak, uygulamanızdaki çalışmaları kullanıcının ihtiyaçlarına öncelik verecek şekilde yapılandırabilir ve daha az kritik çalışmaların da yapılmasını sağlayabilirsiniz. Bu sayede daha duyarlı ve kullanımı daha keyifli bir kullanıcı deneyimi oluşturabilirsiniz.

Bu kılavuzu teknik olarak inceleyen Philip Walton'a özel teşekkürler.

Amirali Mirhashemian tarafından sağlanan ve Unsplash'tan alınan küçük resim.