IlmHamroh
JavaScript Full-stack/13-qism. Brauzer API'lari, performans va xavfsizlik19/32-dars25 daqiqa
Mundarija (34)

JavaScript Web Workers: og'ir hisobni alohida thread'da bajarish

Qisqacha: Web Worker — sahifadan alohida thread'da ishlaydigan JavaScript fayli. U og'ir hisob qilganda sahifa qotmaydi: tugmalar bosiladi, animatsiya yuradi. Worker new Worker("fayl.js") bilan yaratiladi, sahifa bilan faqat xabar orqali gaplashadi: postMessage(ma'lumot) va message hodisasi. Xabar nusxalanadi (structured clone) yoki ArrayBuffer kabi narsalar ko'chiriladi (transfer). Worker'da DOM yo'q. { type: "module" } bilan unda import ishlaydi; Comlink kutubxonasi esa xabarlarni oddiy funksiya chaqiruviga aylantiradi.

Bu darsda

  • Og'ir hisob nega sahifani qotirishini va worker buni qanday hal qilishini tushuntira olasiz.
  • Worker yaratib, u bilan postMessage va message orqali gaplashasiz, xatolarini ushlaysiz va to'xtatasiz.
  • Qaysi ma'lumot nusxalanadi, qaysi biri ko'chiriladi (transfer) va nimani umuman yuborib bo'lmasligini bilasiz.
  • Module worker va Promise o'ramini yozasiz, Comlink bilan ishlaysiz.
  • Brauzerdagi worker'ni Node'dagi worker_threads bilan solishtira olasiz.

Oldin bilishingiz kerak: Event loop, Sayoz va chuqur nusxalash, Typed arrays va binar ma'lumot, Oyna va tablar orasida aloqa, ESM import.

1. Nega bu kerak?

Jasur aka yil oxirida hisobot so'radi: "Yil davomida qaysi taom qaysi oyda ko'p sotildi?" Sardor brauzerda hisoblaydigan sahifa yozdi. Yarim million buyurtmani aylanib chiqish — bir necha yuz millisekund, katta fayllarda — bir necha soniya. Shu vaqt ichida sahifa qotadi: «Bekor qilish» tugmasi bosilmaydi, yuklanish belgisi to'xtaydi, matn maydoniga yozib bo'lmaydi.

Sababi bizga tanish. Event loop darsida ko'rdik: sahifadagi JavaScript bitta threadda (asosiy thread'da) ishlaydi. Shu thread bosishlarni ham qayta ishlaydi, sahifani ham chizadi. U hisob bilan band bo'lsa, qolgan hamma narsa navbatda kutadi.

Bir o'xshatish. Oshxonada bitta oshpaz bor. U katta qozonda osh damlayotganda (40 daqiqalik ish) mehmonlarga choy ham, salat ham bera olmaydi. Yechim — yordamchi oshpaz: osh unga topshiriladi, bosh oshpaz mehmonlar bilan ishlayveradi. Yordamchi tayyor bo'lganda "osh tayyor!" deb xabar beradi. Web Worker — aynan shu yordamchi oshpaz: o'z thread'ida, o'z ishini qiladi va natijani xabar qilib yuboradi.

