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

Asosiy thread'ni bo'shatish: uzun task'lar, scheduler.yield va INP

Qisqacha: Brauzerda JavaScript, bosishga javob va ekranni chizish — hammasi bitta asosiy thread'da navbat bilan ishlaydi. 50 ms dan uzun task (uzun task) shu vaqt davomida sahifani "qotiradi" va INP'ni buzadi. Yechim — ishni bo'laklarga bo'lib, orasida brauzerga navbat berish: await scheduler.yield() (Chrome, Edge, Firefox) yoki zaxira sifatida setTimeout. await null bunga yaramaydi — u faqat microtask.

Bu darsda

  • Uzun task nima ekanini va u INP'ni qanday buzishini o'lchab ko'rsata olasiz.
  • Uzun ishni bo'laklarga bo'lib, scheduler.yield() yoki setTimeout bilan asosiy thread'ni bo'shata olasiz.
  • "Avval javob, keyin og'ir ish" naqshi bilan bosishga darhol javob bera olasiz.
  • scheduler.postTask ustuvorliklari va requestIdleCallback qachon kerakligini bilasiz.
  • Qachon bo'laklash yetmasligini va Web Worker kerakligini ajrata olasiz.

Oldin bilishingiz kerak: Performance API va Core Web Vitals, Event loop, Microtask va macrotask chuqur, Web Workers.

1. Nega bu kerak?

Performance API va Core Web Vitals darsida «Sekin tugma» ni bosdik: 300 ms tinglovchi — 304 ms INP. Endi uni tuzatamiz.

«Bahor» admin panelida Jasur aka «Kunlik hisobot» tugmasini bosadi. Kod 300 ta buyurtmani ko'rib chiqib, tushum va eng ko'p sotilgan taomni hisoblaydi. Kompyuterda bu taxminan 0,6 soniya. Shu vaqt ichida sahifa o'lik: tugma bosilgani ko'rinmaydi, "Hisoblanmoqda…" yozuvi chiqmaydi, aylantirib bo'lmaydi. Jasur aka yana bosadi — ikkinchi hisobot ham navbatga tushadi.

Oshxonani tasavvur qiling. Bitta oshpaz bor, u hamma ishni qiladi: buyurtma qabul qiladi, taom tayyorlaydi, likopchaga suzadi. Kimdir 300 ta somsa buyurtma qildi. Oshpaz hammasini to'xtamasdan yopsa, shu vaqt ichida boshqa mijozlar bir stakan choy ham ololmaydi. Aqlli oshpaz 20 tadan yopadi va har gal "choy kutayotgan bormi?" deb qaraydi. Somsa baribir tayyor bo'ladi, lekin hech kim uzoq kutmaydi.

Brauzerdagi oshpaz — asosiy thread (main thread). Bu darsda ishni "20 tadan" qilishni o'rganamiz.

2. Uzun task: sahifa nega qotadi

2.1 Bitta thread, bitta navbat

Event loop darsini eslang: asosiy thread bir vaqtda faqat bitta task'ni bajaradi. Task tugamaguncha boshqa hech narsa bo'lmaydi:

  • bosish, yozish, aylantirish hodisalari — navbatda kutadi;
  • ekran chizilmaydi — oxirgi kadr "muzlab" turadi;
  • taymerlar, fetch javoblari — ular ham kutadi.

Nega shunday qilingan? JavaScript DOM'ni o'zgartiradi, chizish esa DOM'ni o'qiydi. Ikkalasi bir vaqtda ishlasa, ekranda yarim o'zgargan sahifa chiqardi.

2.2 50 ms chegarasi

Uzun task (long task) — 50 ms dan ko'p davom etgan task. Nega aynan 50? Odam 100 ms ichidagi javobni "darhol" deb sezadi. Agar task 50 ms dan oshmasa, eng yomon holatda ham bosish 50 ms kutadi va yana 50 ms ichida javob tayyor bo'ladi. 50 + 50 = 100.

