← Kursa Dön
📄 Text · 30 min

Web Workers ve Service Workers

JavaScript'in Tek Kol Sorunu

JavaScript single-threaded — tek bir iş parçacığında çalışır. Bunu bir restoranda tek bir garson olarak düşün: sipariş alıyor, yemek taşıyor, hesap kesiyor — ama hepsini sırayla yapıyor. Eğer bir masa çok karmaşık bir sipariş verirse (ağır hesaplama), diğer masalar beklemek zorunda kalır. Kullanıcı arayüzü donar, butonlar tepki vermez, animasyonlar takılır.

İşte Web Worker'lar bu sorunu çözer: arka planda çalışan ek iş parçacıkları. Ana thread (garson) arayüzle ilgilenirken, Worker (mutfak ekibi) ağır işleri arka planda yapar. İki taraf birbirini bloklamaz, mesajlaşarak iletişim kurar.

Bu derste üç tür worker'ı keşfedeceğiz:

  1. Web Worker — Genel amaçlı arka plan hesaplaması

  2. SharedWorker — Birden fazla sekmenin paylaştığı worker

  3. ServiceWorker — Ağ isteklerini yakalayan, offline deneyim sunan özel worker


Web Worker: Ağır İşleri Arka Plana At

Problem: UI Donması

// ❌ Ana thread'de ağır hesaplama — UI donar
function findPrimes(limit) {
  const primes = [];
  for (let i = 2; i <= limit; i++) {
    let isPrime = true;
    for (let j = 2; j <= Math.sqrt(i); j++) {
      if (i % j === 0) { isPrime = false; break; }
    }
    if (isPrime) primes.push(i);
  }
  return primes;
}

// Bu çağrı yapıldığında sayfa donar!
document.getElementById("btn").addEventListener("click", () => {
  const primes = findPrimes(10_000_000); // 🥶 Sayfa 5-10 saniye donar
  console.log(`${primes.length} asal sayı bulundu`);
});

10 milyon sayı arasında asal sayı ararken ana thread bloklanır. Kullanıcı hiçbir şeye tıklayamaz, scroll yapamaz, animasyonlar durur. Bu kötü bir kullanıcı deneyimi.

Çözüm: Web Worker

// worker.js — ayrı dosyada çalışır
self.addEventListener("message", (event) => {
  const { type, data } = event.data;

  if (type === "findPrimes") {
    const primes = findPrimes(data.limit);

    // İlerleme bildirimi (opsiyonel)
    self.postMessage({
      type: "result",
      data: { count: primes.length, sample: primes.slice(0, 10) },
    });
  }
});

function findPrimes(limit) {
  const primes = [];
  for (let i = 2; i <= limit; i++) {
    let isPrime = true;
    for (let j = 2; j <= Math.sqrt(i); j++) {
      if (i % j === 0) { isPrime = false; break; }
    }
    if (isPrime) primes.push(i);

    // Her 100.000 sayıda ilerleme bildir
    if (i % 100000 === 0) {
      self.postMessage({
        type: "progress",
        data: { current: i, total: limit, percent: Math.round(i / limit * 100) },
      });
    }
  }
  return primes;
}
// main.js — ana thread
const worker = new Worker("worker.js");

// Worker'dan mesaj al
worker.addEventListener("message", (event) => {
  const { type, data } = event.data;

  if (type === "progress") {
    document.getElementById("progress").textContent = `%${data.percent}`;
  }

  if (type === "result") {
    console.log(`${data.count} asal sayı bulundu`);
    document.getElementById("result").textContent = `${data.count} asal sayı`;
  }
});

// Hata yakalama
worker.addEventListener("error", (event) => {
  console.error("Worker hatası:", event.message);
});

// Worker'a iş gönder — UI DONMAZ!
document.getElementById("btn").addEventListener("click", () => {
  document.getElementById("progress").textContent = "Hesaplanıyor...";
  worker.postMessage({ type: "findPrimes", data: { limit: 10_000_000 } });
  // ✅ Bu satırdan sonra UI anında tepki vermeye devam eder
});

// Worker'ı sonlandır (gerektiğinde)
// worker.terminate();

Worker İçinden Erişilemeyenler

Worker ayrı bir thread'de çalışır ve DOM'a erişemez:

// ❌ Worker içinde YAPILAMAZ
document.getElementById("btn");      // ReferenceError
window.alert("test");                // ReferenceError
document.body.style.color = "red";   // ReferenceError

// ✅ Worker içinde yapılabilir
fetch("https://api.example.com/data"); // ✅ Ağ istekleri OK
setTimeout(() => {}, 1000);             // ✅ Timer'lar OK
console.log("test");                    // ✅ Console OK
crypto.randomUUID();                    // ✅ Crypto API OK
indexedDB.open("myDB");                // ✅ IndexedDB OK

Transferable Objects

Büyük veri aktarımlarında performans sorunu olabilir — postMessage veriyi kopyalar. Transferable objects ile kopyalama yerine sahiplik devri yapılır:

// ❌ Kopyalama — büyük veri için yavaş
const hugeArray = new Float64Array(1_000_000);
worker.postMessage({ data: hugeArray }); // Kopyalanır, bellek 2x

// ✅ Transfer — sıfır kopya, anında
const hugeArray = new Float64Array(1_000_000);
worker.postMessage({ data: hugeArray }, [hugeArray.buffer]);
// hugeArray artık ana thread'de KULLANILAMAZ (sahiplik devredildi)
console.log(hugeArray.byteLength); // 0 — boşaltıldı

💡 İpucu: Transferable sadece ArrayBuffer, MessagePort, ImageBitmap, OffscreenCanvas gibi nesneler için çalışır. Normal object/array transfer edilemez — kopyalanır.

Inline Worker (Dosyasız)

Bazen ayrı dosya oluşturmak istemezsin. Blob URL ile inline worker yapabilirsin:

function createInlineWorker(fn) {
  const blob = new Blob(
    [`self.onmessage = ${fn.toString()}`],
    { type: "application/javascript" }
  );
  const url = URL.createObjectURL(blob);
  const worker = new Worker(url);

  // URL'yi temizle (worker zaten yüklendi)
  URL.revokeObjectURL(url);

  return worker;
}

// Kullanım
const sortWorker = createInlineWorker(function (event) {
  const arr = event.data;
  arr.sort((a, b) => a - b); // Ağır sıralama
  self.postMessage(arr);
});

sortWorker.onmessage = (e) => console.log("Sıralandı:", e.data);
sortWorker.postMessage([5, 3, 8, 1, 9, 2, 7, 4, 6]);

SharedWorker: Sekmeler Arası Paylaşılan Worker

Normal Web Worker her sekme için ayrı bir instance oluşturur. SharedWorker ise birden fazla sekme (veya iframe) aynı worker'ı paylaşır. Ortak bir sohbet sunucusu gibi düşün: her sekme aynı sunucuya bağlanır, birbirlerinin mesajlarını görebilir.

// shared-worker.js
const connections = [];

self.addEventListener("connect", (event) => {
  const port = event.ports[0];
  connections.push(port);

  port.addEventListener("message", (e) => {
    const { type, data } = e.data;

    if (type === "broadcast") {
      // Tüm bağlı sekmelere mesaj gönder
      connections.forEach(p => {
        p.postMessage({ type: "message", data });
      });
    }

    if (type === "getConnectionCount") {
      port.postMessage({
        type: "connectionCount",
        data: connections.length,
      });
    }
  });

  port.start();

  // Yeni bağlantıyı tüm sekmelere bildir
  connections.forEach(p => {
    p.postMessage({
      type: "connectionCount",
      data: connections.length,
    });
  });
});
// main.js — her sekmede
const worker = new SharedWorker("shared-worker.js");

worker.port.addEventListener("message", (event) => {
  const { type, data } = event.data;

  if (type === "message") {
    console.log("Mesaj:", data);
    // Tüm sekmelerde görünür!
  }

  if (type === "connectionCount") {
    console.log(`Aktif sekme sayısı: ${data}`);
  }
});

worker.port.start();

// Tüm sekmelere mesaj gönder
worker.port.postMessage({
  type: "broadcast",
  data: "Merhaba tüm sekmeler!",
});

⚠️ Dikkat: SharedWorker desteği sınırlı — Safari ve bazı mobil tarayıcılarda çalışmayabilir. BroadcastChannel API daha geniş desteklenir ve benzer işlevi sağlar.


Service Worker: Offline Deneyim

