Web Workers, ana iş parçacığını bloke eden CPU yoğun hesaplamaları ayrı bir thread'e taşır. Hangi görevlerin Worker'a uygun olduğunu, Comlink ile iletişimi nasıl sadeleştireceğinizi ve INP üzerindeki etkiyi nasıl ölçeceğinizi açıklayan pratik rehber.

Web Workers ile Ana İş Parçacığı Yükünü Azaltmak

Bir kullanıcı düğmeye tıklıyor ve sayfa donuyor. Görselde sorun yok, ağ beklemesi yok, sadece hiçlik var; sonra her şey bir anda tamamlanıyor. Bu donma, büyük ihtimalle ana iş parçacığının (main thread) JavaScript hesaplamasına kilitlendiği süredir ve tarayıcı bu süre zarfında kullanıcı etkileşimlerine yanıt veremez.

Web Workers, JavaScript'in tek iş parçacıklı yapısına getirilen en doğrudan yanıttır. Ayrı bir thread'de çalışan Worker'lar, ağır hesaplamaları ana iş parçacığından izole eder; tarayıcı render döngüsünü, kullanıcı etkileşimlerini ve animasyonları kesintisiz sürdürür. Ancak bu, her hesaplamayı Worker'a taşımanın doğru ya da verimli olduğu anlamına gelmez. Serileştirme maliyeti, başlatma gecikmesi ve DOM kısıtlaması, hangi görevlerin gerçekten Worker'a uygun olduğunu belirleyen üç temel sınırdır.

INP (Interaction to Next Paint) metriklerine bakıldığında, etkileşim gecikmelerinin büyük bölümünün ana iş parçacığındaki uzun JavaScript görevlerinden kaynaklandığı görülür. Worker mimarisini doğru anlamak, INP'yi ölçülebilir biçimde iyileştiren kararlar almanızı ve aynı zamanda gereksiz kod karmaşıklığından kaçınmanızı sağlar.

Ana iş parçacığı neden tıkanır: tarayıcının tek şeridi

Tarayıcı, JavaScript yürütümü, stil hesaplama, layout ve paint işlemlerini tek bir iş parçacığında sırayla yürütür. Bu iş parçacığı meşgulken tarayıcı yeni kullanıcı girdilerine yanıt veremez. 50 milisaniyeyi aşan bir JavaScript bloğu, tarayıcının "uzun görev" (long task) olarak işaretlediği ve INP'yi doğrudan etkileyen bir darboğaz oluşturur.

Uzun görevler çoğu zaman fark edilmeden birikir. Büyük JSON ayrıştırması, karmaşık sıralama algoritmaları, kriptografik işlemler veya yoğun veri filtreleme bunların başında gelir. Tek başına her biri 10-15 ms tutabilir; ancak bir etkileşim zincirinde art arda tetiklendiğinde toplam süre INP eşiklerini kolaylıkla aşar. Chrome DevTools'un Performance panelindeki flamegraph, bu uzun görevleri kırmızı üçgenle işaretler ve hangi fonksiyonun ne kadar süre tuttuğunu gösterir.

Ana iş parçacığının tıkanmasının bir başka kaynağı senkron API'lardır. localStorage'ın senkron yapısı veya büyük JSON.parse() çağrıları, çevrelerindeki asenkron kod ne kadar iyi yazılmış olursa olsun o an için ana iş parçacığını engeller. Worker'a geçmeden önce bu tür tuzakları ayırt etmek önemlidir; bazı sorunlar Worker değil, kodu daha küçük parçalara bölmek (scheduler.postTask() veya setTimeout ile) ya da algoritmayı iyileştirmekle çözülür. Worker'ı her sorun için varsayılan yanıt yapmak asıl darboğazı gizleyebilir.

Worker'a uygun görevler ve uygun olmayanlar

Worker, DOM'a erişemez. Bu kısıtlama hangi işlerin Worker'a taşınabileceğini büyük ölçüde belirler. DOM okuma veya yazma gerektiren her şey ana iş parçacığında kalmalıdır. Hesaplama tamamlandıktan sonra sonucu ana iş parçacığına göndermek ve orada DOM'u güncellemek ise sorunsuz çalışır.

Worker'a gerçekten uygun görevler şunlardır: büyük veri kümelerini sıralamak veya filtrelemek, JSON şemalarını doğrulamak, görüntü piksel verisi üzerinde işlem yapmak, şifreleme ve hash hesapları, Markdown veya kod renklendirme ayrıştırması, makine öğrenmesi çıkarımı (özellikle WASM ile birlikte), karmaşık istatistiksel hesaplamalar. Bu işlemlerin ortak özelliği DOM'dan bağımsız olmaları ve belirgin biçimde CPU zamanı tüketmeleridir.