Bu chegara Google'ning RAIL modelidan kelgan. RAIL — foydalanuvchi tezlikni qanday sezishi haqidagi to'rt qoida: Response (javob), Animation (animatsiya), Idle (bo'sh vaqt), Load (yuklanish). Hozir bizga uning birinchisi kerak: javob 100 ms ichida.

Brauzer har bir uzun task uchun longtask turidagi yozuv qoldiradi. Uni ko'ramiz. Quyidagi tugma 300 ta "buyurtmani" ketma-ket hisoblaydi — har biri 2 ms:

html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  button { font: inherit; padding: 0.4rem 0.8rem; }
</style>
<button id="report" type="button">Kunlik hisobot</button>
<p id="status">Tayyor</p>
<script>
  function processOrder() {
    const end = performance.now() + 2; // og'ir hisobni taqlid qiladi
    while (performance.now() < end) {}
  }

  new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      console.log(`Uzun task: ${Math.round(entry.duration)} ms`);
    }
  }).observe({ type: "longtask" });

  document.getElementById("report").addEventListener("click", () => {
    const start = performance.now();
    setTimeout(() => {
      const ms = performance.now() - start;
      const late = Math.round(ms / 100) * 100; // yaxlitlash
      console.log(`50 ms li taymer ${late} ms da keldi`);
    }, 50);

    for (let i = 0; i < 300; i++) processOrder();
    document.getElementById("status").textContent = "300 ta tayyor";
  });
</script>

Bosing. Biz Chrome 154 da bosib sinadik (Intel Core i5-12500H noutbuk):

text
Uzun task: 602 ms
50 ms li taymer 600 ms da keldi

Bitta task 602 ms davom etdi. 50 ms dan keyin ishlashi kerak bo'lgan taymer esa 600 ms da keldi — u navbatda turdi. Bosish ham, chizish ham xuddi shunday kutdi.

Maslahat: Chrome'da longtask dan tashqari yangiroq long-animation-frame (LoAF) yozuvi ham bor (Chrome 123+). U 50 ms dan uzun kadrlarni yozadi va qaysi skript aybdor ekanini ko'rsatadi. Ikkalasi ham hozircha faqat Chromium'da (web-features, 2026-oktabr).

Tekshirib ko'ring: Sahifa yuklanayotganda uchta task ishladi: 30 ms, 120 ms va 45 ms. Ulardan qaysilari uzun task?

Javob

Faqat 120 ms lik. Chegara — 50 ms. 30 va 45 ms lik task'lar qisqa: ular orasida brauzer bosishga javob berishi va kadr chizishi mumkin. Lighthouse'dagi TBT (Total Blocking Time) aynan uzun task'larning 50 ms dan oshgan qismini yig'adi: bu yerda 120 − 50 = 70 ms.

3. Ishni bo'laklash

3.1 G'oya: brauzerga navbat berish

Ish o'zi kamaymaydi — 300 ta buyurtma baribir 600 ms oladi. Biz faqat uni bo'laklarga bo'lamiz. Har bo'lakdan keyin task'ni tugatib, davomini yangi task qilib navbatga qo'yamiz. Ikki task orasida brauzer kutib turgan bosishni bajaradi va kadr chizadi.

Bu asosiy thread'ni bo'shatish (yielding to the main thread) deyiladi. Kodda bu bitta qator: await yieldToMain(). Qolgani — yieldToMain ichida nima borligi.

3.2 await null nega ishlamaydi

Birinchi xayol — async funksiya va await. Lekin await null yoki await Promise.resolve() faqat microtask beradi. Microtask va macrotask chuqur darsini eslang: microtask navbati to'liq bo'shatilmaguncha event loop keyingi task'ga ham, chizishga ham o'tmaydi:

js
setTimeout(() => console.log("taymer: navbatdagi task"), 0);

for (let i = 1; i <= 3; i++) {
  console.log(`bo'lak ${i}`);
  await null; // faqat microtask
}
console.log("sikl tugadi");

Konsolda:

text
bo'lak 1
bo'lak 2
bo'lak 3
sikl tugadi
taymer: navbatdagi task

Taymer — navbatda kutgan "mehmon". U faqat butun sikl tugagandan keyin keldi. await bor, lekin brauzer uchun bu hamon bitta uzun task.

3.3 setTimeout bilan: haqiqiy task

Haqiqiy task beradigan eng eski usul — setTimeout(..., 0) ni Promise'ga o'rash:

js
function yieldToMain() {
  return new Promise((resolve) => setTimeout(resolve, 0));
}

setTimeout(() => console.log("taymer: navbatdagi task"), 0);

for (let i = 1; i <= 3; i++) {
  console.log(`bo'lak ${i}`);
  await yieldToMain(); // haqiqiy task
}
console.log("sikl tugadi");

Konsolda:

text
bo'lak 1
taymer: navbatdagi task
bo'lak 2
bo'lak 3
sikl tugadi