Service Worker, diğer worker'lardan tamamen farklı bir amaca hizmet eder: ağ isteklerini yakalayıp yönetir. Bir resepsiyon görevlisi gibi düşün: her gelen isteği karşılar, cache'de varsa oradan verir, yoksa sunucuya yönlendirir. İnternet kesilse bile cache'den hizmet vermeye devam eder.

Service Worker'lar Progressive Web App (PWA) teknolojisinin temelini oluşturur.

Yaşam Döngüsü

        install → activate → idle
            ↓                  ↕
        waiting           fetch/message events
  1. install — İlk yükleme, cache doldurma

  2. activate — Eski cache temizleme, yeni versiyona geçiş

  3. idle — Bekleme, event'lere yanıt verme

  4. fetch — Her ağ isteğini yakalama

Kayıt (Registration)

// main.js — Service Worker'ı kaydet
async function registerServiceWorker() {
  if (!("serviceWorker" in navigator)) {
    console.log("Service Worker desteklenmiyor");
    return;
  }

  try {
    const registration = await navigator.serviceWorker.register(
      "/sw.js",
      { scope: "/" } // Hangi URL'leri kontrol edecek
    );

    console.log("SW kayıt oldu:", registration.scope);

    // Güncelleme kontrolü
    registration.addEventListener("updatefound", () => {
      const newWorker = registration.installing;
      console.log("Yeni SW versiyonu bulundu");

      newWorker.addEventListener("statechange", () => {
        if (newWorker.state === "activated") {
          console.log("Yeni versiyon aktif — sayfayı yenileyin");
        }
      });
    });
  } catch (error) {
    console.error("SW kayıt hatası:", error);
  }
}

registerServiceWorker();

Cache Stratejileri

// sw.js — Service Worker dosyası

const CACHE_NAME = "app-cache-v1";
const STATIC_ASSETS = [
  "/",
  "/index.html",
  "/styles.css",
  "/app.js",
  "/offline.html",
];

// 1. INSTALL — Statik dosyaları cache'le
self.addEventListener("install", (event) => {
  console.log("SW: Install");
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache => {
      console.log("SW: Statik dosyalar cache'leniyor");
      return cache.addAll(STATIC_ASSETS);
    })
  );
  self.skipWaiting(); // Bekleme olmadan aktif ol
});

// 2. ACTIVATE — Eski cache'leri temizle
self.addEventListener("activate", (event) => {
  console.log("SW: Activate");
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames
          .filter(name => name !== CACHE_NAME) // Eski versiyon
          .map(name => caches.delete(name))     // Sil
      );
    })
  );
  self.clients.claim(); // Mevcut sekmeleri hemen kontrol et
});

// 3. FETCH — İstekleri yakala
self.addEventListener("fetch", (event) => {
  // Sadece GET istekleri cache'lensin
  if (event.request.method !== "GET") return;

  event.respondWith(handleFetch(event.request));
});

Farklı Cache Stratejileri

// Strateji 1: Cache First (Offline First)
// Önce cache'e bak, yoksa ağdan getir
async function cacheFirst(request) {
  const cached = await caches.match(request);
  if (cached) return cached;

  try {
    const response = await fetch(request);
    const cache = await caches.open(CACHE_NAME);
    cache.put(request, response.clone());
    return response;
  } catch {
    // Offline ve cache'de yok — fallback sayfası göster
    return caches.match("/offline.html");
  }
}

// Strateji 2: Network First
// Önce ağdan dene, başarısızsa cache'e bak
async function networkFirst(request) {
  try {
    const response = await fetch(request);
    const cache = await caches.open(CACHE_NAME);
    cache.put(request, response.clone());
    return response;
  } catch {
    const cached = await caches.match(request);
    return cached || caches.match("/offline.html");
  }
}

// Strateji 3: Stale While Revalidate
// Cache'den hemen ver, arka planda güncelle
async function staleWhileRevalidate(request) {
  const cache = await caches.open(CACHE_NAME);
  const cached = await cache.match(request);

  // Arka planda güncelle
  const fetchPromise = fetch(request).then(response => {
    cache.put(request, response.clone());
    return response;
  });

  // Cache varsa hemen dön, yoksa ağdan bekle
  return cached || fetchPromise;
}

