Uzun görevleri optimize edin

"Ana iş parçacığını engellemeyin" ve "uzun süren görevlerinizi bölün" dendiğini duymuşsunuzdur. Peki bunları yapmak ne anlama geliyor?

Yayınlanma tarihi: 30 Eylül 2022, Son güncelleme tarihi: 19 Aralık 2024

JavaScript uygulamalarını hızlı tutmak için verilen yaygın tavsiyeler genellikle aşağıdaki tavsiyelere dayanır:

  • "Ana iş parçacığını engellemeyin."
  • "Uzun süren görevlerinizi bölün."

Bu harika bir tavsiye ama ne tür bir çalışma gerektiriyor? Daha az JavaScript göndermek iyidir ancak bu, otomatik olarak daha duyarlı kullanıcı arayüzleri anlamına gelir mi? Belki de öyle değildir.

JavaScript'te görevleri nasıl optimize edeceğinizi 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ığı her türlü ayrı iş parçasıdır. Bu işler arasında oluşturma, HTML ve CSS'yi ayrıştırma, JavaScript çalıştırma ve doğrudan kontrolünüzün olmayabileceği diğer iş türleri yer alır. Tüm bunlar arasında, yazdığınız JavaScript belki de en büyük görev kaynağıdır.

Chrome Geliştirici Araçları'nın performans profil oluşturucusunda gösterilen bir görevin görselleştirmesi. Görev, bir yığının en üstünde yer alır ve altında bir tıklama etkinliği işleyici, bir işlev çağrısı ve daha fazla öğe bulunur. Görevde, sağ tarafta bazı oluşturma çalışmaları da yer alıyor.
Chrome Geliştirici Araçları'nın performans profil oluşturucusunda gösterilen, click etkinlik işleyici tarafından başlatılan bir görev.

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

  • Tarayıcı, başlangıç sırasında bir JavaScript dosyası indirdiğinde, daha sonra yürütülebilmesi için bu JavaScript'i ayrıştırıp derleyecek görevleri sıraya alır.
  • Sayfanın ömrü boyunca diğer zamanlarda, JavaScript etkinlik işleyiciler, JavaScript tabanlı animasyonlar ve analiz toplama gibi arka plan etkinlikleri aracılığıyla etkileşimlere yanıt verme gibi işlemler yaptığında görevler sıraya alınır.

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

Ana iş parçacığı nedir?

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

Ana iş parçacığı aynı anda yalnızca bir görevi işleyebilir. 50 milisaniyeden uzun süren tüm görevler uzun süren görev olarak kabul edilir. 50 milisaniyeyi aşan görevlerde, görevin toplam süresinden 50 milisaniye çıkarıldığında elde edilen süre, görevin engelleme süresi olarak bilinir.

Tarayıcı, herhangi bir uzunluktaki 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 süren görev varken bir sayfayla etkileşim kurmaya çalıştığında kullanıcı arayüzü yanıt vermiyor gibi görünür ve ana iş parçacığı çok uzun süre engellenirse bozulabilir.

Chrome Geliştirici Araçları'nın performans profil oluşturucusunda uzun süren bir görev. Görevin engelleyici kısmı (50 milisaniyeden uzun), kırmızı çapraz çizgilerden oluşan bir desenle gösterilir.
Chrome'un performans profil oluşturucusunda gösterilen uzun süren bir görev. Uzun süren görevler, görevin köşesinde kırmızı bir üçgenle gösterilir. Görevin engelleyici kısmı, çapraz kırmızı çizgilerden oluşan bir desenle doldurulur.

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

Tek bir uzun süren görev ile daha kısa görevlere bölünmüş aynı görev. Uzun süren görev büyük bir dikdörtgen, parçalara ayrılmış görev ise toplamda uzun süren görevle aynı genişlikte olan beş küçük kutudur.
Tek bir uzun süren görevin, beş kısa göreve bölünmüş haliyle karşılaştırmalı görseli.

Bu önemlidir çünkü görevler bölündüğünde tarayıcı, kullanıcı etkileşimleri de dahil olmak üzere daha yüksek öncelikli çalışmalara çok daha erken yanıt verebilir. Ardından, kalan görevler tamamlanana kadar çalıştırılır ve başlangıçta sıraya aldığınız işlerin tamamlanması sağlanır.

Bir görevi bölmenin kullanıcı etkileşimini nasıl kolaylaştırabileceğini gösteren bir görsel. Üstte, uzun süren bir görev, görev tamamlanana kadar bir etkinlik işleyicinin çalışmasını engeller. Altta, parçalara ayrılmış görev, etkinlik işleyicinin normalde çalışacağından 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ğunu gösteren bir görselleştirme ve daha uzun görevlerin daha küçük görevlere bölündüğü durum.