Endi taymer birinchi bo'lakdan keyinoq ishladi. Siklning davomi navbatning oxiriga turdi va kutayotgan task o'tib ketdi. Brauzerda shu oraliqda bosish ham, chizish ham bo'lishi mumkin. Qadamma-qadam ko'ring:

Nimaga qarang: uchinchi qadamda work to'xtab, stek bo'shaydi — bosish shu "tirqish"dan o'tadi.

setTimeout usulining ikki kamchiligi bor:

  1. Navbat oxiri. Sahifada boshqa taymerlar ko'p bo'lsa (reklama, analitika), ishingiz ularning hammasini kutadi.
  2. 4 ms qisish. Taymerlar darsidagi clamping: ichma-ich taymerlar 5-qavatdan keyin kamida 4 ms kutadi. Biz Chrome 154 da o'lchadik: 100 marta setTimeout bilan bo'shatish taxminan 440 ms oldi — deyarli hammasi bo'sh kutish.

3.4 scheduler.yield(): zamonaviy usul

Brauzerlar buning uchun maxsus API qo'shdi — scheduler.yield(). U ham Promise qaytaradi va task'ni tugatadi, lekin ikki farqi bor:

  • 4 ms qisish yo'q. O'sha 100 marta bo'shatish Chrome 154 da 1 ms dan kam oldi.
  • Davomi navbatning oxiriga emas, oldiga turadi. Brauzer bosishga javob beradi va kadr chizadi, lekin keyin boshqa oddiy task'lardan oldin sizning ishingiz davom etadi.

Ikkinchi farqni ko'z bilan ko'ramiz:

html
<script type="module">
  const viaTimeout = () =>
    new Promise((resolve) => setTimeout(resolve, 0));
  const viaYield = () => scheduler.yield();

  async function run(name, yieldFn) {
    const log = [];
    setTimeout(() => log.push("taymer"), 0);
    for (let i = 1; i <= 3; i++) {
      log.push(`bo'lak ${i}`);
      await yieldFn();
    }
    log.push("tugadi");
    await new Promise((resolve) => setTimeout(resolve, 50));
    console.log(`${name}: ${log.join(" → ")}`);
  }

  await run("setTimeout", viaTimeout);
  await run("scheduler.yield", viaYield);
</script>

Konsolda:

text
setTimeout: bo'lak 1 → taymer → bo'lak 2 → bo'lak 3 → tugadi
scheduler.yield: bo'lak 1 → bo'lak 2 → bo'lak 3 → tugadi → taymer

scheduler.yield bilan taymer oxirida ishladi. Bu xato emas, ataylab qilingan: "men hali ishimni tugatmadim, faqat mehmonga javob berish uchun to'xtadim". Bosish va chizish baribir oraliqda bo'ladi — ularning ustuvorligi yuqori. Oxirgi qatordagi setTimeout(resolve, 50) faqat kutish: taymer ishlab ulgurishi uchun.

Qayerda bor (web-features, 2026-oktabr):

Brauzer scheduler.yield()
Chrome, Edge 129-versiyadan
Firefox 142-versiyadan
Safari yo'q

Baseline emas — Safari'da scheduler umuman yo'q. Shuning uchun amalda doim zaxira bilan yoziladi:

js
function yieldToMain() {
  if (globalThis.scheduler?.yield) {
    return scheduler.yield();
  }
  // Safari va eski brauzerlar: oddiy task bilan
  return new Promise((resolve) => setTimeout(resolve, 0));
}

globalThis.scheduler?.yield — optional chaining: scheduler bo'lmasa xato bermaydi, undefined qaytaradi. Bu funksiyani bundan keyin har joyda ishlatamiz.

Tekshirib ko'ring: Sardor "brauzerga navbat beraman" deb siklga await Promise.resolve() qo'ydi. Sahifa baribir qotyapti. Nega va qanday tuzatiladi?

Javob

Promise.resolve() darhol bajarilgan Promise — await uning davomini microtask qilib qo'yadi. Microtask'lar task'lar orasida emas, bitta task ichida bajariladi. Brauzer chizishga ham, bosishga ham o'tmaydi. Tuzatish: await yieldToMain() — ichida scheduler.yield() yoki setTimeout bor, ikkalasi ham haqiqiy task beradi.

4. Bo'laklash amalda: qachon to'xtash kerak?

4.1 Har elementda emas, vaqt bo'yicha