Worker'a taşımak anlamsız olan ya da ters etki yapan durumlar da vardır. Çok kısa süren hesaplamalar için Worker açma maliyeti (thread oluşturma, script yükleme) asıl işten daha uzun sürebilir. Sürekli DOM güncellemesi gerektiren görevler, her sonucu postMessage ile göndermek zorunda kalır ve bu iletişim yükü avantajı silip süpürür. Ayrıca büyük nesneleri mesajla taşımak serileştirme ve kopyalama maliyeti doğurur; bu maliyetin farkında olmadan Worker'a geçmek işlemi yavaşlatabilir.

Karar basit bir test ile netleşir: işi Worker'a taşırsanız, ana iş parçacığında uzun görev olarak görünen bloğu ortadan kalkıyor mu? DevTools flamegraph'ına bakın; uzun görev Worker içinde değil, postMessage serileştirmesinde veya sonucu aldıktan sonraki DOM güncellemesinde oluşuyorsa asıl sorun oradadır.

postMessage, serileştirme maliyeti ve Transferable Objects

Ana iş parçacığı ile Worker arasındaki iletişim yalnızca postMessage ve onmessage üzerinden yürür. Veri, yapısal klon algoritmasıyla kopyalanır; büyük nesnelerde bu kopya maliyetli olabilir. 10 MB'lık bir JSON nesnesini postMessage ile göndermek, nesneyi hem serileştirip hem de kopyalamayı gerektirir.

Bu maliyeti ortadan kaldırmak için Transferable Objects kullanılır. ArrayBuffer, MessagePort ve OffscreenCanvas gibi nesneler transfer edilebilir; kopyalanmak yerine sahiplik devredilir ve işlem neredeyse sıfır maliyetle gerçekleşir. Görüntü verisini ArrayBuffer olarak Worker'a aktarmak, büyük veri setlerinde bu farkı somut hale getirir.

// Kopyalama yerine transfer: sahiplik devredilir
const buffer = new ArrayBuffer(1024 * 1024 * 10); // 10 MB
worker.postMessage({ data: buffer }, [buffer]);
// buffer artık ana iş parçacığında kullanılamaz

SharedArrayBuffer, her iki tarafın aynı belleği okumasını sağlar; ancak Spectre güvenlik açığı nedeniyle yalnızca cross-origin isolation etkin sayfalarda kullanılabilir (Cross-Origin-Opener-Policy: same-origin ve Cross-Origin-Embedder-Policy: require-corp başlıkları gerekir). Her projede bu başlıkları etkinleştirmek mümkün olmayabilir.

postMessage ve onmessage ile yazılmış Worker kodu hızla karmaşıklaşır. Mesaj türlerini el ile ayırt etmek, hata yönetimini Promise'e dönüştürmek ve ikinci taraftaki olay dinleyicilerini eşleştirmek, özellikle birden fazla fonksiyon sunan Worker'larda ciddi bir boyler kodu (boilerplate) yükü oluşturur.

Comlink, bu iletişimi RPC (uzaktan prosedür çağrısı) soyutlamasına taşır. Worker'da bir nesne veya sınıfı Comlink.expose() ile dışa açarsınız; ana iş parçacığında Comlink.wrap() ile onu çağırırsınız. Her çağrı otomatik olarak Promise döndürür, serileştirme ve mesajlaşma altyapısı görünmez olur.

// worker.js
import * as Comlink from "comlink";

const api = {
  async processData(items) {
    return items
      .filter(item => item.active)
      .sort((a, b) => b.score - a.score);
  }
};

Comlink.expose(api);
// main.js
import * as Comlink from "comlink";

const worker = new Worker(new URL("./worker.js", import.meta.url), {
  type: "module"
});
const api = Comlink.wrap(worker);

const result = await api.processData(largeDataset);

Comlink'in sağladığı asıl kolaylık hata yönetimidir. Worker içinde fırlatılan bir hata, ana iş parçacığında normal try/catch bloğuyla yakalanır; onerror dinleyicilerine gerek kalmaz. Proxy tabanlı bu yapının küçük bir serileştirme ek yükü getirdiği bilinmeli; ancak çoğu durumda bu yük ölçüme yansıyacak kadar büyük değildir.

