Uzun görevleri optimize edin

Size "ana iş parçacığını engellemeyin" ve "uzun görevlerinizi bölün" denmiş olabilir ancak bunları yapmak ne anlama geliyor?

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

JavaScript uygulamalarının hızlı kalmasını sağlamak için verilen yaygın tavsiyeler genellikle şu önerilere dayanır:

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

Bu harika bir tavsiye ancak hangi işleri yapmam gerekiyor? Daha az JavaScript göndermek iyi bir şeydir ancak bu, kullanıcı arayüzlerinin daha duyarlı olacağı anlamına mı gelir? Belki, belki de değil.

JavaScript'teki 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ığı herhangi bir ayrı iş parçasıdır. Bu işler arasında oluşturma, HTML ve CSS'yi ayrıştırma, JavaScript'i çalıştırma ve doğrudan kontrol edemeyebileceğiniz diğer iş türleri yer alır. Tüm bunlar arasında, yazdığınız JavaScript kodu belki de en büyük görev kaynağıdır.

Chrome'un Geliştirici Araçları'ndaki performans profillerinde gösterilen bir görevin görselleştirilmesi. Görev, bir yığının en üstünde yer alır. Altında bir tıklama etkinliği işleyicisi, bir işlev çağrısı ve daha fazla öğe bulunur. Bu görevde sağ tarafta bazı oluşturma işlemleri de yer alıyor.
Chrome Geliştirici Araçları'nın performans profil oluşturucusunda 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'in daha sonra yürütülebilmesi için ayrıştırılıp derlenmesi gereken görevleri sıraya alır.
  • Sayfanın kullanım ömrünün diğer zamanlarında, JavaScript'in etkinlik işleyiciler aracılığıyla etkileşimlere yanıt verme, JavaScript destekli animasyonlar ve analiz toplama gibi arka plan etkinlikleri gibi işlemler yaptığı sırada 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ırıldığı ve yazdığınız JavaScript'lerin neredeyse tamamının yürütüldüğü yerdir.

Ana iş parçacığı tek seferde yalnızca bir görev işleyebilir. 50 milisaniyeden uzun süren tüm görevler uzun 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 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 varken bir sayfayla etkileşim kurmaya çalıştığında kullanıcı arayüzü yanıt vermiyormuş gibi görünür ve ana iş parçacığı çok uzun süre engellenirse arayüz bozulabilir.

Chrome'un Geliştirici Araçları'ndaki performans profil oluşturucusunda uzun bir görev. Görevin engelleme kısmı (50 milisaniyeden uzun) kırmızı diyagonal çizgilerle gösterilir.
Chrome'un performans profil oluşturucusunda gösterildiği gibi uzun bir görev. Uzun görevler, görevin köşesinde kırmızı bir üçgenle gösterilir. Görevin engellenen kısmı, çapraz kırmızı çizgilerle doldurulur.

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

Tek bir uzun görev ile aynı görevin daha kısa görevlere bölünmüş hali arasındaki fark. Uzun görev tek bir büyük dikdörtgen, parçalanmış görev ise toplu olarak uzun görevle aynı genişliğe sahip beş küçük kutudur.
Tek bir uzun görevin görselleştirilmesi ile aynı görevin beş kısa göreve bölünmüş halinin görselleştirilmesi.

Bu önemlidir. Çünkü 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. Ardından, kalan görevler tamamlanarak başlangıçta sıraya aldığınız işlerin yapılması sağlanır.

Bir görevi parçalara ayırmanın kullanıcı etkileşimini nasıl kolaylaştırabileceğini gösteren bir resim. Üst tarafta, uzun bir görev, görev tamamlanana kadar bir etkinlik işleyicinin çalışmasını engeller. En 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ğu ile daha uzun görevler daha küçük görevlere bölündüğünde etkileşimlere ne olduğunun görselleştirilmesi.

Önceki şeklin en üstünde, bir kullanıcı etkileşimi tarafından sıraya alınan bir etkinlik işleyicinin başlamadan önce tek bir uzun görevi beklemesi gerekiyordu. Bu durum, etkileşimin gerçekleşmesini geciktirir. Bu senaryoda kullanıcı gecikme fark etmiş olabilir. En altta, etkinlik işleyici daha erken çalışmaya başlayabilir ve etkileşim anlık olarak algılanabilir.

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 sıkça verilen bir tavsiye, işinizi daha küçük işlevlere bölmenizdir:

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

Bu örnekte, bir formu doğrulamak, bir yükleme animasyonu göstermek, verileri uygulama arka ucuna 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ıştır. Bu işlevlerden birinde hata ayıklamanız gerekirse her işlevin ne yaptığını anlamak için proje ağacında gezinebilirsiniz. Çalışmayı bu şekilde bölmek, projelerde gezinmeyi ve projeleri sürdürmeyi 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 tamamının tek bir görev olarak çalışacağı anlamına gelir.