Har bir buyurtmadan keyin bo'shatsak, keraksiz ko'p to'xtash bo'ladi. Hech qachon bo'shatmasak — uzun task. O'rtasi: vaqt byudjeti. Bo'lak boshlangan vaqtni eslab qolamiz va 50 ms ga yaqinlashganda to'xtaymiz. Biz 40 ms oldik — task 50 ms chegarasidan pastda qolsin.

Endi uch tugmali hisobot. Pastdagi kuzatuvchi har bosishning to'liq vaqtini (INP'ga kiradigan son) yozadi:

html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  button { font: inherit; padding: 0.4rem 0.8rem;
    margin: 0 0.25rem 0.5rem 0; }
</style>
<button id="block" type="button">Bloklab</button>
<button id="chunk" type="button">Bo'laklab</button>
<p id="status">Tayyor</p>
<script>
  const statusText = document.getElementById("status");

  function processOrder() {
    const end = performance.now() + 2;
    while (performance.now() < end) {}
  }

  function yieldToMain() {
    if (globalThis.scheduler?.yield) return scheduler.yield();
    return new Promise((resolve) => setTimeout(resolve, 0));
  }

  document.getElementById("block").addEventListener("click", () => {
    statusText.textContent = "Hisoblanmoqda…";
    for (let i = 0; i < 300; i++) processOrder();
    statusText.textContent = "300 ta tayyor";
  });

  document.getElementById("chunk").addEventListener("click",
    async () => {
      statusText.textContent = "Hisoblanmoqda…";
      let chunkStart = performance.now();
      for (let i = 0; i < 300; i++) {
        processOrder();
        if (performance.now() - chunkStart > 40) {
          statusText.textContent = `${i + 1} / 300`;
          await yieldToMain();
          chunkStart = performance.now();
        }
      }
      statusText.textContent = "300 ta tayyor";
    });

  new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      if (entry.name !== "click") continue;
      const id = entry.target?.id;
      console.log(`${id}: ${Math.round(entry.duration)} ms`);
    }
  }).observe({ type: "event", durationThreshold: 16 });
</script>

Ikkala tugmani bosing. Biz Chrome 154 da 8 marta bosib ko'rdik. Bitta ishga tushirish:

text
block: 608 ms
chunk: 48 ms

Har safar block — 608 ms, chunk — 48–56 ms chiqdi. Ish bir xil — 300 ta buyurtma, taxminan 600 ms. Lekin «Bo'laklab» ning INP'ga kiradigan vaqti 10 baravardan ko'p kichik: brauzer birinchi bo'lakdan keyinoq kadr chizdi. «Bloklab» da "Hisoblanmoqda…" yozuvi umuman ko'rinmaydi — brauzer uni chizishga ulgurmaydi. «Bo'laklab» da esa raqam 300 gacha sanab boradi.