Yukarıdaki şeklin üst kısmında, kullanıcı etkileşimi tarafından sıraya alınan bir etkinlik işleyicinin başlayabilmesi için tek bir uzun süren görevi beklemesi gerekir. Bu durum, etkileşimin gerçekleşmesini geciktirir. Bu senaryoda kullanıcı gecikme fark etmiş olabilir. Alttaki örnekte ise 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, bunu JavaScript'te nasıl yapacağınızı öğrenebilirsiniz.

Görev yönetimi stratejileri

Yazılım mimarisinde yaygın olarak verilen bir tavsiye, çalışmanızı daha küçük işlevlere bölmektir:

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

Bu örnekte, bir formu doğrulamak, dönen simge 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.

Kavramsal olarak saveSettings() iyi tasarlanmış. Bu işlevlerden birinde hata ayıklamanız gerekiyorsa her işlevin ne yaptığını anlamak için proje ağacını inceleyebilirsiniz. İşleri bu şekilde bölmek, projelerin yönetilmesini ve sürdürülmesini kolaylaştırır.

Ancak burada olası bir sorun, JavaScript'in bu işlevlerin her birini ayrı görevler olarak çalıştırmamasıdır. Bunun nedeni, bu işlevlerin saveSettings() işlevi içinde yürütülmesidir. Bu, beş işlevin de tek bir görev olarak çalıştırılacağı anlamına gelir.

Chrome'un performans profil oluşturucusunda gösterilen saveSettings işlevi. Üst düzey işlev beş farklı işlevi çağırsa da tüm işlemler tek bir uzun süren görevde gerçekleşir. Bu nedenle, işlevin çalıştırılmasıyla elde edilen ve kullanıcı tarafından görülebilen sonuç, tüm işlemler tamamlanana kadar görünmez.
Beş işlevi çağıran tek bir işlev saveSettings(). İş, tek bir uzun monolitik görev olarak çalıştırılır ve beş işlevin tümü tamamlanana kadar tüm görsel yanıtlar engellenir.

En iyi senaryoda, bu işlevlerden yalnızca biri bile görevin toplam süresine 50 milisaniye veya daha fazla katkıda bulunabilir. En kötü durumda, bu görevlerin daha fazlası çok daha uzun sürebilir (özellikle kaynak kısıtlaması olan cihazlarda).

Bu durumda saveSettings(), kullanıcı tıklamasıyla tetiklenir. Tarayıcı, işlevin tamamı çalıştırılana kadar yanıt gösteremediğinden bu uzun süren görevin sonucu, yavaş ve yanıt vermeyen bir kullanıcı arayüzü olur ve zayıf bir Interaction to Next Paint (INP) olarak ölçülür.

Kod yürütmeyi manuel olarak erteleme

Kullanıcılara yönelik önemli görevlerin ve kullanıcı arayüzü yanıtlarının daha düşük öncelikli görevlerden önce gerçekleşmesini sağlamak için çalışmanızı kısa bir süre kesintiye uğratarak tarayıcıya daha önemli görevleri çalıştırma fırsatı verebilir ve ana iş parçacığına geçebilirsiniz.

Geliştiricilerin görevleri daha küçük görevlere bölmek için kullandığı yöntemlerden biri setTimeout(). Bu teknikte, işlevi setTimeout() öğesine iletirsiniz. Bu, 0 zaman aşımı süresi belirtmiş olsanız bile geri çağırmanın 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şlem yielding olarak bilinir ve sırayla çalışması gereken bir dizi işlev için en iyi sonucu verir.

Ancak kodunuz her zaman bu şekilde düzenlenmeyebilir. Örneğin, bir döngüde 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 açısından sorunludur ve beş iç içe yerleştirilmiş setTimeout() turundan sonra tarayıcı, her ek setTimeout() için minimum 5 milisaniyelik bir gecikme uygulamaya başlar.

setTimeout, ana iş parçacığına öncelik verme konusunda başka bir dezavantaja daha sahiptir: setTimeout kullanarak kodun sonraki bir görevde çalışmasını erteleyerek ana iş parçacığına öncelik verdiğinizde bu görev, sıranın sonuna eklenir. Bekleyen başka görevler varsa bunlar ertelenen kodunuzdan önce çalıştırılır.

Özel bir verim API'si: scheduler.yield()

Browser Support

  • Chrome: 129.
  • Edge: 129.
  • Firefox: 142.
  • Safari: not supported.

Source

scheduler.yield(), tarayıcıda ana iş parçacığına geçiş yapmak için özel olarak tasarlanmış bir API'dir.

Dil düzeyinde bir söz dizimi veya özel bir yapı değildir. scheduler.yield(), gelecekteki bir görevde çözülecek bir Promise döndüren bir işlevdir. Bu Promise çözümlendikten sonra çalıştırılmak üzere zincirlenen tüm kodlar (açık bir .then() zincirinde veya eşzamansız bir işlevde await edildikten sonra) gelecekteki bu görevde çalıştırılır.