// Ana handler — URL'ye göre strateji seç
async function handleFetch(request) {
  const url = new URL(request.url);

  // Statik dosyalar → Cache First
  if (STATIC_ASSETS.includes(url.pathname)) {
    return cacheFirst(request);
  }

  // API istekleri → Network First
  if (url.pathname.startsWith("/api/")) {
    return networkFirst(request);
  }

  // Diğer her şey → Stale While Revalidate
  return staleWhileRevalidate(request);
}

Cache Stratejileri Özet Tablosu

StratejiAçıklamaKullanım Alanı
Cache FirstÖnce cache, sonra ağStatik dosyalar, fontlar, görseller
Network FirstÖnce ağ, sonra cacheAPI yanıtları, dinamik içerik
Stale While RevalidateCache'den ver, arka planda güncelleBlog yazıları, profil bilgisi
Network OnlySadece ağAuth istekleri, anlık veri
Cache OnlySadece cacheOffline paket içi dosyalar

💡 İpucu: Gerçek projelerde Workbox kütüphanesini kullan (Google tarafından geliştirilir). Cache stratejilerini sıfırdan yazmak yerine hazır, test edilmiş implementasyonlar sunar.


Offline Uygulama Örneği

Tüm kavramları birleştiren basit bir offline-ready uygulama:

// sw.js — Offline Not Defteri Service Worker
const CACHE_NAME = "notes-v1";
const OFFLINE_QUEUE_KEY = "offlineQueue";

// Install: uygulama shell'ini cache'le
self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache =>
      cache.addAll(["/", "/index.html", "/app.js", "/styles.css"])
    )
  );
  self.skipWaiting();
});

// Activate: eski cache temizle
self.addEventListener("activate", (event) => {
  event.waitUntil(
    caches.keys().then(names =>
      Promise.all(names.filter(n => n !== CACHE_NAME).map(n => caches.delete(n)))
    )
  );
  self.clients.claim();
});

// Fetch: GET → cache first, POST → online kuyruk
self.addEventListener("fetch", (event) => {
  if (event.request.method === "GET") {
    event.respondWith(
      caches.match(event.request).then(cached =>
        cached || fetch(event.request).then(response => {
          const clone = response.clone();
          caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone));
          return response;
        }).catch(() => caches.match("/offline.html"))
      )
    );
  }

  // POST istekleri — offline kuyruk
  if (event.request.method === "POST") {
    event.respondWith(
      fetch(event.request.clone()).catch(async () => {
        // Offline — isteği IndexedDB kuyruğuna ekle
        const body = await event.request.json();
        await saveToOfflineQueue({
          url: event.request.url,
          method: "POST",
          body,
          timestamp: Date.now(),
        });
        return new Response(JSON.stringify({ queued: true }), {
          headers: { "Content-Type": "application/json" },
        });
      })
    );
  }
});

// Online olduğunda kuyruktaki istekleri gönder
self.addEventListener("sync", (event) => {
  if (event.tag === "sync-notes") {
    event.waitUntil(processOfflineQueue());
  }
});

async function processOfflineQueue() {
  // IndexedDB'den kuyruktaki istekleri al ve gönder
  const queue = await getOfflineQueue();
  for (const item of queue) {
    try {
      await fetch(item.url, {
        method: item.method,
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(item.body),
      });
      await removeFromQueue(item.timestamp);
    } catch {
      break; // Hâlâ offline — durdur
    }
  }
}

Worker Pool Pattern

Birden fazla worker'ı havuz (pool) olarak yönetmek, birçok paralel görevi verimli şekilde dağıtır:

// workerPool.js — Basit Worker Pool implementasyonu
class WorkerPool {
  constructor(workerScript, poolSize = navigator.hardwareConcurrency || 4) {
    this.workers = [];
    this.queue = [];
    this.activeJobs = new Map();

    // Worker havuzunu oluştur
    for (let i = 0; i < poolSize; i++) {
      const worker = new Worker(workerScript);
      worker.busy = false;
      worker.id = i;

      worker.addEventListener("message", (event) => {
        const { jobId, result } = event.data;
        const resolve = this.activeJobs.get(jobId);
        if (resolve) {
          resolve(result);
          this.activeJobs.delete(jobId);
        }
        worker.busy = false;
        this._processQueue(); // Sıradaki işi ata
      });

      this.workers.push(worker);
    }
  }

