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:
Web Worker — Genel amaçlı arka plan hesaplaması
SharedWorker — Birden fazla sekmenin paylaştığı worker
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 OKTransferable 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,OffscreenCanvasgibi 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.
BroadcastChannelAPI 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 eventsinstall — İlk yükleme, cache doldurma
activate — Eski cache temizleme, yeni versiyona geçiş
idle — Bekleme, event'lere yanıt verme
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
| Strateji | Açıklama | Kullanım Alanı |
|---|---|---|
| Cache First | Önce cache, sonra ağ | Statik dosyalar, fontlar, görseller |
| Network First | Önce ağ, sonra cache | API yanıtları, dinamik içerik |
| Stale While Revalidate | Cache'den ver, arka planda güncelle | Blog yazıları, profil bilgisi |
| Network Only | Sadece ağ | Auth istekleri, anlık veri |
| Cache Only | Sadece cache | Offline 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/onmessageyerine 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.
postMessageile 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 —
ArrayBuffersahiplik 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.
AI Asistan
Sorularını yanıtlamaya hazır