Bitta bosishning vaqti (INP'ga kiradigan son)
  • Bloklabbitta 600 ms li task608 ms
  • Bo'laklab40 ms li bo'laklar48 ms
  • INP chegarasiyaxshi ≤ 200 ms200 ms

Manba: O'lchov: Chrome 154 headless, Intel i5-12500H, event timing, 8 marta: Bloklab har safar 608, Bo'laklab 48–56 (eng ko'p uchragani 48), 2026-10-05; chegara — web.dev

4.2 "Avval javob, keyin og'ir ish"

Ba'zan ishni bo'laklash qiyin: masalan, bitta kutubxona funksiyasi 300 ms ishlaydi va uni ichidan bo'la olmaysiz. Shunda ham bitta qoida yordam beradi: ko'rinadigan javobni birinchi bering, keyin bo'shating, keyin og'ir ishni qiling:

js
button.addEventListener("click", async () => {
  statusText.textContent = "Hisoblanmoqda…"; // 1. darhol javob
  await yieldToMain(); // 2. brauzer shu holatni chizsin
  const report = buildReport(orders); // 3. og'ir ish
  statusText.textContent = report.summary;
});

INP — bosishdan keyingi kadrgacha bo'lgan vaqt. Keyingi kadrda "Hisoblanmoqda…" chizilsa, INP tugadi. Og'ir ish undan keyingi task'da — u INP'ga kirmaydi. Biz shu naqshni 600 ms li ish bilan sinadik: 5 ta sinovning 4 tasida bosish vaqti 16 ms dan kam chiqdi (kuzatuvchi uni yozmadi), bittasida — 16 ms. Bloklangan variantda esa 608 ms edi.

Lekin bu "sehr" emas. Og'ir ish baribir 600 ms uzun task bo'ladi. Shu vaqtda mehmon yana bossa, o'sha ikkinchi bosish kutadi. Eng yaxshi natija — ikkala usul birga: avval javob, keyin bo'laklab ish.

4.3 Bo'laklar orasida dunyo o'zgaradi

Bo'shatgan paytingizda boshqa kod ishlaydi: mehmon filtrni o'zgartirdi, server yangi buyurtma yubordi, foydalanuvchi sahifadan chiqib ketdi. Bo'laklangan kod bunga tayyor bo'lishi kerak:

  • Ish boshlanganda ma'lumotning nusxasini oling — massiv bo'shatish paytida o'zgarsa, sikl adashmaydi.
  • Ikkinchi marta bosilsa, eskisini to'xtating. Buning uchun AbortController ning signal idan foydalanish qulay: har bo'shatishdan keyin signal.aborted ni tekshirasiz. Buni «Bekor qilinadigan processInChunks» mashqida qilasiz.

Tekshirib ko'ring: Bo'laklangan hisobot ketayotganda Jasur aka «Kunlik hisobot» ni yana bosdi. Himoya yo'q. Nima bo'ladi?

Javob

Ikkita sikl parallel ishlaydi: bo'laklari navbatma-navbat almashadi, ikkalasi ham status ga yozadi. Raqamlar sakraydi, ish ikki baravar ko'payadi va oxirida qaysi natija qolishi tasodifga bog'liq. Bu race condition — bo'laklash bilan birga keladigan yangi xavf. Tuzatish: yangi bosishda eskisini bekor qilish yoki tugma ish tugaguncha disabled.

5. Ustuvorlik bilan rejalash

5.1 scheduler.postTask: ishga daraja berish

Hamma ish bir xil muhim emas. Tugma javobi — shoshilinch, ro'yxatni chizish — oddiy, analitikani yuborish — kutsa ham bo'ladi. scheduler.postTask(fn, { priority }) task'ni shu darajalar bilan navbatga qo'yadi:

Ustuvorlik Qachon
"user-blocking" Foydalanuvchi kutayotgan javob
"user-visible" Sukut bo'yicha: ko'rinadigan, lekin shoshilinch emas
"background" Fon ishi: analitika, oldindan yuklash
html
<script>
  setTimeout(() => console.log("setTimeout"), 0);
  scheduler.postTask(() => console.log("background: analitika"),
    { priority: "background" });
  scheduler.postTask(() => console.log("user-visible: ro'yxat"));
  scheduler.postTask(() => console.log("user-blocking: tugma javobi"),
    { priority: "user-blocking" });
  requestIdleCallback(() =>
    console.log("idle: eski keshni tozalash"));
  console.log("sinxron kod tugadi");
</script>

Konsolda:

text
sinxron kod tugadi
user-blocking: tugma javobi
setTimeout
user-visible: ro'yxat
background: analitika
idle: eski keshni tozalash

Navbatga qo'yilish tartibi boshqa edi, bajarilishi esa ustuvorlik bo'yicha. user-blocking hatto undan oldin qo'yilgan setTimeout ni ham ortda qoldirdi. Bu natija Chrome 154 da 5 marta bir xil chiqdi. postTask Promise qaytaradi va signal bilan bekor qilinadi. scheduler.yield() bilan bir xil brauzerlarda bor — Safari'da yo'q.

5.2 requestIdleCallback: bo'sh vaqtda

Oxirgi qator — bo'sh vaqt (idle) ishi. requestIdleCallback(fn) callback'ni brauzer hech narsa qilmayotgan paytga qo'yadi. Callback deadline obyektini oladi: deadline.timeRemaining() — shu bo'sh oraliqdan qancha ms qolgani (ko'pi bilan 50).

js
requestIdleCallback((deadline) => {
  while (deadline.timeRemaining() > 0 && queue.length > 0) {
    sendAnalytics(queue.shift());
  }
}, { timeout: 2000 }); // 2 soniyada bo'sh vaqt bo'lmasa ham ishlasin

U muhim bo'lmagan ishlar uchun: analitika, keyingi sahifani oldindan tayyorlash, eski keshni tozalash. Foydalanuvchi kutayotgan narsa uchun emas — sahifa band bo'lsa, idle callback juda kech kelishi mumkin. Brauzer qo'llashi: Chrome, Edge, Firefox; Safari'da yo'q (web-features, 2026-oktabr).

6. Bo'laklash yetmasa: Web Worker

Ba'zi ishlarni bo'laklab bo'lmaydi yoki bo'laklash noqulay: katta rasmni qayta ishlash, uzoq shifrlash, million qatorli JSON. Bunday sof hisobni Web Workers darsidagi alohida thread'ga ko'chirish mumkin. U asosiy thread'ni umuman band qilmaydi.

Vaziyat Yechim
DOM'ga tegadigan ish (chizish, ro'yxat) Bo'laklash + yieldToMain
Bosishdan keyin og'ir hisob Avval javob, keyin bo'shatib hisoblash
Sof hisob, DOM'siz, katta Web Worker
Muhim emas, kutsa bo'ladi postTask (background) yoki requestIdleCallback

Worker'ga ma'lumotni yuborish va natijani qaytarish ham vaqt oladi (nusxalash). Shuning uchun kichik ishlar uchun u ortiqcha.

Tekshirib ko'ring: Uch vaziyat uchun yechim tanlang. (1) Mehmon «Saralash» ni bosdi — 2 000 qatorli jadval qayta chiziladi. (2) Sahifa ochilgach analitikaga "ko'rildi" xabarini yuborish kerak. (3) Yuklangan 20 MB rasmni kichraytirish kerak.

Javob

(1) DOM'ga tegadigan ish — bo'laklash: avval "Saralanmoqda…" javobi, keyin yieldToMain() bilan bo'laklab chizish. Worker DOM'ga tega olmaydi. (2) Muhim emas, kutsa bo'ladi — postTask (background) yoki requestIdleCallback. (3) Sof, katta hisob, DOM'siz — Web Worker: asosiy thread umuman band bo'lmaydi.

7. Ko'p uchraydigan xatolar

7.1 await null yoki await Promise.resolve() bilan "bo'shatish"

Bu faqat microtask — sahifa baribir qotadi. Tuzatish: yieldToMain() («await null nega ishlamaydi» bo'limi).

7.2 Har elementdan keyin setTimeout

300 ta elementning har biridan keyin setTimeout(0) — 4 ms qisish tufayli 1 soniyadan ortiq bo'sh kutish. Tuzatish: vaqt byudjeti (40–50 ms) yoki scheduler.yield().

7.3 Faqat scheduler.yield() — zaxirasiz

Safari'da scheduler degan nom umuman yo'q — ReferenceError va ish bajarilmaydi. Chrome'da scheduler ni o'chirib sinadik: ReferenceError: scheduler is not defined ("scheduler aniqlanmagan"). Safari xabari boshqacha yoziladi (Can't find variable: scheduler), ma'nosi bir xil. Tuzatish: globalThis.scheduler?.yield tekshiruvi va setTimeout zaxirasi.

7.4 Muhim ishni requestIdleCallback ga qo'yish

Sahifa doim band bo'lsa, callback soniyalab kelmaydi. Foydalanuvchi kutayotgan narsa — oddiy task yoki postTask (user-visible).

7.5 Bo'laklar orasidagi o'zgarishni unutish

Bo'shatish paytida ma'lumot o'zgaradi, tugma qayta bosiladi. Tuzatish: nusxa bilan ishlash, eski ishni bekor qilish («Bo'laklar orasida dunyo o'zgaradi» bo'limi).

8. Mashqlar

1-mashq (oson): Navbatni oldindan ayting

Quyidagi kod nimani chiqaradi? Avval o'zingiz yozing, keyin «Ishga tushir» bilan tekshiring:

js
function yieldToMain() {
  return new Promise((resolve) => setTimeout(resolve, 0));
}

async function save() {
  console.log("saqlash: boshlandi");
  await null;
  console.log("saqlash: microtask'dan keyin");
  await yieldToMain();
  console.log("saqlash: task'dan keyin");
}

setTimeout(() => console.log("bosish"), 0);
save();
console.log("skript tugadi");

Ikkinchi qatorda nima chiqadi?

Yechim

Konsolda:

text
saqlash: boshlandi
skript tugadi
saqlash: microtask'dan keyin
bosish
saqlash: task'dan keyin

save() sinxron boshlanadi va birinchi await da to'xtaydi — skript davom etib, "skript tugadi" ni chiqaradi. Keyin microtask: "microtask'dan keyin". bosish taymeri undan keyin, chunki microtask'lar task'lardan oldin bajariladi. yieldToMain esa davomini navbat oxiriga — bosish ning orqasiga qo'ydi.

2-mashq (o'rta): Menyuni bo'laklab chizing

«Bahor» menyusida 3 000 ta taom bor (masalan, filiallar bo'yicha). Ularni <ul> ga bitta siklda qo'shish uzun task beradi. Kodni bo'laklang: har 40 ms da yieldToMain(), orada #status da nechtasi chizilgani chiqsin. Oxirida konsolga "3000 ta taom chizildi".

Ishora: "Bo'laklash amalda" bo'limidagi chunkStart naqshi.

Yechim
html
<p id="status">Tayyorlanmoqda…</p>
<ul id="menu"></ul>
<script type="module">
  const names = ["Osh", "Manti", "Lag'mon", "Somsa", "Shashlik"];
  const items = Array.from({ length: 3000 }, (_, i) =>
    `${names[i % names.length]} №${i + 1}`);
  const list = document.getElementById("menu");
  const statusText = document.getElementById("status");

  function yieldToMain() {
    if (globalThis.scheduler?.yield) return scheduler.yield();
    return new Promise((resolve) => setTimeout(resolve, 0));
  }

  let chunkStart = performance.now();
  for (let i = 0; i < items.length; i++) {
    const li = document.createElement("li");
    li.textContent = items[i];
    list.append(li);
    if (performance.now() - chunkStart > 40) {
      statusText.textContent = `${i + 1} / ${items.length}`;
      await yieldToMain();
      chunkStart = performance.now();
    }
  }
  statusText.textContent = "Tayyor";
  console.log(`${items.length} ta taom chizildi`);
</script>

Konsolda:

text
3000 ta taom chizildi

Har li ni darhol DOM'ga qo'shdik — bo'shatishdan keyin brauzer shu paytgacha qo'shilganlarini chizadi va mehmon ro'yxat "to'lib borayotganini" ko'radi. Tez kompyuterda 3 000 ta element bitta bo'lakka sig'ishi ham mumkin — unda status darhol "Tayyor" bo'ladi. Sekin telefonda esa bo'laklar ko'payadi, sahifa baribir javob beradi.

3-mashq (qiyin): Bekor qilinadigan processInChunks

Qayta ishlatiladigan funksiya yozing: processInChunks(items, fn, { budget = 40, signal }). U har elementga fn(item) ni chaqirsin, byudjet tugaganda bo'shatsin va natijalar massivini qaytarsin (Promise). signal bekor qilinsa — keyingi bo'shatishdan keyin signal.throwIfAborted() xato tashlasin. Sinov: 2 000 ta sonning kvadratini hisoblang; ikkinchi chaqiruvni darhol bekor qiling.

Ishora: AbortController — Bekor qilish: AbortController darsidagi usul; throwIfAborted() — signal bekor qilingan bo'lsa AbortError tashlaydi.

Yechim
html
<script type="module">
  function yieldToMain() {
    if (globalThis.scheduler?.yield) return scheduler.yield();
    return new Promise((resolve) => setTimeout(resolve, 0));
  }

  async function processInChunks(items, fn, options = {}) {
    const { budget = 40, signal } = options;
    const results = [];
    let chunkStart = performance.now();
    for (const item of items) {
      results.push(fn(item));
      if (performance.now() - chunkStart > budget) {
        await yieldToMain();
        signal?.throwIfAborted();
        chunkStart = performance.now();
      }
    }
    return results;
  }

  const numbers = Array.from({ length: 2000 }, (_, i) => i + 1);
  const squares = await processInChunks(numbers, (n) => n * n);
  console.log("Oxirgi kvadrat:", squares.at(-1));

  const controller = new AbortController();
  const job = processInChunks(numbers, (n) => {
    const end = performance.now() + 1; // har biri ~1 ms
    while (performance.now() < end) {}
    return n * n;
  }, { signal: controller.signal });
  controller.abort();
  try {
    await job;
  } catch (error) {
    console.log("Bekor qilindi:", error.name);
  }
</script>

Konsolda:

text
Oxirgi kvadrat: 4000000
Bekor qilindi: AbortError

Birinchi chaqiruv tez — 2 000 ta ko'paytirish bitta bo'lakka sig'adi. Ikkinchisida har element ~1 ms, shuning uchun 40 ms dan keyin bo'shatish keladi. abort() esa darhol chaqirildi — birinchi bo'shatishdan keyin throwIfAborted() AbortError tashladi va qolgan ~1 960 ta element hisoblanmadi. Tekshiruv bo'shatishdan keyin turibdi: bekor qilish faqat boshqa kod ishlagan paytda bo'lishi mumkin.

4-mashq: Amaliy tajriba — 5 000 ta vazifani import qilish

vazifalar dagi import bitta siklda ishlaydi — 100 ta vazifa uchun bu sezilmaydi. Kanonga tegmasdan, mashqlar papkasida sinab ko'ring: kurs/mashqlar/13/29-boshatish/index.html. Sahifada «5 000 ta vazifa yasash» tugmasi va <ul> bo'lsin. Avval bitta siklda chizing, keyin oldingi mashqdagi processInChunks bilan. Har ikkala variantda DevTools konsolida PerformanceObserver bilan event yozuvlarini kuzating: bosish vaqti qancha?

Talab: ish ketayotganda tugma disabled bo'lsin (ikkinchi bosish ikkinchi sikl ochmasin).

Yechim
js
const button = document.getElementById("make");
const list = document.getElementById("list");

button.addEventListener("click", async () => {
  button.disabled = true;
  const tasks = Array.from({ length: 5000 }, (_, i) =>
    `Vazifa ${i + 1}`);
  const items = await processInChunks(tasks, (text) => {
    const li = document.createElement("li");
    li.textContent = text;
    return li;
  });
  list.replaceChildren(...items);
  button.disabled = false;
});

Bu yerda elementlar bo'laklab yaratiladi, DOM'ga esa bir marta replaceChildren bilan qo'shiladi. Element yaratish tez, DOM'ga qo'shish va chizish — qimmatroq. Agar oxirgi qadam o'zi uzun task bo'lsa, uni ham bo'laklarga bo'ling («Menyuni bo'laklab chizing» mashqidagidek). Sizning kompyuteringizda qaysi biri yaxshiroq — o'lchab ko'ring: bu darsning asosiy saboqi aynan shu.

bash
git add 13/29-boshatish/index.html
git commit -m "13/29: 5000 ta vazifani bo'laklab chizish"

9. Real ishda

  • INP tuzatish. Search Console "INP yomon" desa, birinchi qilinadigan ish — uzun tinglovchilarni topish (keyingi dars, Performance paneli) va ularni "avval javob, keyin bo'laklab ish" ga o'tkazish.
  • Freymvorklar. React ichida o'z rejalovchisi (scheduler) bor: u katta yangilanishni mayda bo'laklarga bo'lib, orada brauzerga navbat beradi. React — 17-qismda, hozir bilish shart emas.
  • Analitika va reklama skriptlari. Uchinchi tomon skriptlari ko'pincha eng uzun task'larni beradi. Ularni postTask (background) yoki requestIdleCallback bilan kechiktirish keng tarqalgan.
  • Intervyu. "Uzun task nima?", "await brauzerni bo'shatadimi?", "INP'ni qanday yaxshilaysiz?", "requestIdleCallback va setTimeout farqi?" — ko'p beriladigan savollar.

Xulosa

  • Asosiy thread bitta: JavaScript, bosishga javob va chizish navbat bilan ishlaydi. 50 ms dan uzun task — uzun task.
  • Bo'shatish — ishni bo'laklarga bo'lib, orasida haqiqiy task berish. await null faqat microtask — yaramaydi.
  • scheduler.yield() — 4 ms qisishsiz, davomi navbat oldiga turadi; Safari uchun setTimeout zaxirasi bilan yoziladi.
  • Har elementda emas, vaqt byudjeti (≈ 40 ms) bilan bo'shating; INP uchun — avval ko'rinadigan javob, keyin og'ir ish.
  • postTask — ustuvorlik bilan, requestIdleCallback — muhim bo'lmagan ish uchun; DOM'siz katta hisob — Web Worker.

Keyingi dars: DevTools: Performance, Memory va Network panellari — uzun task'ni, sekin funksiyani va sahifa siljishini profil yozib topish; vazifalar dagi CLS'ni bitta atribut bilan tuzatish.

Manbalar

  • web.dev: "Optimize long tasks", "Optimize Interaction to Next Paint" — web.dev/articles/optimize-long-tasks
  • Chrome for Developers: "Use scheduler.yield() to break up long tasks" — developer.chrome.com
  • WICG Prioritized Task Scheduling: scheduler.postTask, scheduler.yield — wicg.github.io/scheduling-apis
  • MDN: Window.requestIdleCallback(), PerformanceLongTaskTiming — developer.mozilla.org
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Asosiy thread'ni bo'shatish: uzun task'lar, scheduler.yield va INP — IlmHamroh