Chrome'un performans profil oluşturucusunda gösterildiği gibi saveSettings işlevi. En üst düzey işlev beş işlevi daha çağırsa da tüm işlemler, işlevin çalıştırılmasının kullanıcı tarafından görülebilen sonucunun tamamlanana kadar görünmemesini sağlayan uzun bir görevde gerçekleşir.
Beş işlevi çağıran tek bir işlev saveSettings(). Çalışma, tek bir uzun monolitik görev olarak yürütülür ve beş işlevin tamamı bitene kadar 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üre çalışabilir. Bu durum özellikle kaynak kısıtlamalı cihazlarda görülür.

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

Kod yürütmeyi manuel olarak erteleme

Kullanıcıya 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, tarayıcıya daha önemli görevleri çalıştırma fırsatı vermek üzere çalışmanızı kısa süreliğine keserek ana iş parçacığına geçebilirsiniz.

Geliştiricilerin görevleri daha küçük parçalara ayırmak için kullandığı yöntemlerden biri setTimeout(). Bu teknikte işlevi setTimeout()'ya iletirsiniz. Bu, 0 zaman aşımı belirtmiş olsanız bile geri çağırma işleminin 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 verim 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, döngüde işlenmesi gereken büyük miktarda veriniz olabilir ve çok fazla yineleme 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 setTimeout() turundan sonra tarayıcı, her ek setTimeout() için en az 5 milisaniye gecikme uygulamaya başlar.

setTimeout, kontrolü bırakma konusunda da bir dezavantaja sahiptir: setTimeout kullanarak kodu sonraki bir görevde çalışacak şekilde erteleyerek ana iş parçacığına kontrolü bıraktığınızda bu görev, kuyruğun sonuna eklenir. Bekleyen başka görevler varsa bunlar, ertelenen kodunuzdan önce çalıştırılır.

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

Browser Support

  • Chrome: 129.
  • Edge: 129.
  • Firefox Technology Preview: supported.
  • Safari: not supported.

Source

scheduler.yield(), tarayıcıda ana işleme izin vermek için özel olarak tasarlanmış bir API'dir.

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

Uygulamada: 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ışacak şekilde planlanır. Bu görev başladığında beklenen söz yerine getirilir 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österildiği gibi saveSettings işlevi artık iki göreve ayrıldı. İlk görev iki işlevi çağırır, ardından düzen ve boyama işlemlerinin gerçekleşmesine ve kullanıcıya görünür bir yanıt verilmesine olanak tanıyarak sonuç verir. 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ılıyor. Bu sayede, düzen ve boyama işlemleri görevler arasında çalışabilir. Bu da kullanıcının, artık çok daha kısa olan işaretçi etkileşimiyle ölçüldüğü üzere daha hızlı bir görsel yanıt almasını sağlar.

Ancak scheduler.yield()'nın diğer getiri sağlama yaklaşımlarına kıyasla asıl avantajı, devamlılığına öncelik verilmesidir. Bu nedenle, bir görevin ortasında getiri sağlarsanız mevcut görevin devamı, 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ütülme sırasını kesintiye uğratması önlenir.

Üç diyagramda, verimsiz, verimli ve verimli olup devam eden görevler gösteriliyor. Verim olmadan uzun görevler vardır. Kontrolü bırakma ile daha kısa süren ancak diğer alakasız görevler tarafından kesintiye uğrayabilecek daha fazla görev vardır. Kontrolü bırakma ve devam ettirme ile daha kısa olan daha fazla görev vardır ancak bunların yürütülme sırası korunur.
scheduler.yield() kullandığınızda devam ettirme işlemi, 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ği için yedek bir çözüm gereklidir.

Çözümlerden biri, scheduler-polyfill öğesini derlemenize bırakmaktır. Ardından scheduler.yield() doğrudan kullanılabilir. Polyfill, diğer görev planlama işlevlerine geri dönmeyi yönetir, böylece tarayıcılarda benzer şekilde çalışır.

Alternatif olarak, scheduler.yield() kullanılamıyorsa yedek olarak yalnızca Promise'e sarılmış 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ılarda öncelikli devam ettirme özelliği kullanılamaz ancak tarayıcının yanıt vermeye devam etmesi için yine de sonuç gösterilir.

Son olarak, devamlılığı önceliklendirilmediği takdirde kodunuzun ana iş parçacığına geçemeyeceği durumlar olabilir (ör. geçişin bir süre işin tamamlanmamasına neden olabileceği bilinen yoğun bir sayfa). Bu durumda, scheduler.yield() bir tür aşamalı geliştirme olarak değerlendirilebilir: scheduler.yield()'nın kullanılabildiği tarayıcılarda sonuç verilir, aksi takdirde devam edilir.

