Uzun görevleri optimize edin

Size "ana iş parçacığını engelleme" ve "uzun görevlerinizi bölme" söyleniyor, ama bunları yapmak ne anlama geliyor?

JavaScript uygulamalarını hızlı tutmaya yönelik genel tavsiyeler, genellikle aşağıdaki tavsiyeleri içerir:

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

Bu çok güzel bir tavsiye ama ne gibi bir çalışma yapılır? Daha az JavaScript göndermek iyi bir yöntemdir. Peki bu otomatik olarak daha duyarlı kullanıcı arayüzlerine denk midir? Olabilir ama 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ığı her ayrı ayrı iştir. Bu çalışmalar arasında HTML ve CSS'nin oluşturulması, ayrıştırılması, JavaScript'in çalıştırılması ve doğrudan kontrol edemediğiniz diğer çalışma türleri bulunur. Bunların arasında, yazdığınız JavaScript belki de en büyük görev kaynağıdır.

Chrome'un Geliştirici Araçları'nın performans uzmanlarında gösterildiği şekilde bir görevin vizeasyonu. Görev, bir yığının en üstündedir. Görevin altında bir tıklama etkinlik işleyici, bir işlev çağrısı ve başka öğeler bulunur. Görev, sağ tarafta bazı oluşturma çalışmaları da içerir.
Bir click etkinlik işleyici tarafından başlatılan ve Chrome Geliştirici Araçları'nın performans profil aracında gösterilen bir görev.

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

  • Tarayıcı, başlatma sırasında bir JavaScript dosyası indirdiğinde, daha sonra yürütülebilmesi için JavaScript'i ayrıştırıp derlemek üzere görevleri sıraya alır.
  • Sayfanın ömrü içindeki bazı zamanlarda ise; etkinlik işleyiciler, JavaScript'e dayalı animasyonlar ve analiz toplama gibi arka plan etkinlikleri üzerinden etkileşimlerin yönlendirilmesi gibi, JavaScript çalışırken görevler sıraya alınır.

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

Ana ileti dizisi nedir?

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

Ana iş parçacığı tek seferde yalnızca bir görev işleyebilir. 50 milisaniyeden uzun süren görevler uzun bir görevdir. 50 milisaniyeyi aşan görevlerde, görevin toplam süresi eksi 50 milisaniye ise görevin engelleme süresi olarak bilinir.

Tarayıcı, herhangi bir uzunluktaki görev devam ederken etkileşimlerin gerçekleşmesini engeller. Ancak görevler çok uzun süre devam ettiği sürece kullanıcı bunu algılayamaz. Bununla birlikte, bir kullanıcı çok sayıda uzun görev varken bir sayfayla etkileşimde bulunmaya çalıştığında ana iş parçacığı çok uzun süre engellenmiş olsa bile kullanıcı arayüzü yanıt vermiyor, hatta bozulmuş gibi görünebilir.

Chrome'un Geliştirici Araçları'nın performans profili aracında uzun bir görev var. Görevin bloke eden kısmı (50 milisaniyeden uzun) kırmızı çapraz çizgili bir desenle gösterilmiştir.
Chrome'un performans profil aracında gösterilen uzun bir görev. Uzun görevler, görevin köşesinde kırmızı bir üçgenle gösterilir ve görevi bloke eden kısım ç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 bir görevi birkaç küçük göreve bölebilirsiniz.

Tek bir uzun görev yerine daha kısa bir göreve bölünmüş aynı görev. Uzun görev 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 kutudur.
Tek bir uzun görevin ve aynı görevin beş kısa göreve bölünmesinin görselleştirilmiş hali.

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. Sonrasında kalan görevler sonuna kadar yürütülür ve ilk sıraya aldığınız işin tamamlandığından emin olunur.

