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 sunulan karmaşık uygulama sayısı arttıkça çoklu iş parçacıklı işleme ihtiyacı da artıyor.

Web platformunda iş parçacığı oluşturma ve paralellik için kullanılan temel temel öğe 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ılı aşkın süredir çoğu tarayıcıda kullanılmaktadır. 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, bir çalışana kod yüklemek ve komut dosyası oluşturmak için kullanılan API, 2009'da yaygın olarak kullanılan eşzamanlı komut dosyası yükleme yaklaşımlarına benzedi.

Tarih: klasik çalışanlar

Çalışan kurucusu, belge URL'sine göre bir klasik komut dosyası URL'si alır. Yeni çalışan örneğine hemen bir referans döndürür. Bu durumda hem mesajlaşma arayüzü hem de çalışanı hemen durduran ve yok eden bir terminate() yöntemi ortaya çıkar.

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

Web işçilerinde ek kod yüklemek için bir importScripts() işlevi kullanılabilir ancak bu işlev, her komut dosyasını getirip değerlendirmek için işçinin yürütülmesini duraklatı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, kod yükleme için importScripts() kullanan 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 sarmalayan küçük bir modül yükleyici uygulamasını oluşturulan koda yerleştirir.

Modüldeki çalışanları girin

Chrome 80'de, web çalışanları için JavaScript modüllerinin ergonomisine ve performans avantajlarına sahip yeni bir mod kullanıma sunuluyor. Bu yeni mod, modül çalışanları olarak adlandırılıyor. 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, halihazırda yürütülen modül örneğine referans verir. JavaScript modüllerinin yüklenmesi ve çalıştırılması da 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. Çalışanların JavaScript modüllerini kullanacak şekilde değiştirilmesi, 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 küresel 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ının sağladığı önemli performans iyileştirmelerinden biri, çalışanları ve bağımlılıklarını önceden yükleyebilmektir. Modül çalışanları sayesinde komut dosyaları standart JavaScript modülleri olarak yüklenir ve yürütülür. Yani önceden yüklenebilir ve hatta modulepreload kullanılarak önceden ayrıştırılabilir:

<!-- 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 başlıklarıyla birlikte kullanıldığında, çalışan örneklendirmesinin çalışan komut dosyasını indirmek için 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ılara uygulanmadı. Bu gerçekleştikten sonra, aşağıdaki kodla JavaScript modülü kullanarak Service Worker'ı örneklendirebilirsiniz:

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, bir 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 yöntemin, hizmet çalışanları için kullanıldığında JavaScript modülleri için uygulanması gerekir. 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