Comlink olmadan yazılmış aynı mantık için birden fazla case dalı, manuel Promise oluşturma ve her tarafta ayrı mesaj kimliği yönetimi gerekir. Bu karmaşıklık hata üretir ve bakım maliyeti doğurur. Worker sayısı veya sunulan fonksiyon sayısı arttıkça ham postMessage yaklaşımı hızla sürdürülemez hale gelir.

Worker başlatma stratejisi: ne zaman, nasıl

Worker oluşturma maliyetsiz değildir. Yeni bir Worker başlatıldığında tarayıcı ayrı bir thread açar, Worker script'ini yükler ve ayrıştırır. Bu süre tarayıcıya ve cihaza göre değişmekle birlikte, düşük güçlü mobil cihazlarda belirgin biçimde hissedilir. Worker'ı her ihtiyaç duyulduğunda oluşturup yok etmek yerine havuz (pool) veya tekil (singleton) yaklaşımı çok daha verimlidir.

Erken başlatma (eager initialization), Worker'ı sayfa yüklendiğinde veya boşta kalma sırasında oluşturup sonraki çağrılara hazır tutmayı ifade eder. requestIdleCallback içinde Worker'ı başlatmak, hem başlatma maliyetini kritik yoldan çıkarır hem de ilk kullanımda gecikme yaşatmaz. Geciktirilmiş başlatma (lazy initialization) ise Worker'ı ilk kez gerçekten ihtiyaç duyulduğunda oluşturur; kullanılmayan Worker için kaynak harcanmaz.

// Tekil Worker örneği - her çağrıda yeniden oluşturmak yerine
let workerInstance = null;

function getWorker() {
  if (!workerInstance) {
    workerInstance = new Worker(
      new URL("./data-worker.js", import.meta.url),
      { type: "module" }
    );
  }
  return workerInstance;
}

Birden fazla Worker açmak her zaman daha hızlı sonuç vermez. Tarayıcı, mantıksal çekirdek sayısına göre thread'leri fiziksel çekirdeklere dağıtır; fazla Worker açmak context switching yüküne yol açar. Paralel işlenmesi gereken bağımsız görevler dışında tek bir Worker çoğu kullanım için yeterlidir.

INP üzerindeki ölçülebilir etki ve doğrulama yöntemi

Worker mimarisi INP'yi doğrudan etkiler; ancak bu etkiyi ölçmeden önce darboğazın gerçekten ana iş parçacığındaki hesaplama olduğunu doğrulamak gerekir. Yanlış tanı, yanlış tedavi demektir.

Başlangıç noktası Chrome DevTools Performance panelindeki kayıt almaktır. Etkileşimi tetikleyin ve flamegraph'ta uzun görevleri (50 ms üzeri) bulun. Uzun görevin hangi fonksiyonda ne kadar süre geçirdiğini inceleyin. Hesaplama bir Worker'a taşındıktan sonra aynı kaydı alın; ana iş parçacığındaki uzun görev kaybolmalı veya belirgin biçimde kısalmalıdır.

PerformanceObserver API'sı ile uzun görevleri programatik olarak izleyebilirsiniz:

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 50) {
      console.log("Uzun görev:", entry.duration, "ms", entry.attribution);
    }
  }
});
observer.observe({ type: "longtask", buffered: true });

INP ölçümü için web-vitals kütüphanesi uygundur. Worker değişikliğini üretim ortamına çıkmadan önce bir test grubuna sunup INP değerlerini karşılaştırmak, değişikliğin gerçek etkisini sayıya döker. Lab ortamı (DevTools, Lighthouse) ve alan verisi (gerçek kullanıcı) arasındaki farkı unutmamak önemlidir; ağır hesaplamalar genellikle düşük güçlü cihazlarda çok daha belirgin biçimde kendini gösterir.

Doğrulama sırasında şuna dikkat edin: Worker içindeki hesaplama artık ana iş parçacığında görünmüyor olabilir, ancak postMessage sonrası DOM güncelleme kodu hâlâ ana iş parçacığında çalışır. Eğer bu güncelleme kendisi de uzun bir görevse sorun çözülmemiştir, sadece taşınmıştır. INP üç aşamadan oluşur: giriş gecikmesi, işlem süresi ve sunum gecikmesi. Worker yalnızca işlem süresini etkiler; diğer aşamalarda farklı optimizasyonlar gerekebilir.

Modül Worker ve tarayıcı desteği