Görevleri ayırmanın kullanıcı etkileşimini nasıl kolaylaştırabileceğinin tasviri. Üst kısımda, uzun bir görev, görev tamamlanana kadar bir etkinlik işleyicinin çalışmasını engelliyor. En altta, parçalanmış görev, etkinlik işleyicinin normalde olacağından daha erken çalışmasına olanak tanır.
Görevler çok uzun olduğunda ve tarayıcının etkileşimlere yeterince hızlı yanıt veremediğinde, uzun görevlerin küçük görevlere bölündüğünde etkileşimlere ne olduğunun görselleştirmesi.

Önceki resmin üst kısmında, kullanıcı etkileşimi tarafından sıraya alınan bir etkinlik işleyicinin, başlamadan önce uzun bir görevi beklemesi gerekiyordu. Bu da etkileşimin gerçekleşmesini geciktirmektedir. Bu senaryoda, kullanıcı gecikmeyi fark etmiş olabilir. Alt tarafta, etkinlik işleyici daha erken çalışmaya başlayabilir ve etkileşim anında hissetmiş olabilir.

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

Görev yönetimi stratejileri

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

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

Bu örnekte; formu doğrulamak, döner simge göstermek, uygulama arka ucuna veri göndermek, kullanıcı arayüzünü güncellemek ve analizler göndermek için beş işlev çağıran saveSettings() adlı bir işlev bulunmaktadır.

saveSettings(), kavramsal olarak iyi tasarlanmış. Bu işlevlerden birinde hata ayıklamanız gerekirse her bir işlevin ne işe yaradığını öğrenmek için proje ağacını tarayabilirsiniz. İşleri bu şekilde ayırmak, projelerde gezinmeyi ve bakımını kolaylaştırır.

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

Chrome'un performans profil aracında gösterildiği gibi saveSettings işlevi. Üst düzey işlev diğer beş işlev çağırırken tüm çalışmalar ana iş parçacığını engelleyen uzun tek bir görevde gerçekleştirilir.
Beş işlev çağıran tek bir işlev saveSettings(). Çalışma, uzun tek bir monolitik görevin parçası olarak yürütülür.

En iyi senaryoda, bu işlevlerden yalnızca biri bile görevin toplam uzunluğuna 50 milisaniye veya daha fazla katkı sağlayabilir. En kötü durumda, bu görevlerin çoğu, özellikle de kaynak açısından kısıtlı cihazlarda çok daha uzun süre çalışabilir.

Kod yürütülmesini manuel olarak ertele

setTimeout(), geliştiricilerin görevleri daha küçük görevlere ayırmak için kullandıkları yöntemlerden biridir. Bu teknikle, işlevi setTimeout() ürününe iletirsiniz. Bu işlem, 0 zaman aşımı belirtseniz 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, getiri olarak bilinir ve en iyi şekilde, sıralı olarak çalışması gereken bir dizi işlev için işe yarar.

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() kullanılması, geliştiricinin ergonomisi nedeniyle sorunludur ve her iterasyon hızlı bir şekilde gerçekleşse bile tüm veri dizisinin işlenmesi çok uzun sürebilir. Her şeyin bir arada olması gerekir. setTimeout() ise bu iş için doğru araç değil. En azından bu şekilde kullanıldığında hayır.

Getiri puanları oluşturmak için async/await değerini kullanın

Kullanıcılara yönelik önemli görevlerin daha düşük öncelikli görevlerden önce gerçekleştirildiğinden emin olmak amacıyla, tarayıcıya daha önemli görevleri çalıştırması için fırsat tanımak amacıyla görev sırasını kısa süreliğine keserek ana iş parçacığını yaratabilirsiniz.

Daha önce açıklandığı gibi, ana ileti dizisine veri vermek için setTimeout kullanılabilir. Bununla birlikte, kolaylık ve daha iyi okunabilirlik için Promise içinde setTimeout öğesini çağırabilir ve geri çağırma olarak resolve yöntemini iletebilirsiniz.

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

