Uzun görevleri optimize edin

JavaScript uygulamalarınızı daha hızlı hale getirmek için genellikle "Ana iş parçacığını engellemeyin" ve "Uzun görevlerinizi bölün" tavsiyeleri yer alır. Bu sayfada, bu tavsiyenin ne anlama geldiği ve JavaScript'te görevleri optimize etmenin neden önemli olduğu açıklanmaktadır.

Görev nedir?

Görev, tarayıcının yaptığı ayrı bir iştir. Bunlar arasında oluşturma, HTML ve CSS'yi ayrıştırma, yazdığınız JavaScript kodunu çalıştırma ve doğrudan kontrol sahibi olmadığınız diğer işlemler yer alır. Sayfalarınızın JavaScript'i, tarayıcı görevlerinin başlıca kaynaklarından biridir.

Chrome DevTools'un performans görüntüleyicisindeki bir görevin ekran görüntüsü. Görev, yığının en üst kısmındadır ve altında tıklama etkinlik işleyici, işlev çağrısı ve başka öğeler bulunur. Görev ayrıca sağ tarafta bazı oluşturma çalışmaları da içeriyor.
click etkinlik işleyici tarafından başlatılan ve Chrome Geliştirici Araçları'nın performans profil aracında gösterilen görev.

Görevler, performansı çeşitli şekillerde etkiler. Örneğin, bir tarayıcı başlatma sırasında JavaScript dosyası indirdiğinde, yürütülebilmesi için JavaScript'i ayrıştırıp derlemek üzere görevleri sıraya alır. Sayfa yaşam döngüsünün ilerleyen aşamalarında, etkinlik işleyiciler üzerinden etkileşimleri, JavaScript odaklı animasyonları ve analiz toplama gibi arka plan etkinliklerini yönlendirme gibi diğer görevler, JavaScript'iniz çalıştığında başlar. Web çalışanları ve benzer API'ler dışında tüm bunlar 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 neredeyse tüm JavaScript'lerin yürütüldüğü yerdir.

Ana iş parçacığı bir kerede yalnızca bir görev işleyebilir. 50 milisaniyeden uzun süren görevler uzun bir görev olarak kabul edilir. Kullanıcı, uzun bir görev veya oluşturma güncellemesi sırasında sayfayla etkileşimde bulunmaya çalışırsa tarayıcının bu etkileşimi gerçekleştirmek için beklemesi gerekir. Bu da gecikmeye neden olur.

Chrome Geliştirici Araçları'nın performans profil aracındaki uzun bir görev. Görevin engelleme bölümü (50 milisaniyeden uzun) kırmızı çapraz şeritlerle işaretlenmiştir.
Chrome'un performans profil aracında gösterilen uzun bir görev. Uzun görevler, görevin köşesindeki kırmızı bir üçgenle gösterilir. Bu üçgende, görevin engelleyici kısmı çapraz kırmızı şeritlerle doldurulur.

Bunu önlemek için her uzun görevi her biri daha az zaman alan daha küçük görevlere bölün. Buna uzun görevleri bölme denir.

Tek bir uzun görev ya da aynı görev daha kısa görevlere bölündü. Uzun görev, büyük bir dikdörtgenden oluşur. Parçalı görev ise uzunlukları uzun görevin uzunluğuna denk gelen beş küçük kutudur.
Beş kısa göreve bölünmüş tek bir uzun görev ile aynı görevi karşılaştırmalı olarak görselleştirme.

Görevleri ayırmak, tarayıcının diğer görevler arasında kullanıcı etkileşimleri de dahil olmak üzere daha yüksek öncelikli çalışmalara yanıt vermesi için daha fazla fırsat sunar. Bu, etkileşimlerin çok daha hızlı gerçekleşmesini sağlar. Aksi takdirde, tarayıcı uzun bir görevin tamamlanmasını beklerken kullanıcı gecikme yaşayabilir.