  // İş gönder — Promise döndürür
  execute(data) {
    return new Promise((resolve) => {
      const jobId = crypto.randomUUID();
      this.activeJobs.set(jobId, resolve);
      this.queue.push({ jobId, data });
      this._processQueue();
    });
  }

  _processQueue() {
    const freeWorker = this.workers.find(w => !w.busy);
    if (!freeWorker || this.queue.length === 0) return;

    const job = this.queue.shift();
    freeWorker.busy = true;
    freeWorker.postMessage({ jobId: job.jobId, ...job.data });
  }

  // Havuzu kapat
  terminate() {
    this.workers.forEach(w => w.terminate());
  }
}

// Kullanım
const pool = new WorkerPool("heavy-task-worker.js", 4);

// 100 görevi 4 worker'a dağıt
const results = await Promise.all(
  Array.from({ length: 100 }, (_, i) =>
    pool.execute({ type: "process", index: i })
  )
);

console.log(`100 görev ${results.length} sonuçla tamamlandı`);
pool.terminate();

💡 İpucu: Production'da Comlink kütüphanesini kullanabilirsin — Worker iletişimini proxy nesneleri ile basitleştirir. postMessage/onmessage yerine doğrudan fonksiyon çağrısı gibi kullanabilirsin.


Yaygın Hatalar

1. Worker İçinde DOM Erişimi

// ❌ Worker'da DOM yok!
// worker.js
document.getElementById("test"); // ReferenceError: document is not defined

// ✅ İhtiyacın varsa ana thread'e mesaj gönder
self.postMessage({ type: "updateUI", data: { text: "Sonuç hazır" } });

2. Service Worker Scope Hatası

/js/sw.js → Scope: /js/ (sadece /js/ altındaki sayfaları kontrol eder)
/sw.js    → Scope: /    (tüm sayfaları kontrol eder) ✅
// SW dosyasını root'a yerleştir
navigator.serviceWorker.register("/sw.js", { scope: "/" });

3. Cache Versiyonlama Unutmak

// ❌ Cache ismi değişmezse eski dosyalar sonsuza dek kalır
const CACHE_NAME = "my-cache";

// ✅ Versiyon numarası ile — her deploy'da güncelle
const CACHE_NAME = "my-cache-v2";

4. Worker'ı Durdurmamak

// ❌ Worker sonsuza dek çalışır — bellek sızıntısı
const worker = new Worker("task.js");
worker.postMessage("go");
// ... sayfadan ayrılınca worker hâlâ çalışıyor

// ✅ İş bittiğinde veya sayfa kapatılırken sonlandır
worker.addEventListener("message", (e) => {
  if (e.data.type === "done") {
    worker.terminate(); // Kaynakları serbest bırak
  }
});

// Sayfa kapatılırken
window.addEventListener("beforeunload", () => {
  worker.terminate();
});

Ne Zaman Worker Kullanmalı?

✅ Worker KULLAN:
├── Büyük veri işleme (sıralama, filtreleme, arama)
├── Resim/video işleme (resize, filter, encode)
├── Karmaşık hesaplamalar (kripto, fizik simülasyonu)
├── CSV/JSON parsing (büyük dosyalar)
└── Ağır regex işlemleri

❌ Worker KULLANMA:
├── Basit fetch istekleri (async/await yeterli)
├── DOM manipülasyonu (worker DOM'a erişemez zaten)
├── Küçük hesaplamalar (worker oluşturma maliyeti > kazanç)
└── Senkron olması gereken işler

Özet

  • Web Worker: Ağır hesaplamaları arka plana atar, ana thread'i bloklamaz. postMessage ile iletişim, DOM'a erişemez.

  • SharedWorker: Birden fazla sekme arasında paylaşılan worker. Sekmeler arası iletişim ve ortak kaynak yönetimi.

  • Service Worker: Ağ isteklerini yakalar, offline deneyim sunar. PWA'nın temeli. Cache stratejileri (Cache First, Network First, Stale While Revalidate).

  • Transferable Objects: Büyük verilerde sıfır kopya aktarım — ArrayBuffer sahiplik devri.

  • Background Sync: Offline yapılan işlemleri internet geldiğinde otomatik gönderme.

  • Worker'ların ortak kuralı: DOM'a erişemezler, ana thread ile mesajlaşarak iletişim kurarlar.