yieldToMain() işlevinin avantajı, herhangi bir async işlevinde await işlevi gerçekleştirebilmenizdir. Önceki örneği temel alarak, çalıştırılacak bir işlev dizisi oluşturabilir ve her çalıştırıldıktan sonra ana iş parçacığına getiri yapabilirsiniz:

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çta, eskiden monolitik bir görev artık farklı görevlere bölünmüştür.

Chrome'un performans profil oluşturucusunda gösterilen saveSettings işlevi yalnızca getiri ile. Sonuçta eskiden monolitik bir görev artık her fonksiyon için bir tane olacak şekilde 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ölmek için etkili bir yoldur ancak dezavantajı olabilir: Kodu sonraki bir görevde çalışacak şekilde erteleyerek ana iş parçacığına veri koyduğunuzda bu görev, sıranın sonuna eklenir.

Sayfanızdaki tüm kodu kontrol ediyorsanız görevleri öncelik sırasına koyma özelliğiyle kendi planlayıcınızı oluşturabilirsiniz ancak üçüncü taraf komut dosyaları planlayıcınızı kullanmaz. Aslında bu tür ortamlarda çalışmaya öncelik veremezsiniz. Verileri yalnızca parçalara ayırabilir veya açık bir şekilde kullanıcı etkileşimlerini sağlayabilirsiniz.

Tarayıcı Desteği

  • 94
  • 94
  • x

Kaynak

Scheduler API, görevlerin daha ayrıntılı bir şekilde planlanmasına olanak tanıyan postTask() işlevini sunar. Ayrıca, düşük öncelikli görevlerin ana iş parçacığına üretilmesi için tarayıcının işi önceliklendirmesine yardımcı olmanın bir yoludur. postTask(), taahhütleri kullanır ve üç priority ayarından birini kabul eder:

  • En düşük öncelikli görevler için 'background'.
  • Orta öncelikli görevler için 'user-visible'. priority ayarlanmazsa varsayılan olarak bu ayar kullanılır.
  • Yüksek öncelikte çalışması gereken kritik görevler için 'user-blocking'.

postTask() API'nin üç 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 kullanıldığı aşağıdaki kodu örnek olarak ele alalım.

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, kullanıcı etkileşimleri gibi tarayıcıya öncelik verilen görevlerin arayla verimli şekilde yerine getirilebileceği şekilde planlanır.

SaveSettings işlevi, Chrome'un performans profil aracı bölümünde gösterildiği gibi, postTask'i kullanarak yapılır. postTask, saveSettings tarafından yürütülen her işlevi ayırır ve kullanıcı etkileşiminin engellenmeden çalışma fırsatı bulacağı şekilde bunlara öncelik verir.
saveSettings() çalıştırıldığında işlev, bağımsız işlevleri postTask() kullanarak planlar. Kullanıcının bilmediği çalışma, arka planda çalışacak şekilde planlanırken, kullanıcıya yönelik kritik çalışma yüksek öncelikte planlanır. İşin bölünmesi ve uygun şekilde önceliklendirilmesi nedeniyle bu yaklaşım, kullanıcı etkileşimlerinin daha hızlı yürütülmesine olanak tanır.

Bu, postTask() özelliğinin nasıl kullanılabildiğine dair basit bir örnektir. Gerektiğinde farklı TaskController örnekleri için öncelikleri değiştirme olanağı da dahil olmak üzere, görevler arasında öncelikleri paylaşabilen farklı TaskController nesneleri örneklenebilir.

Yakında kullanıma sunulacak scheduler.yield() API ile sürekliliği olan yerleşik getiri

Scheduler API'sına önerilen bir ekleme de scheduler.yield(), tarayıcıdaki ana iş parçacığına verim sağlamak üzere özel olarak tasarlanmış bir API'dir. Bu işlevin kullanımı, bu kılavuzun ö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 çoğunlukla tanıdık gelecektir, ancak yieldToMain() yerine await scheduler.yield() kullanıyor.