Bir görevi bölmek kullanıcı etkileşimini kolaylaştırabilir. En üstte, uzun bir görev, etkinlik işleyicinin görev tamamlanana kadar çalışmasını engeller. Alt taraftaki parçalanmış görev, etkinlik işleyicinin normalde olduğundan daha erken çalışmasını sağlar.
Görevler çok uzun olduğunda tarayıcı, etkileşimlere yeterince hızlı yanıt veremez. Görevleri bölmek bu etkileşimlerin daha hızlı gerçekleşmesini sağlar.

Görev yönetimi stratejileri

JavaScript, görev yürütmenin çalıştırmaya çalıştır modelini kullandığından her işlevi tek bir görev olarak değerlendirir. Diğer bir deyişle, aşağıdaki örnek gibi başka birden çok işlev çağıran bir işlevin, çağrılan tüm işlevler tamamlanana kadar çalışması gerekir. Bu da tarayıcıyı yavaşlatır:

function saveSettings () { //This is a long task.
  validateForm();
  showSpinner();
  saveToDatabase();
  updateUI();
  sendAnalytics();
}
Chrome'un performans profil aracında gösterilen saveSettings işlevi. Üst düzey işlev beş başka işlev daha çağırırken tüm işler ana iş parçacığını engelleyen uzun tek bir görevde gerçekleştirilir.
Beş işlev çağıran tek işlev saveSettings(). Çalışma, uzun tek bir görevin parçası olarak yürütülür.

Kodunuz birden fazla yöntem çağıran işlevler içeriyorsa kodu birden fazla işleve bölün. Bu hem tarayıcıya etkileşime yanıt vermesi için daha fazla fırsat sağlar hem de kodunuzun okunmasını, korunmasını ve test edilmesini kolaylaştırır. Aşağıdaki bölümlerde, uzun işlevleri ayırmak ve bunları oluşturan görevlerin önceliğini belirlemek için yararlanabileceğiniz bazı stratejiler adım adım açıklanmıştır.

Kod yürütmeyi manuel olarak ertele

İlgili işlevi setTimeout() işlevine ileterek bazı görevlerin yürütülmesini erteleyebilirsiniz. Bu, 0 zaman aşımı belirtseniz bile işe yarar.

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 en çok, sırayla çalışması gereken bir dizi işlev için işe yarar. Farklı şekilde düzenlenen kod için farklı bir yaklaşım gerekir. Bir sonraki örnek, döngü kullanarak büyük miktarda veriyi işleyen bir işlevdir. Veri kümesi ne kadar büyük olursa bu işlem o kadar uzun sürer ve döngüde setTimeout() yerleştirmek için iyi bir yer olmayabilir:

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

Neyse ki kod yürütmeyi sonraki bir göreve ertelemenizi sağlayan birkaç API daha vardır. Daha hızlı zaman aşımları için postMessage() kullanmanızı öneririz.

İşleri requestIdleCallback() kullanarak da bölebilirsiniz ancak görevleri en düşük öncelikli olarak ve yalnızca tarayıcının boşta kaldığı süre boyunca planlar. Diğer bir deyişle, ana iş parçacığı özellikle yoğunsa requestIdleCallback() ile planlanan görevler hiçbir zaman çalıştırılamayabilir.

Getiri puanı oluşturmak için async/await kullanın

Kullanıcılara yönelik önemli görevlerin daha düşük öncelikli görevlerden önce gerçekleştirildiğinden emin olmak için tarayıcıya daha önemli görevleri çalıştırma fırsatı vermek amacıyla görev sırasını kısa bir süre keserek ana iş parçacığı elde edin.

Bunu yapmanın en basit yolu, setTimeout() çağrısıyla çözümlenen bir Promise kullanmaktır:

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

saveSettings() işlevinde, her işlev çağrısından sonra yieldToMain() işlevini await yaparsanız her adımdan sonra ana iş parçacığı elde edebilirsiniz. Böylece uzun göreviniz birden çok göreve etkili şekilde bölünür:

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();
  }
}