Uygulamada: Bir await scheduler.yield() eklediğinizde işlev, yürütmeyi o noktada duraklatır ve ana iş parçacığına geçer. İşlevin geri kalanının (işlevin devamı olarak adlandırılır) yürütülmesi, yeni bir etkinlik döngüsü görevinde çalıştırılacak şekilde planlanır. Bu görev başladığında beklenen Promise çözümlenir ve işlev, kaldığı yerden yürütülmeye devam eder.

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

  // Yield to the main thread:
  await scheduler.yield()

  // Work that isn't user-visible, continued in a separate task:
  saveToDatabase();
  sendAnalytics();
}
Chrome'un performans profil oluşturucusunda gösterilen saveSettings işlevi artık iki göreve ayrılmıştır. İlk görev iki işlevi çağırır, ardından sonuç verir. Böylece düzen ve boyama işlemleri gerçekleşir ve kullanıcıya görünür bir yanıt verilir. Sonuç olarak, tıklama etkinliği çok daha hızlı bir şekilde 64 milisaniyede tamamlanır. İkinci görev, son üç işlevi çağırır.
saveSettings() işlevinin yürütülmesi artık iki göreve ayrılmıştır. Sonuç olarak, düzen ve boyama görevler arasında çalışabilir. Bu sayede, artık çok daha kısa olan işaretçi etkileşimiyle ölçüldüğü üzere kullanıcıya daha hızlı bir görsel yanıt verilir.

Ancak scheduler.yield()'in diğer yield yaklaşımlarına göre asıl avantajı, devamlılığının öncelikli olmasıdır. Yani bir görevin ortasında yield yaparsanız mevcut görevin devamı, diğer benzer görevler başlatılmadan önce çalıştırılır.

Bu sayede, diğer görev kaynaklarından gelen kodların (ör. üçüncü taraf komut dosyalarından gelen görevler) kodunuzun yürütme sırasını kesintiye uğratması önlenir.

Yürütmeyi duraklatmayan, duraklatan ve duraklatıp devam ettiren görevleri gösteren üç diyagram. Kontrolü devretme işlemi yapılmadığında uzun süren görevler oluşur. Yürütmeyi durdurma yönteminde daha fazla sayıda kısa görev vardır ancak bunlar ilgisiz başka görevler tarafından kesintiye uğratılabilir. Yürütmeyi durdurma ve devam ettirme sayesinde daha kısa olan daha fazla görev vardır ancak yürütme sırası korunur.
scheduler.yield() kullandığınızda, devam eden işlem diğer görevlere geçmeden önce kaldığı yerden devam eder.

Tarayıcılar arası destek

scheduler.yield() henüz tüm tarayıcılarda desteklenmediğinden yedek bir çözüm gerekir.

Çözümlerden biri, scheduler-polyfill'i derlemenize eklemektir. Ardından scheduler.yield() doğrudan kullanılabilir. Polyfill, diğer görev planlama işlevlerine geri dönme işlemini gerçekleştirir. Böylece, tarayıcılar arasında benzer şekilde çalışır.

Alternatif olarak, scheduler.yield() kullanılamıyorsa yedek olarak Promise'e sarmalanmış setTimeout kullanılarak birkaç satırda daha az karmaşık bir sürüm yazılabilir.