Görevleri verimsiz, verimsiz, getiri ve devamlı olmadan gösteren üç diyagram. Cevap verilmeden ise uzun görevler yapılır. Getiri ile daha kısa olan ancak alakasız diğer görevler nedeniyle yarıda kesilebilecek daha fazla görev ortaya çıkar. Getiri ve devamla, daha kısa olan daha çok görev ortaya çıkar ama bunların yürütme sırası korunur.
scheduler.yield() kullandığınızda görev yürütme, getiri noktasından sonra bile kaldığı yerden devam eder.

scheduler.yield() ürününün faydası devamlılıktır. Diğer bir deyişle, bir görev kümesinin ortasında verim alırsanız diğer planlanmış görevler getiri noktasından sonra da aynı sırayla devam eder. Bu sayede, üçüncü taraf komut dosyalarından gelen kodların, kodunuzun yürütülme sırasını kesintiye uğratması önlenir.

user-blocking önceliğinin yüksek olması nedeniyle scheduler.postTask() ürününün priority: 'user-blocking' ile birlikte kullanılması da yüksek ihtimalle devam edeceği için bu süre zarfında alternatif olarak bu yaklaşım kullanılabilir.

setTimeout() (veya scheduler.postTask() priority: 'user-visibile' ile veya açıkça belirtilmezse priority) kullanıldığında görev, sıranın arkasına planlanır ve beklemedeki diğer görevler devam etmeden önce çalıştırılabilir.

isInputPending() adını kullanma

Tarayıcı Desteği

  • 87
  • 87
  • x
  • x

isInputPending() API, kullanıcının bir sayfayla etkileşim kurmayı deneyip denemediğini ve yalnızca giriş beklemedeyse getiriyi kontrol etmenin bir yolunu sağlar.

Bu sayede, bekleyen giriş yoksa JavaScript'in devam etmesi ve sonuç olarak görev sırasının sonuna gelmesi yerine işleme devam etmesi sağlanır. Bu, Gönderme Amacı'nda ayrıntılı olarak açıklandığı gibi, aksi halde ana iş parçacığına dönüşmeyebilecek 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 getiri anlayışımız arttı. Artık bu API'yi kullanmanızı önermeyiz. Bunun yerine, çeşitli nedenlerden dolayı girişin beklemede olup olmamasından bağımsız olarak sonuç elde etmenizi öneririz:

  • isInputPending(), kullanıcı bazı durumlarda etkileşimde bulunmasına rağmen yanlışlıkla false sonucu döndürebilir.
  • Görevlerin gerçekleşmesi gereken tek durum girdi değildir. Animasyonlar ve diğer düzenli kullanıcı arayüzü güncellemeleri, duyarlı bir web sayfası sağlamak kadar aynı derecede önemli olabilir.
  • O zamandan beri, scheduler.postTask() ve scheduler.yield() gibi önemli endişeleri gidermek için daha kapsamlı getiri API'leri kullanıma sunuldu.

Sonuç

Görevleri yönetmek zor bir iştir, ancak böyle 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 öneri yok, birkaç farklı teknik var. Tekrar hatırlatmak 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 ileti dizisine odaklanın.
  • postTask() ile görevlere öncelik verin.
  • scheduler.yield() ile denemeler yapabilirsiniz.
  • Son olarak, fonksiyonlarınızda mümkün olduğunca az çalışın.

Bu araçlardan bir veya daha fazlasını kullanarak uygulamanızdaki işi, kullanıcının ihtiyaçlarına öncelik verecek ve kritik öneme sahip olmayan işlerin yapılmasını sağlayacak şekilde yapılandırabilmeniz gerekir. Böylece daha duyarlı ve kullanımı daha keyifli olan daha iyi bir kullanıcı deneyimi oluşturabilirsiniz.

Bu rehberi teknik olarak incelediği için Philip Walton'a özel teşekkürlerimizi sunuyoruz.

Küçük resim, Amirali Mirhashemian'ın izniyle Unsplash'ten alınmıştır.