Muhim farq: yordamchi oshpaz zalga chiqmaydi — mehmonlar bilan gaplashmaydi. Worker ham sahifaga (DOM'ga) tegolmaydi. U faqat hisoblaydi va xabar yuboradi.

2. Muammoni ko'ramiz

Quyidagi oynada yashil nuqta requestAnimationFrame bilan yuradi — har kadrda JavaScript uni suradi. «Hisoblash» tugmasi 2 000 000 gacha bo'lgan tub sonlarni sanaydi. Tub son — faqat 1 ga va o'ziga bo'linadigan son: 2, 3, 5, 7, 11… Har sonni tekshirish uchun uni kichikroq sonlarga bo'lib ko'rish kerak — protsessorni band qiladigan yaxshi "og'ir ish". Avval matn maydoniga yozib ko'ring, keyin tugmani bosib, darhol yana yozing:

html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  .track { position: relative; height: 1.5rem; background: #eef3ee;
    border-radius: 1rem; overflow: hidden; margin-bottom: 0.5rem; }
  #dot { position: absolute; top: 0.25rem; width: 1rem; height: 1rem;
    border-radius: 50%; background: #2e7d32; }
  button, input { font: inherit; padding: 0.3rem 0.6rem; }
</style>
<div class="track"><div id="dot"></div></div>
<p><input aria-label="Sinov uchun yozing"
    placeholder="Bu yerga yozing">
  <button id="run" type="button">Hisoblash</button></p>
<p id="info"></p>
<script>
  // Nuqta har kadrda JavaScript bilan suriladi
  const dot = document.getElementById("dot");
  let x = 0;
  function frame() {
    x = (x + 3) % 280;
    dot.style.transform = `translateX(${x}px)`;
    requestAnimationFrame(frame);
  }
  requestAnimationFrame(frame);

  function countPrimes(limit) {
    let count = 0;
    for (let n = 2; n < limit; n++) {
      let isPrime = true;
      for (let d = 2; d * d <= n; d++) {
        if (n % d === 0) {
          isPrime = false;
          break;
        }
      }
      if (isPrime) count++;
    }
    return count;
  }

  document.getElementById("run").addEventListener("click", () => {
    const started = performance.now();
    const count = countPrimes(2_000_000);
    const ms = Math.round(performance.now() - started);
    document.getElementById("info").textContent =
      `Asosiy thread ${ms} ms band bo'ldi`;
    console.log(`Tub sonlar: ${count}`);
  });
</script>

Tugma bosilganda nuqta to'xtaydi, yozgan harflaringiz esa hisob tugagach birdaniga paydo bo'ladi. Konsolda Tub sonlar: 148933 chiqadi (biz natija oynasida bosib sinadik), sahifada esa thread qancha band bo'lgani.

Biz Chrome 154 da (Intel Core i5-12500H, 5 marta, mediana) o'lchadik: hisob taxminan 370 ms davom etdi. Shu paytda nuqtani suradigan kadrlar orasidagi eng uzun tanaffus 360–880 ms bo'ldi — sahifa butunlay "muzladi". Hisobni worker'ga o'tkazganimizda esa eng uzun tanaffus — 14 ms, ya'ni oddiy kadr. Bu natijalar "Worker qancha turadi?" grafigida.

Bu bilan Promise ham yordam bermaydi: Promise kutishni o'zi yaratmaydi, hisob baribir shu thread'da bajariladi. Kerakli narsa — ikkinchi thread.

3. Birinchi worker

3.1 Ikki fayl

Worker — alohida JavaScript fayli. Sahifa uni new Worker("fayl.js") bilan ishga tushiradi. Ikkalasi orasida faqat xabarlar yuradi:

sequenceDiagram
  participant S as Sahifa (asosiy thread)
  participant W as Worker (alohida thread)
  S->>W: new Worker("hisob-worker.js")
  S->>W: postMessage(2000000)
  Note over S: tugmalar, animatsiya<br/>ishlayveradi
  Note over W: countPrimes(...)<br/>~370 ms
  W-->>S: postMessage({ count })
  S->>S: "message" hodisasi → natijani ko'rsatish

Nimaga qarang: sahifa xabar yuborgach kutmaydi — o'z ishini davom ettiradi. Javob kelganda message hodisasi keladi, xuddi tugma bosilgandagi kabi.

kurs/mashqlar/13/19-worker/ papkasida ikki fayl yarating. Birinchisi — worker:

js
// hisob-worker.js — alohida thread'da ishlaydi
function countPrimes(limit) {
  let count = 0;
  for (let n = 2; n < limit; n++) {
    let isPrime = true;
    for (let d = 2; d * d <= n; d++) {
      if (n % d === 0) {
        isPrime = false;
        break;
      }
    }
    if (isPrime) count++;
  }
  return count;
}

self.addEventListener("message", (event) => {
  const limit = event.data;           // sahifa yuborgan son
  const count = countPrimes(limit);
  self.postMessage({ limit, count });  // natijani qaytarish
});

Ikkinchisi — sahifa:

html
<!doctype html>
<html lang="uz">
<head><meta charset="utf-8"><title>Worker</title></head>
<body>
  <button id="run" type="button">Worker'da hisoblash</button>
  <p id="info" role="status"></p>
  <script>
    const worker = new Worker("hisob-worker.js");
    const info = document.getElementById("info");

    worker.addEventListener("message", (event) => {
      const { limit, count } = event.data;
      info.textContent = `${limit} gacha ${count} ta tub son`;
      console.log(`Tub sonlar: ${count}`);
    });

    worker.addEventListener("error", (event) => {
      console.log(`Worker xatosi: ${event.message}`);
    });

    document.getElementById("run").addEventListener("click", () => {
      info.textContent = "Hisoblanmoqda…";
      worker.postMessage(2_000_000);
    });
  </script>
</body>
</html>

Live Server bilan oching va tugmani bosing. Biz Chrome 154 da sinadik: konsolda Tub sonlar: 148933, sahifa esa hisob paytida ham javob berib turdi.

3.2 Qatorma-qator

  • new Worker("hisob-worker.js") — brauzer faylni yuklaydi va yangi threadda ishga tushiradi. Yo'l sahifaga nisbatan (skriptga emas).
  • worker.postMessage(2_000_000) — xabar yuborish. Bu chaqiruv darhol qaytadi — javobni kutmaydi.
  • Worker ichida self — worker'ning global obyekti (sahifadagi window o'rnida). self.addEventListener("message", ...) — sahifadan kelgan xabarlarni tinglash. self ni yozmasa ham bo'ladi (postMessage(...)), lekin u bilan "bu worker'ning o'zi" ekani ko'rinib turadi.
  • event.data — kelgan ma'lumot. Ikkala tomonda ham shunday.
  • Worker hisoblab bo'lgach, self.postMessage(...) bilan javob yuboradi; sahifada message hodisasi keladi.

Bu Oyna va tablar orasida aloqa darsidagi postMessage ning o'zi — o'sha yerda "worker bilan aloqa ham shunday" degan edik.

Worker bir marta yaratiladi va ko'p marta ishlatiladi: har bosishda yangi xabar, yangi worker emas. Uni ishga tushirish ham vaqt oladi — sinovimizda taxminan 10–20 ms.

3.3 Nega natija oynasida emas

Bu darsdagi worker misollari Live Server'da. Natija oynasida worker ishlamaydi — buni o'zingiz ko'ring:

html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  button { font: inherit; padding: 0.3rem 0.6rem; }
</style>
<button id="try" type="button">Worker yaratish</button>
<script>
  document.addEventListener("securitypolicyviolation", (event) => {
    const { violatedDirective, blockedURI } = event;
    console.log(`CSP: ${violatedDirective} — ${blockedURI}`);
  });

  document.getElementById("try").addEventListener("click", () => {
    // 1) Fayldan — natija oynasida fayl yo'q
    try {
      new Worker("hisob-worker.js");
    } catch (error) {
      console.log(error.name);
    }
    // 2) Kod satridan, blob: URL orqali
    const code = "postMessage('salom')";
    const blob = new Blob([code], { type: "text/javascript" });
    const worker = new Worker(URL.createObjectURL(blob));
    worker.addEventListener("error", () => {
      console.log("Worker ishga tushmadi");
    });
  });
</script>

Bosganda konsolda (biz natija oynasida sinadik) uch qator chiqadi: SecurityError, CSP: worker-src — blob va Worker ishga tushmadi. Ikki sabab bor:

  • Natija oynasining origin'i null. Brauzer null origin'li sahifaga sayt fayllaridan worker yaratishga ruxsat bermaydi. Xato matnining to'liq ko'rinishi: Script at 'https://ilmhamroh.uz/…/hisob-worker.js' cannot be accessed from origin 'null' — "skriptga null origin'dan kirib bo'lmaydi".
  • Kodni Blob qilib, blob: URL bilan worker yaratish — ko'p ishlatiladigan hiyla. Lekin saytning CSP'si (eval, new Function va with darsida ko'rgan xavfsizlik siyosati) blob: manbali skriptlarga ruxsat bermaydi. securitypolicyviolation hodisasi qaysi qoida buzilganini aytdi: worker-src.

Bu sizning saytingiz uchun ham foydali saboq: CSP qat'iy bo'lsa, blob: worker'lar ishlamaydi. Worker'ni alohida fayl qilib berish — eng ishonchli yo'l.

Tekshirib ko'ring: Sardor worker'ni Live Server'siz, index.html ni ikki marta bosib (fayl sifatida) ochdi. Nima bo'ladi?

Javob

Worker yaratilmaydi. Fayldan ochilgan sahifaning manzili file:///… va brauzer uning origin'ini null deb hisoblaydi. Biz sinadik — xato natija oynasidagi bilan bir xil: SecurityError: Failed to construct 'Worker': Script at 'file:///C:/…/w.js' cannot be accessed from origin 'null'. Worker uchun sahifa server orqali (Live Server, http://localhost) ochilishi kerak.

4. Worker ichida nima bor, nima yo'q

Worker — "kesilgan" brauzer muhiti. Biz Chrome 154 da worker ichida typeof bilan tekshirdik:

Bor Yo'q
fetch, setTimeout, setInterval document va DOM
indexedDB, crypto, performance window
OffscreenCanvas, WebSocket localStorage, sessionStorage
importScripts (oddiy worker'da) alert, confirm

Worker'ning global obyekti — DedicatedWorkerGlobalScope (worker konsolida self shunday ko'rinadi). "Dedicated" — "bitta sahifaga bag'ishlangan". Bir nechta tab bitta worker'ni bo'lishadigan SharedWorker va sahifa bilan tarmoq orasida turadigan Service Worker ham bor — ikkinchisi ikki dars keyin, Service Worker hayot sikli darsida.

Worker DOM'ga tegsa nima bo'ladi? Biz worker ichida document.title = "x" yozdik. Sahifadagi error hodisasiga shu keldi:

text
Uncaught ReferenceError: document is not defined

Tarjimasi: "document aniqlanmagan". event.filename va event.lineno — worker faylidagi aniq joy. Natijani sahifaga chiqarish faqat sahifaning ishi: worker raqamni yuboradi, sahifa uni DOM'ga yozadi.

OffscreenCanvas bu yerda alohida e'tiborga loyiq: Canvas amaliyotda darsida aytgandik — u worker'da ishlaydi. Rasm filtrlari, grafik chizish kabi og'ir canvas ishlari shunday qilib butunlay asosiy thread'dan chiqariladi.

5. Xabarlar: nusxa yoki ko'chirish

5.1 Structured clone

postMessage ga berilgan obyekt worker'ga havola sifatida bormaydi — u nusxalanadi. Ikki thread bitta obyektga birga yozsa, tartibsizlik bo'lardi; shuning uchun har biri o'z nusxasi bilan ishlaydi. Nusxalash algoritmi — structured clone. Siz uni Sayoz va chuqur nusxalash darsidagi structuredClone() dan bilasiz — bu aynan o'sha algoritm. Shuning uchun uning qoidalarini Node'da ham sinash mumkin:

js
class Order {
  constructor(id, total) {
    this.id = id;
    this.total = total;
  }
  format() {
    return `#${this.id}`;
  }
}

const copy = structuredClone(new Order(7, 35000));
console.log(copy);                 // { id: 7, total: 35000 }
console.log(copy instanceof Order); // false
console.log(typeof copy.format);   // undefined

const data = structuredClone({
  at: new Date("2026-10-05T12:00:00Z"),
  tags: new Set(["osh"]),
});
console.log(data);

Konsolda:

text
{ id: 7, total: 35000 }
false
undefined
{ at: 2026-10-05T12:00:00.000Z, tags: Set(1) { 'osh' } }

Ikki xulosa:

  • Klass nusxalanmaydi — faqat ma'lumot. Order obyekti oddiy obyektga aylandi: prototip va metodlar yo'qoldi. Worker'da metod kerak bo'lsa, u yerda klassni qayta yarating: new Order(data.id, data.total). Biz brauzerda ham sinadik — worker'ga kelgan obyektning prototipi oddiy Object.prototype edi.
  • JSON'dan kuchliroq. Date, Map, Set, ArrayBuffer, typed arrays, Blob, File, Error va hatto halqali (o'ziga havola qiluvchi) obyektlar to'g'ri nusxalanadi. JSON bularning yarmini buzardi.

