webroad.online
  1. 1Web
  2. 2HTML
  3. 3CSS
  4. 4JavaScript
  5. 5TypeScript
  6. 6Git
  7. 7Unelte
  8. 8React
  9. 9State management
  10. 10Next.js
  11. 11Formulare
  12. 12Date și backend
  13. 13SEO
  14. 14Tailwind CSS
  15. 15Animații
  16. 16Testare
  17. 17Arhitectură
JavaScript · Lecția 18 din 18

Web Workers, Service Workers și alți workeri

Main thread-ul, Web Workers cu postMessage, Service Workers pentru cache și offline, plus ce înseamnă „worker” pe edge.

Actualizat

Main thread-ul și de ce îngheață pagina

JS-ul paginii rulează pe main thread, același thread care desenează pagina, răspunde la click-uri și rulează React. Event loop-ul ia următorul task doar când cel curent s-a terminat. O buclă de 2 secunde (un filtru pe o imagine, parsarea unui CSV mare, o căutare fuzzy în 50.000 de rânduri) înseamnă 2 secunde fără click-uri, fără scroll fluid și fără animații din JS.

async/await nu rezolvă asta. Un Promise doar amână codul, care rulează tot pe main thread. Ca să rulezi cu adevărat în paralel, ai nevoie de alt thread, adică de un worker.

Încearcă: același calcul de 2 secunde, o dată pe main thread și o dată într-un worker.