Önemli nokta: Her işlev çağrısından sonra veri sağlamanız gerekmez. Örneğin, kullanıcı arayüzünde kritik güncellemelerle sonuçlanan iki işlev çalıştırıyorsanız muhtemelen ikisi arasında geçiş yapmak istemezsiniz. Mümkünse önce bu çalışmayı bırakın, ardından kullanıcının görmediği, arka planda veya daha az kritik düzeyde iş yapan işlevler arasında geçiş yapmayı düşünün.

Chrome'un performans profil aracındaki aynı saveSettings işlevi artık getiri sağlar.
    Görev şimdi her fonksiyon için bir tane olmak üzere beş ayrı göreve bölündü.
saveSettings() işlevi artık alt işlevlerini ayrı görevler olarak yürütüyor.

Özel planlayıcı API'sı

Şimdiye kadar bahsedilen API'ler görevleri bölmenize yardımcı olabilir ancak bu API'lerin ciddi bir dezavantajı vardır. Kodu daha sonraki bir görevde çalışacak şekilde erteleyerek ana iş parçacığına giderseniz bu kod görev sırasının sonuna eklenir.

Sayfanızdaki tüm kodları kontrol ediyorsanız görevlere öncelik vermek için kendi planlayıcınızı oluşturabilirsiniz. Ancak üçüncü taraf komut dosyaları planlayıcınızı kullanmaz. Bu nedenle, bu durumda çalışmaya öncelik veremezsiniz. Bunu yalnızca bölebilir veya kullanıcı etkileşimlerine sağlayabilirsiniz.

Tarayıcı Desteği

  • 94
  • 94
  • x

Kaynak

Planlayıcı API'si, görevlerin daha ayrıntılı bir şekilde planlanmasına olanak tanıyan postTask() işlevini sunar. Bu işlev, düşük öncelikli görevlerin ana iş parçacığına dönüşmesi için tarayıcının çalışmalara öncelik vermesine yardımcı olabilir. postTask(), vaatleri kullanır ve bir priority ayarını kabul eder.

postTask() API'nin üç önceliği vardır:

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

Aşağıdaki örnek kod, üç görevi mümkün olan en yüksek öncelikte, geri kalan iki görevi ise mümkün olan en düşük öncelikte çalıştırmak için postTask() API'yi kullanır:

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 planlanır. Böylece, kullanıcı etkileşimleri gibi tarayıcı öncelikli görevler işe yarayabilir.

Chrome'un performans profil oluşturucusunda gösterilen saveSettings işlevi, postTask kullanırken saveSettings'in çalıştırıldığı her bir işlevi böler ve kullanıcı etkileşimi engellenmeden yürütülebilecek şekilde önceliklendirir.
saveSettings() çalıştığında, işlev postTask() kullanarak bağımsız işlev çağrılarını planlar. Kullanıcıya yönelik kritik işler yüksek öncelikte planlanırken, kullanıcının bilmediği işler arka planda çalışacak şekilde planlanır. İş hem parçalara ayrıldığı hem de uygun şekilde önceliklendirildiği için bu, kullanıcı etkileşimlerinin daha hızlı yürütülmesine olanak tanır.

Gerektiğinde farklı TaskController örneklerinin önceliklerini değiştirebilme dahil, görevler arasında öncelikleri paylaşan farklı TaskController nesneleri de örneklendirmeniz mümkündür.

Yakında kullanıma sunulacak scheduler.yield() API ile devamlı yerleşik getiri

Önemli nokta: scheduler.yield() ile ilgili daha ayrıntılı bir açıklama için kaynak denemesi (sonucundan beri) ve açıklayıcı hakkında bilgi edinin.

Planlayıcı API'sine önerilen bir ekleme işlemi de tarayıcıdaki ana iş parçacığını oluşturmak için özel olarak tasarlanmış bir API'dir scheduler.yield(). Kullanımı, bu sayfada daha önce 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 ölçüde tanıdık gelir, ancak yieldToMain() yerine await scheduler.yield() kodunu kullanır.