5.2 Yuborib bo'lmaydiganlar

Funksiya va DOM elementi nusxalanmaydi:

js
structuredClone({ onDone: () => 1 });

Node 24 da: DataCloneError: () => 1 could not be cloned. — "nusxalab bo'lmadi". Brauzerda worker'ga funksiya yuborilganda Chrome 154 matni:

text
DataCloneError: Failed to execute 'postMessage' on 'Worker': (x) => x could not be cloned.

DOM elementi uchun esa: HTMLBodyElement object could not be cloned. Funksiya — kod va uning closure muhiti; uni boshqa thread'ga "ko'chirib" bo'lmaydi. Funksiya o'rniga buyruq nomini yuboring ({ task: "countPrimes" }), funksiyaning o'zi worker ichida bo'lsin — Promise o'rami bo'limida shunday qilamiz.

5.3 Transfer: nusxasiz ko'chirish

Nusxalash bepul emas. Rasm piksellari yoki katta fayl kabi o'nlab megabaytli ArrayBuffer har safar nusxalansa, vaqt va xotira ketadi. Buning uchun transfer (ko'chirish) bor: bufer nusxalanmaydi — uning egaligi boshqa thread'ga o'tadi.

js
const buffer = new ArrayBuffer(8 * 1024 * 1024); // 8 MB
// ikkinchi argument — ko'chiriladiganlar ro'yxati
worker.postMessage(buffer, [buffer]);
console.log(buffer.byteLength); // 0 — endi u worker'niki

Ikkinchi argument — transfer ro'yxati. Undagi narsalar ko'chiriladi. Sahifadagi buffer "ajratilgan" (detached) holatga o'tadi: uzunligi 0, ichiga yozib ham, qayta yuborib ham bo'lmaydi. Biz sinadik: sahifada byteLength — 0, worker'da — 8 388 608. Uni ikkinchi marta yuborganda:

text
DataCloneError: Failed to execute 'postMessage' on 'Worker': An ArrayBuffer is detached and could not be cloned.

Tarjimasi: "ArrayBuffer ajratilgan, uni nusxalab bo'lmaydi". Bu xuddi kalitni boshqaga berib yuborgandek: kalit endi sizda yo'q.

Xuddi shu qoida structuredClone da ham ishlaydi — Node'da ko'ring:

js
const buffer = new ArrayBuffer(8);
const moved = structuredClone(buffer, { transfer: [buffer] });
console.log(buffer.byteLength, moved.byteLength); // 0 8
console.log(buffer.detached); // true

Ko'chiriladigan (transferable) narsalar: ArrayBuffer, MessagePort, ImageBitmap, OffscreenCanvas, oqimlar va boshqalar. Oddiy obyekt yoki satr — yo'q: ular doim nusxalanadi.

Farq qanchalik katta? Chrome 154 da o'lchadik (5 marta, mediana):

Worker qancha turadi? (Chrome 154)
  • Hisob asosiy thread'da2 mln gacha tub sonlar374 ms
  • 64 MB nusxastructured clone47 ms
  • Worker'ni yaratishbirinchi javobgacha14 ms
  • 64 MB transferko'chirish2 ms

Manba: O'lchandi: Chrome 154, Intel Core i5-12500H, 5 marta mediana, Live Server, 2026-10-05

Nimaga qarang: 64 MB ni nusxalash 47 ms — bir necha kadr. Ko'chirish esa 2 ms: o'lcham qanchalik katta bo'lmasin, deyarli bepul. Worker'ni yaratish ham arzon, lekin har hisob uchun yangisini yaratish shart emas. Asosiy xulosa: kichik hisob uchun worker'ning "xarajati" foydasidan ko'p bo'lishi mumkin. Worker 50 ms dan uzun ishlar uchun. Nega aynan 50? Brauzer asosiy thread'ni 50 ms dan uzoq band qilgan ishni uzun task (long task) deb belgilaydi — Sinxron va asinxron kod darsida tilga olgan edik. Undan qisqa ishni foydalanuvchi deyarli sezmaydi.

Tekshirib ko'ring: Sahifa worker'ga { pixels: buffer, name: "osh.jpg" } ni postMessage(data, [buffer]) bilan yubordi. Nima nusxalanadi, nima ko'chiriladi?

Javob