Arată codul
<style>
  body { align-content: center; }
  .spinner { width: 56px; height: 56px; border-radius: 50%; border: 6px solid rgb(255 252 225 / 0.15); border-top-color: #0ae448; }
  .buttons { display: flex; gap: 8px; }
  button { font: 600 14px system-ui, sans-serif; padding: 8px 14px; border: 0; border-radius: 8px; cursor: pointer; background: #fffce1; color: #0e100f; }
  button.off { background: #0ae448; }
  output { font: 13px ui-monospace, monospace; color: #abff84; min-height: 1.4em; text-align: center; }
</style>
<div class="spinner"></div>
<div class="buttons">
  <button class="main">Main thread</button>
  <button class="off">Web Worker</button>
</div>
<output>2 seconds of math. Watch the spinner.</output>
<script>
  const spinner = document.querySelector('.spinner')
  const output = document.querySelector('output')

  // the spinner is driven by JS, like any interactive UI
  let angle = 0
  const spin = () => {
    angle += 6
    spinner.style.transform = `rotate(${angle}deg)`
    requestAnimationFrame(spin)
  }
  requestAnimationFrame(spin)

  // the same heavy function, used by both buttons
  function heavy(ms) {
    const end = performance.now() + ms
    let ops = 0
    while (performance.now() < end) ops++
    return ops
  }

  // the worker gets the same function as text, through a Blob URL
  const source = heavy.toString() + '\nonmessage = e => postMessage(heavy(e.data))'
  const worker = new Worker(URL.createObjectURL(new Blob([source], { type: 'text/javascript' })))
  worker.onmessage = e => {
    output.textContent = `Worker: ${e.data.toLocaleString('en')} ops, the spinner kept spinning`
  }

  document.querySelector('.main').onclick = () => {
    output.textContent = 'Main thread busy...'
    setTimeout(() => {
      const ops = heavy(2000)
      output.textContent = `Main thread: ${ops.toLocaleString('en')} ops, the spinner froze`
    }, 50)
  }
  document.querySelector('.off').onclick = () => {
    output.textContent = 'Worker busy...'
    worker.postMessage(2000)
  }
</script>

Web Workers

Un Web Worker e un script care rulează pe un thread separat, cu propriul event loop. Nu împarte variabile cu pagina: comunicați doar prin mesaje.

// main.js: pagina
const worker = new Worker(new URL('./primes.worker.js', import.meta.url), { type: 'module' })

worker.postMessage(2_000_000)                     // trimite date
worker.onmessage = event => {                     // primește rezultatul
  document.querySelector('.result').textContent = event.data
}
worker.onerror = event => console.error(event.message)
// primes.worker.js: worker-ul
import { countPrimes } from './math.js'          // { type: 'module' } permite import

self.onmessage = event => {
  self.postMessage(countPrimes(event.data))
}

worker.terminate() oprește worker-ul imediat, din pagină (sau self.close() din interior). Fă asta când componenta dispare sau când calculul nu mai e necesar, altfel thread-ul rămâne în memorie.

Ce primește worker-ul: structured clone și transferables

postMessage copiază datele cu algoritmul structured clone. Merg obiecte, array-uri, Map, Set, Date, Blob, ArrayBuffer. Nu merg funcțiile și nodurile DOM (aruncă DataCloneError), iar instanțele de clase ajung ca obiecte simple, fără metode.

Copierea a 100 MB costă timp. Un ArrayBuffer se poate transfera: memoria trece la worker fără copie, iar în pagină buffer-ul rămâne gol.

const pixels = new Uint8ClampedArray(width * height * 4)
worker.postMessage(pixels.buffer, [pixels.buffer]) // al doilea argument: lista de transferat
pixels.byteLength                                    // 0, buffer-ul aparține acum worker-ului

Ce poate și ce nu poate un worker

Nu are Are
document, DOM, window fetch, WebSocket
localStorage, alert setTimeout, setInterval
acces la variabilele paginii IndexedDB, Cache API
importScripts (clasic) sau import (module)

Regula: worker-ul calculează, pagina afișează.

Tipurile de workeri

Tip Ce face Durata de viață Când îl folosești
Dedicated Worker (new Worker) thread pentru o singură pagină cât pagina, sau până la terminate() calcule grele: imagini, parsare, criptare, căutare
Shared Worker (new SharedWorker) un singur worker comun pentru toate tab-urile aceluiași site cât e deschis măcar un tab o conexiune WebSocket comună pentru toate tab-urile; verifică suportul pe mobil
Service Worker proxy între pagină și rețea pornit și oprit de browser, după nevoie cache, offline, push, PWA
Worklets (Audio, Paint) mini-scripturi rulate în pipeline-ul de audio sau de desenare controlată de browser procesare audio în timp real, desen CSS custom cu paint() (doar în Chromium)

Service Workers

Un Service Worker stă între pagină și rețea: fiecare request al paginii (HTML, CSS, imagini, fetch) trece prin evenimentul lui fetch, iar el decide dacă răspunde din cache, din rețea sau cu altceva. Funcționează doar pe HTTPS (și pe localhost, pentru development), pentru că un proxy injectat pe HTTP ar fi un cadou pentru atacatori.

register() din pagină→install -> pune în cache fișierele de bază→activate -> șterge cache-urile vechi→fetch -> interceptează request-urile
// în pagină
if ('serviceWorker' in navigator) navigator.serviceWorker.register('/sw.js')
// sw.js
const CACHE = 'static-v2'

self.addEventListener('install', event => {
  event.waitUntil(caches.open(CACHE).then(cache => cache.addAll(['/offline.html', '/styles.css'])))
})

self.addEventListener('activate', event => {
  event.waitUntil(caches.keys().then(keys =>
    Promise.all(keys.filter(key => key !== CACHE).map(key => caches.delete(key)))))
})

self.addEventListener('fetch', event => {
  event.respondWith(fetch(event.request).catch(() => caches.match('/offline.html')))
})

Scope: un SW controlează doar URL-urile din folderul lui și de sub el. /sw.js controlează tot site-ul, /blog/sw.js doar /blog/.... De aceea fișierul stă de obicei la rădăcină.

Strategii de cache

Strategie Cum Potrivită pentru
cache-first cache, iar dacă lipsește, rețea fișiere cu hash în nume (app.3f9a.js), fonturi, imagini
network-first rețea, iar dacă pică, cache HTML, date care trebuie să fie proaspete
stale-while-revalidate răspunde din cache și actualizează cache-ul din rețea, pentru data viitoare avatare, feed-uri, API-uri unde o versiune puțin veche e OK
async function staleWhileRevalidate(request) {
  const cache = await caches.open('api-v1')
  const cached = await cache.match(request)
  const fresh = fetch(request).then(response => {
    if (response.ok) cache.put(request, response.clone())
    return response
  })
  return cached ?? fresh
}

Offline, PWA și push

Un SW care servește pagini din cache plus un web app manifest (manifest.json, cu nume și iconițe) fac din site un PWA: se poate instala pe ecran și pornește și fără internet. Tot SW-ul primește notificările push: pagina se abonează cu registration.pushManager.subscribe(...), serverul trimite mesajul prin Web Push, iar SW-ul îl afișează în evenimentul push cu self.registration.showNotification(...), chiar dacă site-ul e închis. Pe iPhone, push-ul merge doar pentru un PWA adăugat pe ecranul principal.

Pericolul: cache-ul vechi

Cea mai frecventă problemă cu un SW: ai publicat o versiune nouă, dar userii o văd tot pe cea veche. De ce:

  • SW-ul vechi servește HTML-ul cache-first, deci pagina nici nu ajunge să afle de versiunea nouă;
  • browserul descarcă noul sw.js, dar acesta stă în starea waiting până se închid toate tab-urile controlate de cel vechi; un simplu refresh nu ajunge.

Cum previi:

  • network-first pentru HTML, cache-first doar pentru fișierele cu hash în nume;
  • un nume nou de cache la fiecare versiune (static-v3), iar în activate le ștergi pe cele vechi;
  • self.skipWaiting() în install și self.clients.claim() în activate, ca SW-ul nou să preia controlul imediat; sau, mai politicos, un banner „Versiune nouă, reîncarcă” care trimite skipWaiting doar la click;
  • sw.js servit cu Cache-Control: no-cache, ca browserul să verifice mereu dacă s-a schimbat;
  • ca kill switch, un sw.js nou care face self.registration.unregister().

În DevTools → Application → Service Workers vezi starea, poți forța actualizarea și poți bifa „Update on reload” cât timp lucrezi.

Workerii în practică

RPC în loc de mesaje. Când worker-ul are mai multe funcții, postMessage cu { type, payload } devine repede un switch mare. Comlink (de la Google Chrome Labs) le transformă în apeluri async obișnuite:

// primes.worker.js
import * as Comlink from 'comlink'
Comlink.expose({ countPrimes })

// main.js
const api = Comlink.wrap(new Worker(new URL('./primes.worker.js', import.meta.url), { type: 'module' }))
const total = await api.countPrimes(2_000_000)   // rulează în worker

Service Workers fără cod scris de mână. Workbox dă strategiile gata făcute (new CacheFirst(), new StaleWhileRevalidate(), precache la build). Pentru Next.js, ghidul oficial de PWA menționează Serwist, un fork modern al Workbox.

Bundlerele și new URL(..., import.meta.url). Vite, webpack 5 și Turbopack (în Next.js) recunosc exact tiparul new Worker(new URL('./file.js', import.meta.url)) și construiesc worker-ul ca fișier separat, cu importurile lui. Scrie URL-ul direct în apel, nu într-o variabilă, altfel bundlerul nu-l vede. În Next.js, un worker se creează doar într-un Client Component, în useEffect (pe server nu există Worker), iar în funcția de cleanup apelezi worker.terminate().

„Workeri” pe server: alt sens al cuvântului

Cloudflare Workers și Vercel Functions nu au legătură cu thread-urile din browser. Sunt funcții care rulează pe server sau pe edge, aproape de user, și primesc un Request și returnează un Response. API-ul Cloudflare Workers a pornit chiar de la cel al Service Worker-ilor, de aici și numele.

export default {
  async fetch(request) {
    return new Response('Hello from the edge')
  },
}

Pe scurt

  • Main thread-ul face și JS, și UI; calculele grele îl blochează, iar async nu ajută. Mută-le într-un Web Worker, cu postMessage și transfer de ArrayBuffer.
  • Un worker n-are DOM sau window, dar are fetch, timere și IndexedDB. El calculează, pagina afișează.
  • Un Service Worker e un proxy de rețea pentru cache, offline și push; versionează cache-ul și folosește network-first pentru HTML, ca să nu rămână userii pe versiunea veche.

Surse oficiale

Exerciții

Ți-a fost utilă pagina?

Un click — fără cont.