function yieldToMain () {
  if (globalThis.scheduler?.yield) {
    return scheduler.yield();
  }

  // Fall back to yielding with setTimeout.
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

scheduler.yield() desteği olmayan tarayıcılar öncelikli devamlılık elde etmese de tarayıcının yanıt vermeye devam etmesini sağlar.

Son olarak, kodunuzun devamı önceliklendirilmezse ana iş parçacığına geçiş yapamayacağı durumlar olabilir (örneğin, geçişin bir süre boyunca işi tamamlamama riski taşıdığı, meşgul olduğu bilinen bir sayfa). Bu durumda scheduler.yield(), bir tür progresif geliştirme olarak değerlendirilebilir: scheduler.yield()'nın kullanılabildiği tarayıcılarda verim elde edilir, aksi takdirde devam edilir.

Bu, hem özellik algılama hem de tek bir mikro görevi beklemeye geri dönme yoluyla tek bir satırda yapılabilir:

// Yield to the main thread if scheduler.yield() is available.
await globalThis.scheduler?.yield?.();

scheduler.yield() ile uzun süren işleri bölün

scheduler.yield() kullanmanın bu yöntemlerinden herhangi birini kullanmanın avantajı, bunu herhangi bir async işlevinde await yapabilmenizdir.

Örneğin, çalıştırılacak bir dizi işiniz varsa ve bunlar genellikle uzun süren bir görev oluşturuyorsa görevi bölmek için yield ekleyebilirsiniz.

async function runJobs(jobQueue) {
  for (const job of jobQueue) {
    // Run the job:
    job();

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

runJobs() işleminin devamına öncelik verilir ancak kullanıcı girişine görsel olarak yanıt verme gibi daha yüksek öncelikli işlerin çalışmasına izin verilir. Bu sayede, uzun sürebilecek iş listesinin tamamlanmasını beklemek gerekmez.

Ancak bu, verim almanın verimli bir kullanımı değildir. scheduler.yield() hızlı ve verimli olsa da ek yükü vardır. jobQueue içindeki işlerden bazıları çok kısaysa ek yük, asıl işi yürütmekten daha fazla zaman harcanmasına neden olabilir.

Yaklaşımlardan biri, görevleri toplu olarak işlemek ve yalnızca son yield işleminden bu yana yeterince zaman geçtiyse aralarında yield işlemi yapmaktır. Görevlerin uzun süren görevlere dönüşmesini önlemek için genellikle 50 milisaniyelik bir son tarih belirlenir. Ancak bu süre, yanıt verme hızı ile iş sırasını tamamlama süresi arasında bir denge kurmak için ayarlanabilir.

async function runJobs(jobQueue, deadline=50) {
  let lastYield = performance.now();

  for (const job of jobQueue) {
    // Run the job:
    job();

    // If it's been longer than the deadline, yield to the main thread:
    if (performance.now() - lastYield > deadline) {
      await yieldToMain();
      lastYield = performance.now();
    }
  }
}

Sonuç olarak, işler hiçbir zaman çok uzun sürmeyecek şekilde bölünür ancak çalıştırıcı, ana iş parçacığına yalnızca yaklaşık her 50 milisaniyede bir izin verir.

Chrome Geliştirici Araçları performans panelinde gösterilen ve yürütme işlemi birden fazla göreve bölünmüş bir dizi iş işlevi
İşler birden fazla görev halinde gruplandırılır.

isInputPending() kullanma

Browser Support

  • Chrome: 87.
  • Edge: 87.
  • Firefox: not supported.
  • Safari: not supported.

Source

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

Bu sayede JavaScript, beklemede olan giriş yoksa görev sırasının sonuna geçmek yerine devam eder. Bu, Intent to Ship'te ayrıntılı olarak açıklandığı gibi, aksi takdirde ana iş parçacığına geri dönmeyebilecek siteler için etkileyici performans iyileştirmeleri sağlayabilir.

Ancak bu API'nin kullanıma sunulmasından bu yana, özellikle INP'nin kullanıma sunulmasıyla birlikte, verim alma konusundaki anlayışımız arttı. Bu API'nin kullanılmasını artık önermiyoruz. Bunun yerine, çeşitli nedenlerle girişin beklemede olup olmadığına bakılmaksızın getirmenizi öneririz:

  • isInputPending(), kullanıcının bazı durumlarda etkileşimde bulunmasına rağmen yanlışlıkla false değerini döndürebilir.
  • Görevlerin beklemesi gereken tek durum giriş değildir. Animasyonlar ve diğer düzenli kullanıcı arayüzü güncellemeleri, duyarlı bir web sayfası sunmak için aynı derecede önemli olabilir.
  • Daha sonra, scheduler.postTask() ve scheduler.yield() gibi verimle ilgili endişeleri gideren daha kapsamlı verim API'leri kullanıma sunuldu.

Sonuç

Görevleri yönetmek zor olsa da bunu yapmak, sayfanızın kullanıcı etkileşimlerine daha hızlı yanıt vermesini sağlar. Görevleri yönetmek ve önceliklendirmek için tek bir tavsiye yoktur, bunun yerine bir dizi farklı teknik vardır. Tekrar etmek gerekirse, görevleri yönetirken göz önünde bulundurmanız gereken başlıca noktalar şunlardır:

  • Kullanıcılara yönelik kritik görevler için ana iş parçacığına öncelik verin.
  • Ergonomik olarak verim almak ve öncelikli devamlılıklar elde etmek için scheduler.yield() (tarayıcılar arası yedek ile) kullanın.
  • Son olarak, işlevlerinizde mümkün olduğunca az işlem yapın.

scheduler.yield(), açık görev planlama akrabası scheduler.postTask() ve görev önceliklendirme hakkında daha fazla bilgi edinmek için Prioritized Task Scheduling API belgelerine bakın.

Bu araçlardan birini veya daha fazlasını kullanarak uygulamanızdaki işleri, kullanıcının ihtiyaçlarına öncelik verecek şekilde yapılandırabilir ve daha az kritik işlerin de yapılmasını sağlayabilirsiniz. Bu sayede daha duyarlı ve daha keyifli bir kullanıcı deneyimi sunabilirsiniz.

Bu kılavuzun teknik incelemesini yapan Philip Walton'a özel teşekkürler.