buffer ko'chiriladi — u transfer ro'yxatida. Obyektning qolgan qismi (name satri va obyektning o'zi) nusxalanadi. Ro'yxatdagi narsa obyekt ichida qayerda bo'lishidan qat'i nazar, nusxalanmay ko'chadi. Yuborgandan keyin sahifadagi buffer.byteLength — 0.

6. Xatolar va to'xtatish

  • error hodisasi — worker ichida ushlanmagan xato bo'lsa, sahifadagi worker obyektida keladi: event.message, event.filename, event.lineno. event.preventDefault() chaqirilsa, xato konsolga qizil bo'lib chiqmaydi.
  • messageerror — kelgan xabarni ochib (deserialize qilib) bo'lmasa. Kam uchraydi.
  • worker.terminate() — sahifadan worker'ni darhol o'ldirish. Hisob yarmida bo'lsa ham to'xtaydi, javob kelmaydi. «Bekor qilish» tugmasi uchun aynan shu.
  • self.close() — worker o'zini o'zi yopadi (ishini tugatgach).

Worker xotira va thread egallaydi. Sahifada kerak bo'lmay qolsa — terminate(). Lekin har hisobdan keyin o'chirib, yangisini yaratish ham shart emas: bitta worker ko'p ish bajaradi.

Tekshirib ko'ring: Worker 5 soniyalik hisob qilyapti. Sahifa unga postMessage("to'xta") yubordi. Worker to'xtaydimi?

Javob

Yo'q. Worker'ning o'z thread'i ham bitta: u hisob bilan band, yangi xabarni hisob tugagach o'qiydi. Yarim yo'lda to'xtatishning yagona ishonchli yo'li — sahifadan worker.terminate().

7. Module worker va Promise o'rami

7.1 type: "module"

Oddiy worker'da import yozib bo'lmaydi. Biz sinadik:

text
Uncaught SyntaxError: Cannot use import statement outside a module

Tarjimasi: "import ni moduldan tashqarida ishlatib bo'lmaydi" — ESM darsidagi tanish xato. Yechim — worker'ni modul qilib yaratish:

js
const worker = new Worker("hisob-worker.js", { type: "module" });

Endi worker ichida import ishlaydi va hisob funksiyasini alohida modulga chiqarish mumkin — uni sahifa ham, test ham, worker ham ishlatadi. Module worker'lar 2023-yil iyunidan beri hamma asosiy brauzerlarda bor (web-features: Baseline 2023). Eski usul — oddiy worker'da importScripts("fayl.js"); uni eski kodda uchratasiz.

7.2 So'rov — javob: callWorker

Bitta worker ko'p xil ish qilsa, javoblar aralashib ketadi: "bu javob qaysi so'rovga?" Yechim — har so'rovga raqam (id) berish va javobni shu raqam bo'yicha Promise'ga ulash.

Buni bir marta o'ram (wrapper) funksiyaga yashiramiz. O'ram — noqulay ishni ichiga olib, tashqariga qulay ko'rinish beradigan funksiya. Pochta qutisi kabi: siz xatni tashlaysiz, javob o'z manzilingizga keladi — ichkarida nima bo'lganini bilish shart emas. Bizning o'ram — callWorker. Uchta fayl:

js
// tub-sonlar.js — oddiy modul, DOM'siz
export function countPrimes(limit) {
  let count = 0;
  for (let n = 2; n < limit; n++) {
    let isPrime = true;
    for (let d = 2; d * d <= n; d++) {
      if (n % d === 0) {
        isPrime = false;
        break;
      }
    }
    if (isPrime) count++;
  }
  return count;
}
js
// hisob-worker.js — module worker
import { countPrimes } from "./tub-sonlar.js";

const tasks = { countPrimes };

self.addEventListener("message", (event) => {
  const { id, task, payload } = event.data;
  try {
    if (!Object.hasOwn(tasks, task)) {
      throw new Error(`Noma'lum vazifa: ${task}`);
    }
    self.postMessage({ id, result: tasks[task](payload) });
  } catch (error) {
    self.postMessage({ id, error: error.message });
  }
});
js
// asosiy.js — sahifa (<script type="module" src="asosiy.js">)
const worker = new Worker("hisob-worker.js", { type: "module" });
const pending = new Map(); // id → { resolve, reject }
let nextId = 1;

worker.addEventListener("message", (event) => {
  const { id, result, error } = event.data;
  const request = pending.get(id);
  pending.delete(id);
  if (error) request.reject(new Error(error));
  else request.resolve(result);
});

function callWorker(task, payload) {
  return new Promise((resolve, reject) => {
    const id = nextId++;
    pending.set(id, { resolve, reject });
    worker.postMessage({ id, task, payload });
  });
}

console.log(await callWorker("countPrimes", 2_000_000));

Biz sinadik: konsolda 148933. callWorker endi oddiy asinxron funksiya kabi: await bilan kutiladi, xato try/catch bilan ushlanadi.

  • pending — Map: kutilayotgan so'rovlar. Javob kelganda o'z resolve ini topadi.
  • Worker'da funksiyalar nomi bo'yicha tanlanadi — funksiyaning o'zi yuborilmaydi (structured clone qoidasi).
  • Object.hasOwn tekshiruvi — begona nom kelsa ("toString"), prototipdagi metod chaqirilib ketmasin.
  • Worker'dagi xato Error bo'lib qaytadi va catch ga tushadi.

Bu o'ram har loyihada kerak bo'ladi, shuning uchun tayyor kutubxona bor — Comlink (Google Chrome jamoasi). U worker funksiyalarini sahifada xuddi oddiy asinxron funksiyadek chaqirishga imkon beradi. Biz 4.4.2 versiyasini (npm view comlink version — 2026-10-05) CDN'dan ESM sifatida ulab sinadik:

js
// hisob-worker.js
import * as Comlink from
  "https://cdn.jsdelivr.net/npm/comlink@4.4.2/+esm";
import { countPrimes } from "./tub-sonlar.js";

Comlink.expose({ countPrimes }); // tashqariga ochish
js
// asosiy.js
import * as Comlink from
  "https://cdn.jsdelivr.net/npm/comlink@4.4.2/+esm";

const worker = new Worker("hisob-worker.js", { type: "module" });
const api = Comlink.wrap(worker);
console.log(await api.countPrimes(2_000_000)); // 148933

expose — "bu funksiyalarni sahifaga ber", wrap — "worker'ni obyektga o'xshat". api.countPrimes(...) aslida postMessage yuboradi va javobni Promise bilan qaytaradi — «So'rov — javob» bo'limidagi mexanizm, faqat kutubxona ichida. Haqiqiy loyihada Comlink npm install comlink bilan o'rnatiladi; npm va bundler'lar — 16-qismda, hozir bilish shart emas.

8. Node'da: worker_threads

Node ham bir threadli, va u ham og'ir hisobda "qotadi": server bu paytda boshqa so'rovlarga javob bermaydi. Node'da worker'ning egizagi — node:worker_threads moduli. Bitta fayl o'zini worker sifatida ham ishga tushirishi mumkin:

js
import {
  Worker, isMainThread, parentPort, workerData,
} from "node:worker_threads";

function countPrimes(limit) {
  let count = 0;
  for (let n = 2; n < limit; n++) {
    let isPrime = true;
    for (let d = 2; d * d <= n; d++) {
      if (n % d === 0) {
        isPrime = false;
        break;
      }
    }
    if (isPrime) count++;
  }
  return count;
}

if (isMainThread) {
  // Asosiy thread: shu faylning o'zini worker qilib ishga tushiramiz
  const worker = new Worker(new URL(import.meta.url), {
    workerData: 500_000,
  });
  worker.on("message", (count) => {
    console.log(`Tub sonlar: ${count}`);
  });
  console.log("Asosiy thread bo'sh — kutyapti");
} else {
  // Worker: boshlang'ich ma'lumot workerData da
  parentPort.postMessage(countPrimes(workerData));
}

Konsolda:

text
Asosiy thread bo'sh — kutyapti
Tub sonlar: 41538

Ikkinchi qator birinchisidan keyin — asosiy thread worker'ni ishga tushirib, darhol o'z ishiga qaytdi. Brauzer bilan farqlar:

Brauzer Node
Yaratish new Worker("fayl.js") new Worker(fayl URL'i, { workerData })
Xabar tinglash addEventListener("message", …), event.data on("message", …), ma'lumotning o'zi
Worker ichida self, self.postMessage parentPort.postMessage, isMainThread
Boshlang'ich ma'lumot birinchi xabar bilan workerData

G'oya bir xil: alohida thread, xabarlar, structured clone, transfer. Node'da worker'lar server qismida (24-qism) og'ir ishlar — rasm siqish, hisobot yasash — uchun ishlatiladi. Thread'lar xotirani bo'lishishi ham mumkin — SharedArrayBuffer bilan. Bu keyingi darsning mavzusi.

9. Qachon worker kerak, qachon yo'q

Vazifa Worker kerakmi?
Katta JSON/CSV faylni tahlil qilish Ha
Rasm filtrlari, siqish, shifrlash Ha
Minglab elementda qidiruv va saralash Ha, agar 50 ms dan uzun
fetch bilan so'rov Yo'q — u allaqachon asinxron
DOM'ni yangilash Yo'q — worker DOM'ga tegolmaydi
Kichik hisob (bir necha ms) Yo'q — xarajat foydadan ko'p

fetch haqida alohida: tarmoq so'rovi davomida asosiy thread band emas — brauzer kutadi, thread bo'sh. Worker faqat JavaScript'ning o'zi uzoq ishlaganda yordam beradi. Agar ishni worker'ga chiqarib bo'lmasa (masalan, DOM bilan bog'liq), uni bo'laklarga bo'lib, orasida brauzerga nafas berish mumkin — buni Asosiy thread'ni bo'shatish darsida o'rganamiz.

10. Ko'p uchraydigan xatolar

10.1 Sahifani fayl sifatida ochish

file:///… — SecurityError: … cannot be accessed from origin 'null'. Tuzatish: Live Server yoki boshqa lokal server.

10.2 Worker'da DOM

document is not defined. Tuzatish: worker natijani yuboradi, DOM'ni sahifa yangilaydi.

10.3 Funksiya yoki DOM elementini yuborish

DataCloneError: … could not be cloned. Tuzatish: oddiy ma'lumot va buyruq nomini yuboring; funksiya worker ichida bo'lsin.

10.4 Ko'chirilgan buferni ishlatish

Transferdan keyin bufer bo'sh (byteLength 0), qayta yuborish — An ArrayBuffer is detached. Tuzatish: buferni worker'dan javob sifatida qaytarib oling (u ham transfer bilan qaytishi mumkin) yoki ko'chirishdan oldin nusxa oling.

10.5 Har hisobga yangi worker

Tugma har bosilganda new Worker(...) va hech qachon terminate() yo'q — thread'lar to'planib, xotira o'sadi. Tuzatish: bitta worker'ni qayta ishlating; kerak bo'lmasa terminate().

10.6 Oddiy worker'da import

Cannot use import statement outside a module. Tuzatish: new Worker(fayl, { type: "module" }).

11. Mashqlar

1-mashq (oson): Kunlik tushum

«Bahor» ning bir yillik buyurtma summalari massivda (sinov uchun: Array.from({ length: 1_000_000 }, (_, i) => (i % 50 + 1) * 1000)). Worker shu massivni olib, yig'indisini qaytarsin. Sahifa natijani Jami: … so'm ko'rinishida chiqarsin. Live Server bilan oching.

Ishora: «Birinchi worker» bo'limidagi ikki fayl naqshi; massiv postMessage(prices) bilan nusxalanib boradi.

Yechim
js
// tushum-worker.js
self.addEventListener("message", (event) => {
  const prices = event.data;
  let total = 0;
  for (const price of prices) total += price;
  self.postMessage(total);
});
js
// sahifadagi skript
const worker = new Worker("tushum-worker.js");
const prices = Array.from(
  { length: 1_000_000 },
  (_, i) => (i % 50 + 1) * 1000,
);
worker.addEventListener("message", (event) => {
  console.log(`Jami: ${event.data} so'm`);
});
worker.postMessage(prices);

Konsolda: Jami: 25500000000 so'm (biz Live Server'da sinadik). Har 50 ta buyurtmada summalar 1 000 dan 50 000 gacha takrorlanadi: 50 tasining yig'indisi 1 275 000, million buyurtmada bunday guruh 20 000 ta. Million sonli massivni nusxalash ham vaqt oladi — haqiqiy loyihada narxlarni Float64Array qilib, transfer bilan yuborish tezroq.

2-mashq (o'rta): Progress xabarlari

Worker 2 000 000 gacha tub sonlarni sanab, har 10% da sahifaga progress yuborsin: { type: "progress", percent }, oxirida esa { type: "done", count }. Sahifa <progress max="100"> ni yangilasin va oxirida natijani chiqarsin.

Ishora: tashqi sikl ichida if (n % (limit / 10) === 0). Sahifada event.data.type bo'yicha ajrating.

Yechim
js
// progress-worker.js
self.addEventListener("message", (event) => {
  const limit = event.data;
  const step = limit / 10;
  let count = 0;
  for (let n = 2; n < limit; n++) {
    let isPrime = true;
    for (let d = 2; d * d <= n; d++) {
      if (n % d === 0) {
        isPrime = false;
        break;
      }
    }
    if (isPrime) count++;
    if (n % step === 0) {
      const percent = (n / step) * 10;
      self.postMessage({ type: "progress", percent });
    }
  }
  self.postMessage({ type: "done", count });
});
js
// sahifadagi skript
const bar = document.querySelector("progress");
const worker = new Worker("progress-worker.js");
worker.addEventListener("message", (event) => {
  const message = event.data;
  if (message.type === "progress") {
    bar.value = message.percent;
  } else if (message.type === "done") {
    bar.value = 100;
    console.log(`Tub sonlar: ${message.count}`);
  }
});
worker.postMessage(2_000_000);

Worker bir nechta xabar yuborishi mumkin — bitta so'rovga bitta javob degan qoida yo'q. Xabarlarga type maydoni berish — bunday "protokol"ning eng oddiy ko'rinishi. Progress tez-tez yuborilmasin: har xabar sahifada message hodisasi va DOM yangilanishi. Sinovimizda 9 ta progress (10% … 90%) va bitta done keldi — sikl limit ga yetmagani uchun 100% ni done o'zi qo'yadi.

3-mashq (qiyin): Bekor qilish

callWorker ga bekor qilish imkonini qo'shing: callWorker(task, payload, { signal }). AbortController signali kelganda worker terminate() qilinsin, Promise AbortError bilan rad etilsin, keyingi so'rovlar uchun esa yangi worker yaratilsin.

Ishora: hisob yarmida turgan worker'ni xabar bilan to'xtatib bo'lmaydi — u band, xabarni o'qimaydi. Shuning uchun faqat terminate(). signal.reason — bekor qilish sababi (AbortError turidagi DOMException).

Yechim
js
let worker = createWorker();
const pending = new Map();
let nextId = 1;

function createWorker() {
  const w = new Worker("hisob-worker.js", { type: "module" });
  w.addEventListener("message", (event) => {
    const { id, result, error } = event.data;
    const request = pending.get(id);
    if (!request) return; // bekor qilingan so'rov
    pending.delete(id);
    if (error) request.reject(new Error(error));
    else request.resolve(result);
  });
  return w;
}

function callWorker(task, payload, { signal } = {}) {
  return new Promise((resolve, reject) => {
    signal?.throwIfAborted();
    const id = nextId++;
    pending.set(id, { resolve, reject });
    worker.postMessage({ id, task, payload });

    signal?.addEventListener("abort", () => {
      worker.terminate();          // band worker'ni o'ldirish
      worker = createWorker();     // keyingi so'rovlar uchun yangisi
      for (const request of pending.values()) {
        request.reject(signal.reason);
      }
      pending.clear();
    }, { once: true });
  });
}

const controller = new AbortController();
document.getElementById("cancel")
  .addEventListener("click", () => controller.abort());

try {
  console.log(await callWorker("countPrimes", 50_000_000,
    { signal: controller.signal }));
} catch (error) {
  console.log(error.name); // AbortError — «Bekor qilish» bosilsa
}

terminate() worker'dagi hamma ishni to'xtatadi — shuning uchun kutayotgan barcha so'rovlar rad etiladi, faqat bittasi emas. message tinglovchisidagi if (!request) return — eski worker o'lishidan oldin yuborib ulgurgan javobni e'tiborsiz qoldiradi. Biz Live Server'da sinadik: «Bekor qilish» bosilganda konsolda AbortError, keyingi callWorker yangi worker'da odatdagidek ishladi.

4-mashq: Amaliy tajriba — 200 000 vazifada qidiruv

Kanon kodga tegmasdan tajriba qiling: kurs/mashqlar/13/19-qidiruv/. Worker 200 000 ta soxta vazifa yaratsin ({ id, matn: "Vazifa 1 — osh", bajarildi }, matnlarga navbat bilan osh, non, hisobot qo'shilsin) va qidiruv so'rovlariga javob bersin: { type: "search", query } → mos kelganlar soni va birinchi 20 tasi. Sahifada qidiruv maydoni (har harfda xabar) va natijalar ro'yxati. Live Server bilan oching.

Ishora: ma'lumot worker'da turadi — har qidiruvda 200 000 obyekt yuborilmaydi, faqat so'rov va 20 ta natija. Harflar tez yozilsa, eski javob yangisidan keyin kelmasligi uchun so'rovga raqam bering (Debounce va throttle darsidagi g'oya bilan birga).

Yechim
js
// qidiruv-worker.js
const words = ["osh", "non", "hisobot"];
const tasks = Array.from({ length: 200_000 }, (_, i) => ({
  id: i + 1,
  matn: `Vazifa ${i + 1} — ${words[i % words.length]}`,
  bajarildi: i % 4 === 0,
}));

self.addEventListener("message", (event) => {
  const { id, query } = event.data;
  const q = query.trim().toLowerCase();
  const found = tasks.filter(
    (v) => v.matn.toLowerCase().includes(q),
  );
  const first = found.slice(0, 20);
  self.postMessage({ id, total: found.length, first });
});
js
// sahifadagi skript
const worker = new Worker("qidiruv-worker.js");
const input = document.getElementById("qidiruv");
const list = document.getElementById("natijalar");
let lastId = 0;

input.addEventListener("input", () => {
  lastId++;
  worker.postMessage({ id: lastId, query: input.value });
});

worker.addEventListener("message", (event) => {
  const { id, total, first } = event.data;
  if (id !== lastId) return; // eskirgan javob
  list.replaceChildren(...first.map((v) => {
    const li = document.createElement("li");
    li.textContent = v.matn;
    return li;
  }));
  console.log(`Topildi: ${total} ta`);
});

Qidiruvga hisobot deb yozsangiz — Topildi: 66666 ta (har uchinchi vazifa; biz Live Server'da sinadik). Barcha 200 000 obyekt bir marta, worker ichida yaratiladi va hech qachon thread'lar orasida yurmaydi — faqat qisqa so'rov va 20 ta natija. id !== lastId — kech kelgan eski javobni tashlab yuboradi. Natijalar textContent bilan yoziladi — vazifa matni HTML sifatida talqin qilinmaydi.

bash
git add 13/19-worker 13/19-qidiruv
git commit -m "13/19: tub sonlar va vazifalar qidiruvi worker'da"

12. Real ishda

  • Katta ma'lumotli ilovalar. Excel'ga o'xshash jadvallar (Google Sheets), PDF ko'ruvchilar (PDF.js), xaritalar — fayllarni worker'da tahlil qiladi, sahifa esa silliq qoladi.
  • Rasm va video ishlovi. Brauzerdagi rasm muharrirlari filtrlarni worker'da OffscreenCanvas bilan bajaradi; Squoosh kabi siqish vositalari ham shunday.
  • Qidiruv va matn tahlili. Hujjat saytlaridagi tezkor qidiruv indeksi, kod muharrirlaridagi sintaksis tekshiruvi (VS Code'ning brauzer versiyasi) worker'larda ishlaydi.
  • Vositalar. Comlink — worker bilan aloqani soddalashtiradi; Vite kabi bundler'lar new Worker(new URL("./w.js", import.meta.url)) yozuvini tushunadi va worker faylini o'zi yig'adi (16-qism).
  • Intervyu. "Web Worker nima va qachon kerak?", "Structured clone va transfer farqi?", "Worker'da DOM nega yo'q?" — frontend intervyularida tez-tez so'raladi.

Xulosa

  • Sahifa JavaScript'i bitta asosiy thread'da: og'ir hisob bosish va chizishni to'xtatadi. Worker — alohida thread.
  • new Worker("fayl.js") → postMessage / message (event.data); xato — error, to'xtatish — terminate().
  • Worker'da DOM, window va localStorage yo'q; fetch, indexedDB, OffscreenCanvas bor. Sahifa server orqali (Live Server) ochilishi kerak.
  • Xabarlar structured clone bilan nusxalanadi: klass metodlari yo'qoladi, funksiya va DOM — DataCloneError. ArrayBuffer transfer bilan nusxasiz ko'chadi.
  • { type: "module" } — worker'da import; id li Promise o'rami yoki Comlink — worker'ni oddiy asinxron funksiyadek chaqirish.
  • Node'da xuddi shu g'oya — node:worker_threads (isMainThread, parentPort, workerData).

Keyingi dars: SharedArrayBuffer va Atomics — worker'lar xotirani nusxalamasdan bo'lishadi: umumiy bufer, poyga holati (race condition) va Atomics bilan sinxronlash.

Manbalar

  • HTML Living Standard: "Web workers", "Structured serialize" — html.spec.whatwg.org
  • MDN: "Using Web Workers", "Transferable objects", structuredClone() — developer.mozilla.org
  • Node.js 24 hujjatlari: worker_threads — nodejs.org
  • Comlink (GoogleChromeLabs) — github.com/GoogleChromeLabs/comlink
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
JavaScript Web Workers: og'ir hisobni alohida thread'da bajarish — IlmHamroh