Görevleri getirilmeden, sonuç veren ve getiri ile devam ettiren üç şema. Getirisi olmayan uzun görevler söz konusudur. Getiri sayesinde, daha kısa olan daha fazla görev vardır ancak bu görevler alakasız diğer görevler tarafından kesintiye uğratılabilir. Getiri ve devam ettirme sayesinde daha kısa görevlerin 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() özelliğinin avantajı devamlı olmasıdır. Diğer bir deyişle, bir görev grubunun ortasında getiri elde ederseniz planlanan diğer görevler, getiri noktasından sonra aynı sırada devam eder. Bu, üçüncü taraf komut dosyalarının, kodunuzun çalıştığı sırayı kontrol etmesini önler.

priority: 'user-blocking' ile scheduler.postTask() kullanıldığında, yüksek user-blocking önceliği nedeniyle devam etme olasılığı da yüksektir. Bu nedenle, scheduler.yield() daha yaygın bir şekilde kullanıma sunulana kadar bunu bir alternatif olarak kullanabilirsiniz.

setTimeout() (veya priority: 'user-visible' ile scheduler.postTask() kullanıldığında ya da açık bir priority olmadan) sıranın arkasındaki görevi planlar ve beklemedeki diğer görevlerin devam etmeden önce çalıştırılmasını sağlar.

isInputPending() ile girişte verim verin

Tarayıcı Desteği

  • 87
  • 87
  • x
  • x

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, bekleyen giriş yoksa JavaScript'in devam edip görev sırasının sonunda ilerlemek yerine devam etmesini sağlar. Bu, aksi halde ana iş parçacığına geri dönmeyebilecek siteler için Gönderim Amacı bölümünde ayrıntılı olarak açıklandığı gibi etkileyici performans iyileştirmeleriyle sonuçlanabilir.

Ancak bu API'nin kullanıma sunulmasından bu yana, özellikle INP'nin kullanıma sunulmasından sonra verim konusundaki anlayışımız arttı. Artık bu API'yi kullanmanızı önermeyiz ve bunun yerine, girinin beklemede olup olmamasından bağımsız olarak işlem yapılmasını öneririz. Önerilerdeki bu değişikliğin çeşitli nedenleri vardır:

  • API, kullanıcının etkileşimde bulunduğu bazı durumlarda yanlış bir şekilde false döndürebilir.
  • Görevlerin verilmesi 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.
  • Bu endişeleri gidermek için scheduler.postTask() ve scheduler.yield() gibi daha kapsamlı ve getiri sağlayan API'ler kullanıma sunulmuştur.

Sonuç

Görevleri yönetmek zordur, ancak bunu yapmak sayfanızın kullanıcı etkileşimlerine daha hızlı yanıt vermesine yardımcı olur. Kullanım alanınıza bağlı olarak görevleri yönetmek ve önceliklendirmek için çeşitli teknikler vardır. Tekrarlamak gerekirse, görevleri yönetirken göz önünde bulundurmanız gereken temel unsurlar şunlardır:

  • Kritik, kullanıcı odaklı görevler için ana iş parçacığına dönün.
  • scheduler.yield() ile denemeler yapabilirsiniz.
  • postTask() ile görevlere öncelik verin.
  • Son olarak, fonksiyonlarınızda mümkün olduğunca az işlem yapın.

Bu araçlardan biri veya birkaçını kullandığınızda, uygulamanızdaki işi kullanıcının ihtiyaçlarına öncelik verecek şekilde yapılandırabilmeniz ve daha az kritik işlerin yapılmasını sağlarsınız. Bu, uygulamayı daha duyarlı ve kullanımı daha keyifli hale getirerek kullanıcı deneyimini iyileştirir.

Bu dokümanı teknik olarak incelediği için Philip Walton'a özel teşekkür ederiz.

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