Modül çalışanlarıyla web'de mesaj dizisi oluşturma

Web çalışanlarındaki JavaScript modülleri sayesinde, yoğun işlemler artık arka plan iş parçalarına taşınabilir.

JavaScript tek iş parçacıklı olduğundan aynı anda yalnızca bir işlem gerçekleştirebilir. Bu yöntem sezgiseldir ve web'de birçok durumda işe yarar. Ancak veri işleme, ayrıştırma, hesaplama veya analiz gibi ağır işler yapmamız gerektiğinde sorunlu olabilir. Web'de giderek daha karmaşık uygulamalar kullanıma sunulduğundan çok iş parçacıklı işleme ihtiyacı artmaktadır.

Web platformunda, mesaj dizileri ve paralellik için ana ilkel Web Workers API'dir. İşçiler, işletim sistemi iş parçacıklarının üzerinde, iş parçacığı içi iletişim için mesaj aktarma API'si sunan hafif bir soyutlamadır. Bu, maliyetli hesaplamalar yaparken veya büyük veri kümeleriyle çalışırken son derece yararlı olabilir. Böylece, maliyetli işlemler bir veya daha fazla arka plan iş parçacığında gerçekleştirilirken ana iş parçacığı sorunsuz şekilde çalışabilir.

Bir çalışan komut dosyasının ana mesaj dizisinden gelen mesajları dinleyip kendi mesajlarını göndererek yanıt verdiği, çalışan kullanımıyla ilgili tipik bir örnek aşağıda verilmiştir:

page.js:

const worker = new Worker('worker.js');
worker.addEventListener('message', e => {
  console.log(e.data);
});
worker.postMessage('hello');

worker.js:

addEventListener('message', e => {
  if (e.data === 'hello') {
    postMessage('world');
  }
});

Web Worker API, on yıldan uzun bir süredir çoğu tarayıcıda kullanılabilmektedir. Bu, çalışanların mükemmel tarayıcı desteğine sahip olduğu ve iyi optimize edildiği anlamına gelir. Ancak bu, JavaScript modüllerinden çok daha eski oldukları anlamına da gelir. Çalışanlar tasarlanırken modül sistemi olmadığından, çalışana kod yükleme ve komut dosyası oluşturma API'si 2009'da yaygın olan senkronize komut dosyası yükleme yaklaşımlarına benzer şekilde kalmıştır.

Geçmiş: klasik çalışanlar

Çalışan kurucusu, belge URL'sine göre bir klasik komut dosyası URL'si alır. Hemen yeni işleyici örneğine ait bir referans döndürür. Bu referans, bir mesajlaşma arayüzünün yanı sıra işleyiciyi hemen durdurup yok eden bir terminate() yöntemi gösterir.

const worker = new Worker('worker.js');

Web işçilerinde ek kod yüklemek için bir importScripts() işlevi kullanılabilir ancak her komut dosyasını almak ve değerlendirmek için işçinin yürütülmesi duraklatılır. Ayrıca, klasik bir <script> etiketi gibi global kapsamda komut dosyalarını yürütür. Yani bir komut dosyasındaki değişkenler başka bir komut dosyasındaki değişkenler tarafından yazılabilir.

worker.js:

importScripts('greet.js');
// ^ could block for seconds
addEventListener('message', e => {
  postMessage(sayHello());
});

greet.js:

// global to the whole worker
function sayHello() {
  return 'world';
}

Bu nedenle, web işçileri geçmişte bir uygulamanın mimarisi üzerinde büyük bir etki yaratmıştır. Geliştiriciler, modern geliştirme uygulamalarından vazgeçmeden web çalışanlarını kullanmayı mümkün kılmak için akıllıca araçlar ve geçici çözümler oluşturmak zorunda kalmıştır. Örneğin, webpack gibi paketleyiciler, oluşturulan koda küçük bir modül yükleyici uygulaması yerleştirir. Bu uygulama, kod yükleme için importScripts()'yi kullanır ancak değişken çakışmalarını önlemek ve bağımlılık içe aktarma ve dışa aktarma işlemlerini simüle etmek için modülleri işlevlere sarar.

Modüldeki çalışanları girme

Chrome 80'de, web işçileri için JavaScript modüllerinin ergonomi ve performans avantajlarını sunan yeni bir mod olan modül işçileri kullanıma sunuluyor. Worker kurucusu artık yeni bir {type:"module"} seçeneğini kabul ediyor. Bu seçenek, komut dosyası yükleme ve yürütme işlemini <script type="module"> ile eşleşecek şekilde değiştiriyor.

const worker = new Worker('worker.js', {
  type: 'module'
});

Modül çalışanları standart JavaScript modülleri olduğundan içe aktarma ve dışa aktarma ifadelerini kullanabilirler. Tüm JavaScript modüllerinde olduğu gibi, bağımlılıklar belirli bir bağlamda (ana iş parçacığı, çalışan vb.) yalnızca bir kez yürütülür ve gelecekteki tüm içe aktarma işlemleri, önceden yürütülmüş modül örneğine referans verir. JavaScript modüllerinin yüklenmesi ve yürütülmesi de tarayıcılar tarafından optimize edilir. Bir modülün bağımlılıkları, modül çalıştırılmadan önce yüklenebilir. Bu sayede modül ağaçlarının tamamı paralel olarak yüklenebilir. Modül yükleme, ayrıştırılmış kodu da önbelleğe alır. Bu, ana iş parçacığında ve bir çalışanda kullanılan modüllerin yalnızca bir kez ayrıştırılması gerektiği anlamına gelir.