Bu işlem, hem özellik algılama hem de tek bir mikro görevin beklenmesine geri dönme yoluyla tek satırlık bir kodla yapılabilir:

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

Uzun süren işleri scheduler.yield() ile bölme

scheduler.yield() kullanmanın avantajı, bu işlevi herhangi bir async işlevinde kullanabilmenizdir.await

Örneğin, genellikle uzun bir görevle sonuçlanan bir dizi işiniz varsa 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()'nın devam etmesine öncelik verilir ancak yine de kullanıcının girişine görsel olarak yanıt verme gibi daha yüksek öncelikli işlerin çalışmasına izin verilir. Bu sayede, potansiyel olarak uzun iş listesinin tamamlanması beklenmek zorunda kalınmaz.

Ancak bu, geliri artırmanın etkili bir yolu değildir. scheduler.yield() hızlı ve verimlidir ancak biraz ek yükü vardır. jobQueue içindeki bazı işler çok kısaysa ek yük, gerçek işi yürütmekten daha fazla zaman harcanmasına neden olabilir.

Bir yaklaşım, işleri toplu olarak işlemek ve yalnızca son verimden yeterince uzun zaman geçtiyse aralarında verim sağlamaktır. Görevlerin uzun görevlere dönüşmesini önlemek için yaygın bir son tarih 50 milisaniyedir ancak bu süre, yanıt verme hızı ile iş kuyruğunu tamamlama süresi arasında bir denge kurularak 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 koşucu, ana iş parçacığına yaklaşık 50 milisaniyede bir teslim olur.

Chrome Geliştirici Araçları performans panelinde gösterilen ve yürütülmesi birden fazla görevde parçalanmış bir dizi işlev
İşler birden fazla görev halinde gruplandırılır.

isInputPending() kullanmayın

Browser Support

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

Source

isInputPending() API, kullanıcının bir sayfayla etkileşim kurmayı deneyip denemediğini kontrol etmenin bir yolunu sunar ve yalnızca bir giriş bekleniyorsa sonuç verir.

Bu sayede, giriş beklenmiyorsa JavaScript, görev sırasının sonuna gitmek yerine devam edebilir. Bu, Intent to Ship belgesinde ayrıntılı olarak açıklandığı gibi, aksi takdirde ana iş parçacığına geri dönmeyecek sitelerde etkileyici performans iyileştirmeleri sağlayabilir.

Ancak bu API'nin kullanıma sunulmasından bu yana, özellikle INP'nin kullanıma sunulmasıyla birlikte, gelir elde etme konusundaki anlayışımız arttı. Bu API'nin kullanılmasını artık önermiyoruz. Bunun yerine, girişin beklemede olup olmadığına bakılmaksızın yield yapılmasını öneriyoruz. Bunun çeşitli nedenleri vardır:

  • isInputPending(), kullanıcının bazı durumlarda etkileşimde bulunmasına rağmen yanlışlıkla false döndürebilir.
  • Görevlerin kontrolü bırakması gereken tek durum giriş değildir. Animasyonlar ve diğer normal 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 gelir elde etmeyle ilgili endişeleri giderecek daha kapsamlı gelir elde etme API'leri kullanıma sunuldu.

Sonuç

Görevleri yönetmek zor olsa da bu sayede sayfanız kullanıcı etkileşimlerine daha hızlı yanıt verir. Görevleri yönetme ve önceliklendirme konusunda tek bir tavsiye yoktur. Bunun yerine, farklı teknikler kullanabilirsiniz. Tekrar hatırlatmak gerekirse görevleri yönetirken dikkate almanız gereken temel noktalar şunlardır:

  • Kullanıcıya yönelik kritik görevler için ana iş parçacığına öncelik verin.
  • Ergonomik olarak sonuç almak ve öncelikli devam ettirme işlemleri elde etmek için scheduler.yield() (tarayıcılar arası yedeklemeyle birlikte) kullanın.
  • Son olarak, işlevlerinizde mümkün olduğunca az işlem yapın.

scheduler.yield(), göreve göre açıkça zaman planlaması scheduler.postTask() ve görev önceliklendirme hakkında daha fazla bilgi edinmek için Öncelikli Görev Zaman Planlama API dokümanlarına bakın.

Bu araçlardan birini veya birkaçı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, daha iyi bir kullanıcı deneyimi sunarak daha hızlı yanıt veren ve daha keyifli bir kullanım sağlar.

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

Unsplash'ten alınan küçük resim, Amirali Mirhashemian'ın izniyle.