Uzun görevleri optimize edin

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

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

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şimlere yanıt verme, 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 DevTools'undaki 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ı bir üçgenle gösterilir. Görevin engelleyen kısmı, çapraz kırmızı şeritlerden oluşan bir desenle 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örev daha kısa görevlere bölünmüş halde. 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. İşleri bu şekilde bölmek, projelerde gezinmeyi ve projeleri sürdürmeyi 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 tek bir uzun görevde gerçekleşir. Bu da işlevin çalıştırılmasının kullanıcı tarafından görülebilen sonucunun, tüm işlemler tamamlanana kadar görünmemesini sağlar.
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 ve beş işlevin tümü tamamlanana kadar görsel yanıtları engeller.

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.

Bu durumda saveSettings(), kullanıcı tıklaması tarafından tetiklenir ve işlevin tamamı çalıştırılıncaya kadar tarayıcı yanıt gösteremez. Bu uzun görevin sonucu olarak yavaş ve yanıt vermeyen bir kullanıcı arayüzü ortaya çıkar ve bu durum kötü bir Sonraki Boyamayla Etkileşim (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, öncelikliliğ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 çalışmanızı kısa bir süre kesintiye uğratarak ana iş parçacığına yol verebilirsiniz.

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 verme 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 beş yuvalanmış setTimeout() grubundan sonra tarayıcı her ek setTimeout() için en az 5 milisaniyelik bir gecikme uygulamaya başlar.

setTimeout'ün, işleri devretme konusunda başka bir dezavantajı da vardır: setTimeout'ü kullanarak kodu sonraki bir görevde çalıştırılmak üzere erteleyerek ana iş parçacığına devrettiğinizde, söz konusu görev kuyruğun sonuna eklenir. Beklemede olan 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: not supported.
  • Safari: not supported.

Source

scheduler.yield(), tarayıcıdaki ana iş parçacığına yol vermek 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üldükten sonra çalıştırılmak üzere zincirlenen tüm kodlar (açık bir .then() zincirinde veya bir asynkron 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 verir. İşlevin geri kalanının yürütülmesi (işlevin devamı olarak adlandırılır) yeni bir etkinlik döngüsü görevinde çalışacak şekilde planlanır. Bu görev başladığında, beklenen söz çözülür ve işlev kaldığı yerden çalışmaya 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 profilleyicisinde gösterilen saveSettings işlevi artık iki göreve ayrıldı. İlk görev iki işlevi çağırır, ardından yield verir. Böylece düzen ve boyama çalışması 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ı olan 64 milisaniyede tamamlanır. İkinci görev, son üç işlevi çağırır.
saveSettings() işlevinin yürütme işlemi artık iki göreve ayrılmıştır. Sonuç olarak, düzen ve boyama, görevler arasında çalışabilir. Bu da kullanıcıya daha hızlı bir görsel yanıt verir. Bu yanıt, artık çok daha kısa olan işaretçi etkileşimiyle ölçülür.

Bununla birlikte, scheduler.yield()'ün diğer yield yaklaşımlarına kıyasla asıl avantajı, devamına öncelik verilmesidir. Yani bir görevin ortasında yield ederseniz mevcut görevin devamı, diğer benzer görevler başlatılmadan önce çalışır.

Bu sayede, üçüncü taraf komut dosyalarındaki görevler gibi diğer görev kaynaklarından gelen kod, kodunuzun yürütülme sırasını kesintiye uğratmaz.

Verimi olmayan, verimi olan ve verimi olan ve devam eden görevleri gösteren üç diyagram. Verimli olmayan uzun görevler vardır. Verirken, daha kısa ancak alakasız diğer görevler tarafından kesintiye uğrayabilecek daha fazla görev vardır. Verme ve devam etme ile daha kısa görevler olsa da bunların yürütme sırası korunur.
scheduler.yield()'ı kullandığınızda devam etme işlemi, diğer görevlere geçmeden önce kaldığınız yerden devam eder.

Tarayıcılar arası destek

scheduler.yield() henüz tüm tarayıcılarda desteklenmemektedir. Bu nedenle yedek bir yönteme ihtiyaç vardır.

Çözümlerden biri, scheduler-polyfill dosyasını derlemenize eklemektir. Ardından scheduler.yield() doğrudan kullanılabilir. Diğer görev planlama işlevlerine geri dönme işlemini polyfill işlevi gerçekleştirir. Böylece, polyfill işlevi tüm tarayıcılarda benzer şekilde çalışır.

Alternatif olarak, scheduler.yield() kullanılamıyorsa yedek olarak yalnızca Promise içine sarmalanmış setTimeout kullanılarak birkaç satırda daha basit 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 devamı almamasına rağmen tarayıcının duyarlı kalması için yine de verim sağlar.

Son olarak, devamına öncelik verilmeyen durumlarda kodunuzun ana iş parçacığına vermeyi göze alamadığı durumlar olabilir (örneğin, yoğun olduğu bilinen bir sayfada, vermenin bir süre boyunca çalışmanın tamamlanma riskini artırması). Bu durumda, scheduler.yield() bir tür aşamalı iyileştirme olarak değerlendirilebilir: scheduler.yield()'ün kullanılabildiği tarayıcılarda verim, aksi takdirde devam edin.

Bu işlem, hem özellik algılayarak hem de tek satırda tek bir mikro görevi beklemeye geri dönerek 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ölme

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

Örneğin, çalıştırmanız gereken ve genellikle uzun bir göreve dönüşen bir dizi işiniz varsa görevi bölmek için verim 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. Böylece, uzun olabilecek iş listesinin tamamlanmasını beklemeniz gerekmez.

Ancak bu, verimi verimli bir şekilde kullanma şekli değildir. scheduler.yield() hızlı ve etkilidir ancak bazı ek maliyetleri vardır. jobQueue içindeki işlerin bazıları çok kısaysa ek maliyetler, gerçek işi yürütmekten daha fazla zaman harcamanıza neden olabilir.

Bir yaklaşım, işleri gruplandırmak ve yalnızca son verimden bu yana yeterince zaman geçmişse aralarında verim elde etmektir. Görevlerin uzun görevler haline gelmesini önlemek için genellikle 50 milisaniyelik bir son tarih belirlenir ancak bu süre, yanıt verme süresi ile iş kuyruğunu tamamlama süresi arasında bir denge oluşturacak şekilde 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şlerin çalışmasının çok uzun sürmemesi için parçalara ayrılır ancak çalıştırıcı yalnızca yaklaşık 50 milisaniyede bir ana iş parçacığına verir.

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

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 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 sağlama konusundaki 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, birkaç farklı teknik vardır. Görevleri yönetirken dikkate almanız gereken temel noktalar şunlardır:

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

scheduler.yield(), açık görev planlamayla ilgili akrabası scheduler.postTask() ve görev önceliklendirmesi hakkında daha fazla bilgi edinmek için Öncelikli Görev Planlama API dokümanlarını inceleyin.

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.