type: "module" seçeneğiyle oluşturulan Worker'lar, import ifadelerini doğrudan kullanabilir. Bu, Worker kodunu modüler tutmayı ve mevcut kütüphaneleri Worker içinde içe aktarmayı kolaylaştırır. Comlink gibi araçların Worker içinde kullanılabilmesi de buna bağlıdır.

const worker = new Worker(
  new URL("./worker.js", import.meta.url),
  { type: "module" }
);

Modül Worker desteği Chromium ve Firefox'ta sağlamdır. Safari, modül Worker'ları daha geç ekledi. Eğer hedef kitlenizde eski Safari sürümleri varsa bundler (Vite, Webpack, Rollup) aracılığıyla Worker kodunu klasik script olarak paketlemek güvenli bir yol sunar. Vite'ın ?worker sözdizimi ve Webpack'in worker-loader'ı bu dönüşümü yapılandırma gerektirmeden halleder.

Service Worker ile Web Worker arasındaki fark sık karıştırılır. Service Worker, ağ isteklerini karşılar ve push bildirimleri gibi arka plan görevleri yürütür; sayfa kapandıktan sonra da çalışmaya devam edebilir. Web Worker ise hesaplama için açılan ve sayfa kapatıldığında sonlanan bir thread'dir. Render yükünü azaltmak için Web Worker, önbellekleme ve çevrimdışı destek için Service Worker tercih edilir. İkisi birbiriyle çelişmez; aynı uygulamada ikisini birlikte kullanmak mümkündür.

Pratik geçiş örnekleri: hangi kod Worker'a taşınır?

Tablo bileşenlerinde istemci tarafı sıralama ve filtreleme çok yaygın bir Worker adayıdır. Binlerce satırlık bir listeyi her sütun değişiminde sıralamak, orta düzeyde bir cihazda bile gözle görülür bir duraklama üretir. Sıralama fonksiyonunu Worker'a taşıdığınızda kullanıcı tıklar, arayüz hemen tepki verir; sonuç birkaç milisaniye sonra güncellenir. Aradaki gecikme çoğu durumda bir iskelet ekranla (skeleton loader) maskelenebilir.

Metin arama da benzer bir adaydır, özellikle fuzzy matching veya regex tabanlı aramalar söz konusuysa. Kullanıcı her tuşa basışında yüzlerce veya binlerce kaydı tarayan bir arama, anahtara basışı ile sonucun görünmesi arasına büyük bir gecikme sokabilir. Bu aramayı debounce etmek gecikmeli sonuç verir; Worker'a taşımak ise klavyeyi hızlı yanıt vermeye devam ettirirken aramayı arka planda çalıştırır.

Görüntü işleme, Worker'ın en belirgin avantaj sağladığı alandır. Resim kırpma, renk dönüşümü, histogram hesabı veya piksel bazlı filtreler, OffscreenCanvas API'sı ile tamamen Worker içinde yürütülebilir. Kullanıcı fotoğraf düzenleme arayüzünde kaydırıcıyı hareket ettirdiğinde ana iş parçacığı serbest kalır, arayüz donmaz.

Dikkatli olunması gereken durum, Worker'a taşınan işin gerçekte çok kısa sürdüğünün ortaya çıkmasıdır. 5 ms süren bir sıralama işlemini Worker'a taşımak, postMessage maliyetiyle birlikte toplam süreyi artırabilir. Worker yerine algoritmayı iyileştirmek veya hesaplamayı önbelleğe almak daha uygun çözümdür. Her kararı DevTools ölçümleriyle destekleyin.

Web Workers, ana iş parçacığı yükünü azaltmak için doğru araçtır; ancak tüm sorunların tek çözümü değildir. DOM'dan bağımsız, CPU yoğun ve tekrarlanabilir hesaplamalar için gerçek bir iyileşme sağlar. Diğer durumlar için işi küçük parçalara bölmek veya algoritmayı iyileştirmek daha basit ve etkili olabilir.

INP iyileştirmesini Worker mimarisine bağlamadan önce DevTools ile hangi aşamanın sorun yarattığını netleştirin. Uzun görev ana iş parçacığında gerçek bir hesaplama ise Worker doğru yatırımdır. Eğer sorun giriş gecikmesinde veya sunum aşamasındaysa farklı bir yaklaşım gerekir.

Comlink gibi soyutlamalar, Worker mimarisini yönetilebilir tutar. Ham postMessage ile başlamak ne olduğunu anlamak için iyi bir öğrenme adımıdır; üretim kodunda ise mesajlaşma altyapısını elle yönetmek yerine RPC soyutlamasının sağladığı sadeliği tercih etmek bakım maliyetini düşürür.