JavaScript modüllerine geçiş yapmak, çalışanın yürütülmesini engellemeden kodun gecikmeli yüklenmesi için dinamik içe aktarma özelliğinin de kullanılmasını sağlar. İçe aktarılan modülün dışa aktarma işlemleri, genel değişkenlere bağlı kalmak yerine döndürüldüğünden dinamik içe aktarma, bağımlılıkları yüklemek için importScripts() kullanmaktan çok daha açıktır.

worker.js:

import { sayHello } from './greet.js';
addEventListener('message', e => {
  postMessage(sayHello());
});

greet.js:

import greetings from './data.js';
export function sayHello() {
  return greetings.hello;
}

Mükemmel performans sağlamak için eski importScripts() yöntemi, modül çalışanları içinde kullanılamaz. İşleyicileri JavaScript modüllerini kullanacak şekilde değiştirmek, tüm kodun katı modda yüklenmesi anlamına gelir. Dikkat çeken bir diğer değişiklik de, JavaScript modülünün üst düzey kapsamındaki this değerinin undefined olmasıdır. Klasik çalışanlarda ise bu değer, çalışanın genel kapsamıdır. Neyse ki global kapsama referans veren bir self global değişkeni her zaman mevcuttur. Hizmet çalışanları da dahil olmak üzere tüm çalışan türlerinde ve DOM'da kullanılabilir.

Çalışanları modulepreload ile önceden yükleme

Modül çalışanlarıyla birlikte gelen önemli bir performans iyileştirmesi, çalışanları ve bunların bağımlılarını önceden yükleme olanağıdır. Modül işleyicileri sayesinde komut dosyaları standart JavaScript modülleri olarak yüklenir ve çalıştırılır. Bu, komut dosyalarının modulepreload kullanılarak önceden yüklenmesi ve hatta önceden ayrıştırılması anlamına gelir:

<!-- preloads worker.js and its dependencies: -->
<link rel="modulepreload" href="worker.js">

<script>
  addEventListener('load', () => {
    // our worker code is likely already parsed and ready to execute!
    const worker = new Worker('worker.js', { type: 'module' });
  });
</script>

Önceden yüklenmiş modüller hem ana iş parçacığı hem de modül çalışanları tarafından da kullanılabilir. Bu, her iki bağlamda da içe aktarılan modüller veya bir modülün ana iş parçacığında mı yoksa bir çalışanda mı kullanılacağını önceden bilmenin mümkün olmadığı durumlarda yararlıdır.

Daha önce, web çalışanı komut dosyalarını önceden yüklemek için kullanılabilen seçenekler sınırlıydı ve her zaman güvenilir değildi. Klasik çalışanların önceden yükleme için kendi "işçi" kaynak türleri vardı ancak hiçbir tarayıcı <link rel="preload" as="worker">'yi uygulamadı. Sonuç olarak, web işleyicilerini önceden yüklemek için kullanılabilen birincil teknik, tamamen HTTP önbelleğini kullanan <link rel="prefetch"> kullanmaktı. Bu, doğru önbelleğe alma üstbilgileriyle birlikte kullanıldığında, çalışanın oluşturulması sırasında çalışan komut dosyasının indirilmesini beklemek zorunda kalmamasını sağladı. Ancak modulepreload'ün aksine bu teknik, bağımlılıkları önceden yüklemeyi veya önceden ayrıştırmayı desteklemiyordu.

Ortak çalışanlar ne olacak?

Ortak çalışanlar, Chrome 83'ten itibaren JavaScript modülleri desteğiyle güncellendi. Özel çalışanlar gibi, {type:"module"} seçeneğiyle paylaşılan bir çalışan oluşturmak artık çalışan komut dosyasını klasik bir komut dosyası yerine modül olarak yükler:

const worker = new SharedWorker('/worker.js', {
  type: 'module'
});

JavaScript modülleri desteklenmeden önce SharedWorker() kurucusu yalnızca bir URL ve isteğe bağlı bir name bağımsız değişkeni bekliyordu. Bu, klasik paylaşılan işleyici kullanımı için çalışmaya devam edecektir. Ancak modül paylaşılan işleyici oluşturmak için yeni options bağımsız değişkenini kullanmanız gerekir. Kullanılabilir seçenekler, önceki name bağımsız değişkeninin yerini alan name seçeneği dahil olmak üzere özel bir çalışan için sunulan seçeneklerle aynıdır.

Hizmet çalışanı ne olacak?

Hizmet çalışanı spesifikasyonu, modül çalışanlarıyla aynı {type:"module"} seçeneğini kullanarak giriş noktası olarak JavaScript modülü kabul edilmesini desteklemek için zaten güncellendi. Ancak bu değişiklik henüz tarayıcılarda uygulanmadı. Bu işlem tamamlandıktan sonra, aşağıdaki kodu kullanarak bir JavaScript modülüyle bir hizmet çalışanı oluşturabilirsiniz:

navigator.serviceWorker.register('/sw.js', {
  type: 'module'
});

Spesifikasyon güncellendiğine göre tarayıcılar yeni davranışı uygulamaya başlıyor. JavaScript modüllerini hizmet işçisine getirmeyle ilgili bazı ek komplikasyonlar olduğundan bu işlem zaman alır. Hizmet çalışanı kaydının, güncellemenin tetiklenip tetiklenmeyeceğini belirlerken içe aktarılan komut dosyalarını önceki önbelleğe alınmış sürümleriyle karşılaştırması gerekir. Bu işlem, hizmet çalışanları için kullanıldığında JavaScript modülleri için de uygulanmalıdır. Ayrıca, hizmet çalışanlarının güncellemeleri kontrol ederken belirli durumlarda komut dosyaları için önbelleği atlayabilmesi gerekir.

Ek kaynaklar ve daha fazla okuma