IlmHamroh
JavaScript Full-stack/18-qism. React chuqur24/42-dars21 daqiqa
Mundarija (32)

Og'ir ishni UI'dan ajratish: debounce, throttle va Web Worker

Qisqacha: Uch xil "ortiqcha ish" — uch xil vosita. Debounce — tez-tez keladigan chaqiruvlardan faqat oxirgisini, to'xtagandan keyin bajaradi (qidiruvdan server so'rovi, qoralamani saqlash). Throttle — oraliqda ko'pi bilan bittasini (scroll, sudrash, tugmani qayta-qayta bosish). Web Worker — og'ir hisobni alohida oqimga chiqaradi, UI esa javob berishda davom etadi. React'dagi asosiy tuzoq: debounce funksiyasini render ichida yasash — har renderda yangi taymer, debounce ishlamaydi; to'g'risi — useMemo/useRef bilan bitta nusxa va unmount'da cancel(). Kanon vazifalar da statistika hisobi Worker'ga ko'chdi: 10 000 vazifada panel ochilishidagi 53–54 ms lik uzun vazifa — yo'qoldi.

Bu darsda

  • Debounce va throttle'ni React komponentida to'g'ri (bitta nusxa, tozalash bilan) yozasiz.
  • "Render ichidagi debounce" tuzog'ini konsolda ko'rasiz.
  • Debounce, useDeferredValue, throttle va Worker'dan qaysi biri qachon kerakligini tanlaysiz.
  • Vite'da Web Worker ulash usullarini (new URL, ?worker, ?worker&url) va Trusted Types talabini bilasiz.
  • vazifalar statistikasini Worker'ga ko'chirib, uzun vazifalar bilan o'lchaysiz.

Oldin bilishingiz kerak: Debounce va throttle, Web Workers, Custom hooklar II: tayyor retseptlar, useDeferredValue, use(): promise va context'ni o'qish.

1. Nega bu kerak?

18-qismning performans bo'limida React renderini tezlashtirdik: transition, memo, virtualizatsiya. Lekin har sekinlik render emas. Performance'ni o'lchash darsidagi Performance panelini eslang — asosiy oqimda React'dan tashqari ish ham bor:

  • Qidiruv maydoni har harfda serverga so'rov yuborsa — "osh" uchun uchta so'rov, ikkitasi keraksiz. Bu render muammosi emas, chaqiruvlar soni muammosi.
  • Scroll hodisasi soniyasiga 60+ marta keladi, har birida og'ir hisob — yana chaqiruvlar soni.
  • vazifalar dagi batafsil statistika 10 000 vazifada bitta funksiya chaqiruvi — lekin u asosiy oqimni 50 ms dan ortiq band qiladi. Uni na transition (komponent ichida to'xtay olmaydi, Concurrent rendering), na memo (birinchi hisobni tejamaydi) qisqartiradi.

Uchala muammoni 13-qismda vanilla JS'da ko'rgansiz (Debounce va throttle, Web Workers). React'da ularning o'z tuzoqlari bor — renderlar orasida nusxani saqlash, tozalash va natijani state'ga qaytarish.

Hayotiy o'xshatish: «Bahor» ofitsianti. Mehmon buyurtmani o'zgartirib turibdi — ofitsiant har so'zda oshxonaga yugurmaydi, mehmon gapirib bo'lishini kutadi (debounce). Telefon tinimsiz jiringlasa — har daqiqada bir marta ko'taradi (throttle). Osh damlash esa — alohida oshpazga, ofitsiant zalda qoladi (Worker).

2. Debounce React'da

2.1 Tuzoq: renderdagi debounce

Debounce va throttle darsidagi debounce funksiyasi — closure ichida bitta taymer. Uni komponentga to'g'ridan-to'g'ri qo'yamiz. Mehmon qidiruvga "o", "os", "osh" yozadi (har harf orasida tekshiruvchi 60 ms kutadi — tez yozuvchi odamdek):

jsx
import { useState } from "react";

function debounce(fn, ms) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), ms);
  };
}

function search(text) {
  console.log("so'rov:", text);
}

function App() {
  const [text, setText] = useState("");
  // ❌ har renderda yangi debounce
  const debouncedSearch = debounce(search, 400);
  function handleChange(e) {
    setText(e.target.value);
    debouncedSearch(e.target.value);
  }
  return (
    <input
      placeholder="Qidiruv"
      value={text}
      onChange={handleChange}
    />
  );
}

Konsolda:

text
so'rov: o
so'rov: os
so'rov: osh

Uchta so'rov — debounce ishlamadi. Sababi: setText App ni qayta chizadi, render esa debounce(search, 400) ni qaytadan chaqiradi — yangi closure, yangi timer o'zgaruvchisi. Keyingi harf yangi debounce'ga tushadi, eskisining taymerini hech kim bekor qilmaydi. Har nusxa o'z yagona chaqiruvini 400 ms dan keyin bajaradi. Bu Pure komponentlar va Closure qoidalarining birgalikdagi oqibati: render — funksiyani qayta chaqirish, ichidagi hamma narsa yangi.

2.2 To'g'risi: bitta nusxa va tozalash

Debounce funksiyasi renderlar orasida bitta bo'lishi kerak. useMemo(…, []) buning eng qisqa yo'li (memo, useMemo, useCallback):

jsx
import { useEffect, useMemo, useState } from "react";

function debounce(fn, ms) {
  let timer;
  const debounced = (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), ms);
  };
  debounced.cancel = () => clearTimeout(timer);
  return debounced;
}

function search(text) {
  console.log("so'rov:", text);
}

function App() {
  const [text, setText] = useState("");
  const debouncedSearch = useMemo(() => debounce(search, 400), []);
  useEffect(() => () => debouncedSearch.cancel(), [debouncedSearch]);
  function handleChange(e) {
    setText(e.target.value);
    debouncedSearch(e.target.value);
  }
  return (
    <input
      placeholder="Qidiruv"
      value={text}
      onChange={handleChange}
    />
  );
}

Konsolda:

text
so'rov: osh

Bitta so'rov — oxirgi matn bilan. Ikki o'zgarish:

  1. useMemo(() => debounce(search, 400), []) — debounce bir marta yasaladi, keyingi renderlarda o'sha funksiya va o'sha timer.
  2. useEffect tozalovchisi cancel() ni chaqiradi (useEffect III: cleanup). Komponent yopilsa (boshqa sahifaga o'tildi), kutayotgan taymer endi yo'q komponent uchun so'rov yubormaydi.

Uchinchi nozik joy — eskirgan closure. Bu yerda search komponentdan tashqarida va hamma ma'lumotni argumentda oladi (debouncedSearch(e.target.value)). Agar debounce qilinadigan funksiya state yoki props'ni o'qisa, u birinchi renderdagi qiymatlarni ko'radi (massiv bo'sh). Yechim — qiymatni argument bilan bering yoki "latest ref" naqshini ishlating (useDeferredValue darsidagi vazifalar qator hodisalari kabi).

Tekshirib ko'ring: useMemo o'rniga useCallback(debounce(search, 400), []) yozilsa, debounce ishlaydimi?

Javob

Ishlaydi (qaytgan funksiya o'sha bo'ladi), lekin isrof bilan: debounce(search, 400) argument sifatida har renderda chaqiriladi va yangi closure yasaladi — useCallback faqat uni tashlab yuborib, birinchisini qaytaradi. useMemo(() => debounce(…), []) esa funksiyani faqat bir marta chaqiradi. Ikkalasi ham to'g'ri natija beradi, useMemo — aniqroq niyat.

2.3 Qiymatni kechiktirish: useDebouncedValue

Ba'zan funksiya emas, qiymat kerak: "matn 400 ms o'zgarmay tursa — shu qiymat bilan ishlayman". Custom hooklar II darsida bunday hook yozgansiz:

jsx
function useDebouncedValue(value, ms) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), ms);
    return () => clearTimeout(timer);
  }, [value, ms]);
  return debounced;
}

Effekt har value o'zgarishida eski taymerni tozalaydi va yangisini qo'yadi — debounce'ning o'zi, faqat React tilida. Narxi — qo'shimcha render (setDebounced). Natija qiymatiga bog'liq so'rovni esa effektda yuborasiz (race condition'ga ehtiyot bo'lib, useEffect III).

Tayyor kutubxona — use-debounce: useDebounce(value, ms) (qiymat) va useDebouncedCallback(fn, ms) (funksiya, cancel, flush, maxWait bilan). U renderlar orasida nusxani o'zi saqlaydi va eng so'nggi fn ni chaqiradi — eskirgan closure muammosi yo'q:

jsx
import { useDebouncedCallback } from "use-debounce";

const debouncedSearch = useDebouncedCallback((text) => {
  fetchDishes(text);
}, 400);

Natija oynasi faqat react va react-dom ni yuklaydi — bu blokni o'z loyihangizda sinang.

2.4 Debounce yoki useDeferredValue?

useDeferredValue darsida bu savolni ko'rgan edik. Qisqasi:

Debounce useDeferredValue
Nima kamayadi chaqiruvlar soni (so'rov, yozish) hech narsa — og'ir render to'xtatib bo'ladigan bo'ladi
Kechikish doim ms (masalan 400 ms) tez kompyuterda deyarli yo'q
Qachon tarmoq, localStorage, analitika xotiradagi ro'yxatni filtrlash, grafik

vazifalar da ikkalasi ham bor va har biri o'z joyida. Qidiruv xotiradagi ro'yxatda — debounce yo'q, useDeferredValue bor. App.tsx dagi izoh buni aytadi: "Har harfda darhol, kechiktirishsiz (debounce yo'q): qidiruv xotiradagi ro'yxatda, tarmoq va localStorage'ga tegmaydi". Qoralama esa localStorage ga yoziladi — debounce bilan.

2.5 vazifalar dagi debounce: kechiktir

taymer.ts (13-qismdan) — debounce'ning to'liqroq ko'rinishi, ikki qo'shimcha metod bilan:

ts
export function kechiktir<A extends unknown[]>(
  ishlovchi: (...argumentlar: A) => void,
  kutish: number,
): Kechiktirilgan<A> {
  let taymer: ReturnType<typeof setTimeout> | undefined;
  // kutayotgan chaqiruv argumentlari
  let kutayotgan: A | null = null;
  …
  return Object.assign(kechiktirilgan, { hoziroq, bekor });
}

App.tsx da u modul darajasida yasalgan — komponentdan tashqarida, demak renderlar uni qayta yasamaydi (useMemo ham kerak emas):

tsx
// Qoralama har harfda emas, yozish to'xtagach bir marta saqlanadi:
// localStorage sinxron — har harfda yozish kiritishni sekinlatadi
const QORALAMA_KUTISH_MS = 400;
const qoralamaniKechiktir = kechiktir(
  qoralamaniSaqla,
  QORALAMA_KUTISH_MS,
);

Va ikki muhim chaqiruv: useOynaHodisasi("pagehide", () => qoralamaniKechiktir.hoziroq()) — tab yopilayotganda kutayotgan qoralama darhol saqlanadi (aks holda oxirgi 400 ms yo'qolardi). Forma yuborilganda esa qoralamaniKechiktir.bekor() — eski qoralama keyin yozilib, tozalangan maydonni qayta to'ldirmasin. Bu flush va cancel — use-debounce'dagi o'sha ikki metod.

3. Throttle

3.1 Oraliqda bittasi

Throttle — chaqiruvlar qanchalik tez kelmasin, ms oraliqda ko'pi bilan bittasi bajariladi. Debounce'dan farqi: debounce kutadi (jim bo'lguncha), throttle esa darhol bajaradi va keyin bir muddat "eshitmaydi". «Saqlash» tugmasini sabrsiz mehmon ketma-ket bosmoqda:

jsx
import { useRef } from "react";

function App() {
  const last = useRef(-Infinity);
  function save() {
    const now = performance.now();
    if (now - last.current < 1000) {
      console.log("o'tkazildi");
      return;
    }
    last.current = now;
    console.log("saqlandi");
  }
  return <button onClick={save}>Saqlash</button>;
}

Konsolda:

text
saqlandi
o'tkazildi
o'tkazildi
saqlandi

Oxirgi bajarilish vaqti — useRef da: u renderlar orasida saqlanadi va o'zgarganda render chaqirmaydi (useRef I). Boshlang'ich -Infinity — birinchi bosish doim o'tadi. Bu leading (oldingi chetdagi) throttle: birinchisi darhol, keyingilar tashlanadi. Trailing variant oraliq oxirida oxirgisini ham bajaradi — scroll uchun muhim (oxirgi holat yo'qolmasin).

Tugma uchun throttle'dan ham yaxshiroq yo'l bor — disabled={isPending} (useTransition darsidagi async transition): so'rov tugaguncha tugma bosilmaydi. Throttle — vaqt bo'yicha, isPending — haqiqiy holat bo'yicha.

3.2 Scroll: requestAnimationFrame va qadam

Scroll, resize, pointermove — brauzer ularni har kadrdan ko'proq yuborishi mumkin. Ekran baribir soniyasiga ~60 marta yangilanadi, shuning uchun eng tabiiy throttle — kadrga bittasi: requestAnimationFrame (requestAnimationFrame). Virtualizatsiya darsida esa uchinchi yo'lni ko'rdik: useSyncExternalStore snapshot'ini 50 px lik qadamga yaxlitlash. Hodisa har safar o'qiladi, lekin qadam o'zgarmasa — render yo'q. Bu ham throttle'ning bir ko'rinishi, faqat vaqt emas, masofa bo'yicha.

flowchart TD
  A{"Muammo nima?"} -->|"Chaqiruv ko'p: so'rov, yozish"| B["Debounce"]
  A -->|"Hodisa oqimi: scroll, sudrash"| C["Throttle yoki rAF"]
  A -->|"Og'ir render, xotiradagi ma'lumot"| D["useDeferredValue"]
  A -->|"Bitta og'ir hisob, 50 ms+"| E["Web Worker"]

Diagrammadagi to'rtta shox — to'rt xil kasallik. Noto'g'ri dori kasalni davolamaydi: debounce og'ir hisobni tezlashtirmaydi, Worker esa so'rovlar sonini kamaytirmaydi.

4. Web Worker React'da

4.1 Muammo: bitta og'ir funksiya

vazifalar dagi batafsil statistika (Suspense va use() darslarida qurilgan) — batafsilHisobla(vazifalar): teglar, ko'p uchragan so'zlar, foizlar. Node 24 da 10 000 vazifa uchun 26 ms (mediana, natijalar/24-keyin.txt). Brauzerda, sahifa bilan birga — kanon o'lchovida panel ochilishida 53–54 ms lik uzun vazifa (Long Task — 50 ms dan uzun vazifa, Performance API).

50 ms — chegara. Shundan uzun vazifa paytida kelgan bosish kutadi va INP'ga qo'shiladi. Bu hisob React'ning render'idan tashqarida (setTimeout da, Promise ichida) — transition uni to'xtata olmaydi.

4.2 Vite'da Worker ulash

Web Workers darsida new Worker("worker.js") ni ko'rgansiz. Vite loyihasida fayl build paytida hash'li nom oladi va TypeScript'dan o'giriladi — shuning uchun manzilni Vite'ning o'zi berishi kerak (Vite IV: maxsus importlar). Uch yo'l:

Yo'l Kod Qachon
new URL new Worker(new URL("./ishchi.ts", import.meta.url), { type: "module" }) standart, bundler'lar tushunadi
?worker import Ishchi from "./ishchi.ts?worker"; new Ishchi() eng qisqa
?worker&url import manzil from "./ishchi.ts?worker&url"; new Worker(manzil) manzilni o'zingiz tekshirishingiz kerak bo'lsa

vazifalar uchinchisini tanladi. Sababi — keyingi bo'limda.

4.3 Trusted Types: Worker manzili ham "xavfli joy"

vazifalar ning CSP'sida require-trusted-types-for 'script' bor (Brauzer xavfsizlik modeli). Bu siyosat ostida skript manzilini qabul qiladigan har joy — script.src, serviceWorker.register va new Worker — oddiy satrni rad etadi. Kanondagi e2e-18 buni tasdiqladi: new Worker(oddiySatr) CSP ostida TypeError; boshqa nomli siyosat va blob: ishchi ham — TypeError.

?worker varianti manzilni Vite'ning ichida, bizning siyosatimizdan o'tkazmasdan ishlatardi. ?worker&url esa faqat satrni beradi — uni o'zimiz siyosatdan o'tkazamiz. xavfsiz-manzil.ts (holat 24, 13-qismdagi siyosat alohida modulga ko'chdi):

ts
import ishchiManzili from "./statistika-ishchi.ts?worker&url";

export const SW_MANZILI = "sw.js";
// Statistika ishchisi (18/#24): Vite build'da hash'li fayl
// (assets/statistika-ishchi-<hash>.js) — manzilini Vite o'zi beradi
export const ISHCHI_MANZILI: string = ishchiManzili;

const RUXSAT: ReadonlySet<string> = new Set([
  SW_MANZILI,
  ISHCHI_MANZILI,
]);

const siyosat = globalThis.trustedTypes?.createPolicy("vazifalar", {
  createScriptURL(url) {
    if (!RUXSAT.has(url)) {
      throw new TypeError(`Ruxsat etilmagan skript manzili: ${url}`);
    }
    return url;
  },
});

/** Skript manzili — siyosatdan o'tgan (Trusted Types bo'lsa) */
export function skriptManzili(url: string): string | TrustedScriptURL {
  return siyosat?.createScriptURL(url) ?? url;
}

Oq ro'yxat — ikki aniq manzil (naqsh emas). Siyosat CSP'da trusted-types vazifalar nomi bilan va bir marta yaratilishi mumkin — shuning uchun SW ham, Worker ham shu bitta moduldan foydalanadi. Bu Frontend xavfsizligi darsida batafsil.

4.4 Ishchi fayli

ts
// statistika-ishchi.ts — Web Worker (18/#24): batafsil
// statistika hisobi alohida oqimda. … Ishchida DOM, React,
// localStorage yo'q — faqat sof hisob (batafsil-statistika.ts,
// node:test bilan sinalgan)
import { batafsilHisobla } from "./batafsil-statistika.ts";
…
ishchi.onmessage = (event) => {
  const { id, vazifalar } = event.data;
  ishchi.postMessage({ id, natija: batafsilHisobla(vazifalar) });
};

Izoh qisqartirildi (…). Ishchi — oddiy modul: batafsilHisobla o'sha sof funksiya, faqat endi boshqa oqimda chaqiriladi. Hisobni avval sof funksiyaga ajratgan edik (React'siz, DOM'siz) — shuning uchun ko'chirish 31 qatorli fayl bilan bo'ldi. Javobda id qaytariladi — nega, keyingi bo'limda.

4.5 Promise o'rami

React tomoni Worker'ni bilmasligi kerak: StatistikaPaneli use(statistikaVadasi(vazifalar)) qiladi va Promise kutadi (use()). Demak, Worker'ning postMessage/onmessage juftini Promise qaytaradigan funksiyaga o'rash kerak. Muammo: bir vaqtda bir nechta so'rov bo'lishi mumkin (mehmon katak bosdi — yangi ro'yxat, eski hisob hali kelmagan). Javob qaysi so'rovniki? Yechim — har so'rovga id.

Natija oynasida Worker yo'q (sandbox CSP'si ruxsat bermaydi, Web Workers darsida aytilgan). Shuning uchun naqshni soxta ishchi bilan ko'ramiz: u postMessage ni oladi va javobni setTimeout bilan, teskari tartibda qaytaradi — haqiqiy Worker'dagi kabi asinxron:

jsx
// Soxta ishchi: haqiqiy Worker o'rnida (natija oynasida Worker yo'q)
const worker = {
  onmessage: null,
  postMessage({ id, items }) {
    const delay = id === 1 ? 200 : 100; // 2-so'rov oldin qaytadi
    setTimeout(() => {
      worker.onmessage({ data: { id, result: items.length } });
    }, delay);
  },
};

let lastId = 0;
const pending = new Map();

worker.onmessage = (event) => {
  const { id, result } = event.data;
  pending.get(id)?.(result);
  pending.delete(id);
};

function calculate(items) {
  return new Promise((resolve) => {
    const id = ++lastId;
    pending.set(id, resolve);
    worker.postMessage({ id, items });
  });
}

function App() {
  async function run() {
    const a = calculate(["osh", "manti", "somsa"]);
    const b = calculate(["choy"]);
    console.log("javoblar:", await a, await b);
  }
  return <button onClick={run}>Ikki hisob</button>;
}

Konsolda:

text
javoblar: 3 1
Vaqt Nima bo'ldi
0 ms ikki so'rov ketdi: id 1 (3 taom) va id 2 (1 taom)
100 ms id 2 javobi keldi → pending dan id 2 ning resolve i → b = 1
200 ms id 1 javobi keldi → a = 3; ikkala await tugadi, konsolga yoziladi

Ikkinchi so'rov birinchi qaytdi (100 ms), lekin a baribir 3, b — 1: har javob o'z id'si bo'yicha o'z Promise'ini topdi. Map — id → resolve. Id bo'lmasa, birinchi kelgan javob birinchi kutayotganga tushardi va statistika boshqa ro'yxatniki bo'lardi.

Kanondagi statistika-vadasi.ts — xuddi shu naqsh, yana uch narsa bilan:

  1. Rad etish — kutilmoqda Map'ida { hal, rad }; ishchi yiqilsa (error hodisasi) — hamma kutayotganlar rad etiladi, ishchi terminate() qilinadi va keyingi so'rov yangisini yaratadi. Panel atrofidagi error boundary xabar ko'rsatadi.
  2. Kesh — bir ro'yxat (massiv havolasi) — bitta Promise (WeakMap, use() dagi qoida o'zgarmadi).
  3. Worker yo'q muhit — Vitest (jsdom) da typeof Worker === "undefined": hisob asosiy oqimda, setTimeout bilan. Testlar o'zgarmadi.

4.6 postMessage nusxalaydi

postMessage({ id, vazifalar }) ro'yxatni ishchiga nusxalaydi — structured clone algoritmi bilan (Sayoz va chuqur nusxalash). Bu bepul emas: kanon Node'da o'lchadi — 10 000 vazifani structuredClone qilish 5.9 ms, hisobning o'zi esa 26 ms. Nusxa asosiy oqimda bo'ladi, lekin 54 ms lik bloklash 6 ms ga aylandi — foyda aniq.

Katta binar ma'lumot (rasm, audio) uchun nusxa o'rniga transfer bor — ArrayBuffer egaligini ishchiga o'tkazish, nusxasiz (Web Workers dagi transferable). Vazifalar ro'yxati — obyektlar, ularni transfer qilib bo'lmaydi.

O'z Promise o'ramini yozish o'rniga — Comlink kutubxonasi (Google Chrome jamoasidan): ishchida expose(obyekt), asosiy oqimda wrap(worker) — va ishchidagi funksiyalar oddiy async funksiyalar kabi chaqiriladi:

js
// ishchi.js
import { expose } from "comlink";
expose({ batafsilHisobla });

// asosiy oqim
import { wrap } from "comlink";
const ishchi = wrap(new Worker(manzil, { type: "module" }));
const natija = await ishchi.batafsilHisobla(vazifalar);

Id'lar, Map, onmessage — hammasi ichkarida. vazifalar uchun bitta funksiya — kanon 30 qatorli o'z o'ramini qoldirdi (yangi runtime bog'liqlik yo'q). Ko'p funksiyali ishchida Comlink kodni ancha soddalashtiradi.

5. Vazifalar qadami: statistika — Web Worker'da (holat 24)

5.1 O'zgarishlar

Yangi branch: perf/statistika-ishchi. Diff: 5 fayl, +135 −23:

Fayl Nima
statistika-ishchi.ts (yangi) ishchi: onmessage → batafsilHisobla → postMessage
xavfsiz-manzil.ts (yangi) bitta Trusted Types siyosati — SW va ishchi manzili
statistika-vadasi.ts setTimeout o'rniga ishchi, id bo'yicha javob, xato bo'lsa — hammasi rad + yangi ishchi
main.tsx SW ro'yxatdan o'tkazish — skriptManzili(SW_MANZILI)
trusted-types.d.ts izoh yangilandi

Ishchi yaratilishi (statistika-vadasi.ts):

ts
  const yangi = new Worker(skriptManzili(ISHCHI_MANZILI) as string, {
    name: "statistika",
    type: import.meta.env.DEV ? "module" : "classic",
  });

Ikki nozik joy. Birinchisi — as string: TypeScript'ning DOM turlarida Worker konstruktori TrustedScriptURL ni bilmaydi va uni kengaytirib bo'lmaydi (u var bilan e'lon qilingan) — izohda yozilgan. Ikkinchisi — type: dev'da Vite ishchini ES modul qilib beradi (?worker_file&type=module, natijalar/24-dev.txt), build'da esa bitta oddiy skript (worker.format: "iife").

5.2 O'lchov

olchov/statistika.mjs — 10 000 vazifa, Long Tasks API (50 ms dan uzun vazifalar), 3 urinish (natijalar/24-oldin.txt → 24-keyin.txt):

holat 23 (asosiy oqim) holat 24 (Worker)
panel ochilishi: uzun vazifalar [53], [54] (va bitta []) [], [], []
katak bosish: uzun vazifalar [62, 58, 66], [52, 50, 62] [57], [52, 57] (va bitta [])
10 000 vazifa: statistika paneli ochilishidagi uzun vazifa (Long Task)
  • holat 23: hisob asosiy oqimda3 urinishdan 2 tasida 53–54 ms54 ms
  • holat 24: hisob Web Worker'da3 urinishda ham uzun vazifa yo'q0 ms

Manba: O'lchandi: vazifalar holat 23 va 24, olchov/statistika.mjs (Long Tasks API), Chrome headless, 10 000 vazifa, 3 urinish, 2026-10-13 (natijalar/24-oldin.txt, 24-keyin.txt)

Panel ochilishida uzun vazifa butunlay yo'qoldi. Katak bosishda 3 tadan 1–2 ga kamaydi — qolgani statistika emas: ro'yxatni localStorage uchun JSON qilish va boshqa sinxron ish (TEXNIK-QARZ 7 da yozilgan). Har ikki holatda birinchi urinishda uzun vazifa umuman yo'q — o'lchovda shovqin bor. Shuning uchun jadvalda xom qiymatlar, mediana emas.

Hajm: ishchi alohida fayl — 1.46 KB (gzip 853 B), faqat panel ochilganda yuklanadi; asosiy bundle deyarli o'zgarmadi (natijalar/tekshiruv-24.txt). SW keshidagi qobiq 13 → 14 fayl (ishchi oflayn ham ishlasin).

5.3 Tekshiruvlar va commit

Testlar: node 169, Vitest 39 (o'zgarmadi — jsdom'da asosiy oqim varianti). Brauzer: 159/159, e2e-18 44/44 (yangi: ishchi CSP ostida ishlaydi, oddiy satr bilan — TypeError). Dev: panel ochiladi, CSP buzilishi 0 (24-dev.txt).

text
perf: statistika hisobi Web Worker'da — Trusted Types siyosati bilan

6. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
debounce bor, so'rovlar baribir har harfda debounce render ichida yasalgan useMemo/useRef, modul darajasi yoki use-debounce
komponent yopilgandan keyin so'rov ketdi taymer tozalanmagan useEffect tozalovchisida cancel()
debounce eski state bilan ishlaydi eskirgan closure qiymatni argumentda bering, latest ref
scroll'da oxirgi holat yo'qoladi faqat leading throttle trailing yoki rAF
TypeError new Worker(...) da Trusted Types siyosati ?worker&url + siyosatdan o'tkazish
javob boshqa so'rovga tushdi postMessage da id yo'q id → resolve Map'i
Worker'dan keyin ham UI qotadi katta nusxa (structuredClone) yoki ish asosiy oqimda qoldi o'lchang; transfer yoki kamroq ma'lumot

7. Mashqlar

Mashqlar kurs/mashqlar/18/24-debounce-worker/ da. Har yechimni «Ishga tushir» bilan sinang.

1-mashq (oson): qaysi vosita?

  • Qidiruv maydoni har harfda fetch yuboradi:
  • Xaritani sudraganda har pointermove da koordinatalar hisoblanadi:
  • 50 000 qatorli CSV faylni tahlil qilish 300 ms oladi:
  • 5 000 taomli ro'yxat xotirada filtrlanadi, maydon qotadi:
Yechim

So'rovlar soni — debounce. Uzluksiz hodisa oqimi — throttle (yoki rAF). Bitta og'ir hisob — Worker (u faylni ham, FileReader natijasini ham oladi). Xotiradagi og'ir render — useDeferredValue: so'rov yo'q, kamaytiradigan chaqiruv yo'q, faqat renderni to'xtatib bo'ladigan qilish kerak.

2-mashq (o'rta): qoralama saqlash

«Bahor» buyurtma izohi localStorage ga saqlanadi (bu yerda — console.log). Hozir har harfda saqlanadi. Shunday qiling: yozish to'xtagandan 400 ms keyin bir marta saqlansin. Tekshiruvchi "a", "ac", "achchiq" yozadi va 600 ms kutadi.

jsx
import { useState } from "react";

function saveDraft(text) {
  console.log("saqlandi:", text);
}

function App() {
  const [note, setNote] = useState("");
  function handleChange(e) {
    setNote(e.target.value);
    saveDraft(e.target.value);
  }
  return (
    <input placeholder="Izoh" value={note} onChange={handleChange} />
  );
}

Konsolda:

text
saqlandi: a
saqlandi: ac
saqlandi: achchiq
Yechim
jsx
import { useEffect, useMemo, useState } from "react";

function debounce(fn, ms) {
  let timer;
  const debounced = (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), ms);
  };
  debounced.cancel = () => clearTimeout(timer);
  return debounced;
}

function saveDraft(text) {
  console.log("saqlandi:", text);
}

function App() {
  const [note, setNote] = useState("");
  const debouncedSave = useMemo(() => debounce(saveDraft, 400), []);
  useEffect(() => () => debouncedSave.cancel(), [debouncedSave]);
  function handleChange(e) {
    setNote(e.target.value);
    debouncedSave(e.target.value);
  }
  return (
    <input placeholder="Izoh" value={note} onChange={handleChange} />
  );
}

Konsolda:

text
saqlandi: achchiq

Maydon har harfda darhol yangilanadi (setNote — debounce'siz), saqlash esa bir marta. Muhim tafsilot: setNote ni debounce qilmang — u holda maydon kechikib, kursor sakraydi (useTransition darsidagi "controlled input shoshilinch" qoidasi). Debounce faqat tashqi ishga.

3-mashq (qiyin): useThrottle hook'i

useThrottledCallback(fn, ms) hook'ini yozing: qaytargan funksiya ms ichida faqat birinchi chaqiruvni bajaradi (leading). Talablar: fn ning eng so'nggi versiyasi chaqirilsin (eskirgan closure bo'lmasin), qaytgan funksiya barqaror bo'lsin. Tekshiruv: «Sevimli» tugmasi 3 marta tez bosiladi, keyin 1 100 ms dan so'ng yana bir marta; har bajarilishda joriy likes soni chiqadi.

jsx
function App() {
  const [likes, setLikes] = useState(0);
  const like = useThrottledCallback(() => {
    console.log("bajarildi, oldin:", likes);
    setLikes(likes + 1);
  }, 1000);
  return <button onClick={like}>Sevimli</button>;
}
Yechim
jsx
import {
  useCallback,
  useLayoutEffect,
  useRef,
  useState,
} from "react";

function useThrottledCallback(fn, ms) {
  const latest = useRef(fn);
  const last = useRef(-Infinity);
  useLayoutEffect(() => {
    latest.current = fn; // eng so'nggi fn — commit'da
  });
  return useCallback(
    (...args) => {
      const now = performance.now();
      if (now - last.current < ms) return;
      last.current = now;
      latest.current(...args);
    },
    [ms],
  );
}

function App() {
  const [likes, setLikes] = useState(0);
  const like = useThrottledCallback(() => {
    console.log("bajarildi, oldin:", likes);
    setLikes(likes + 1);
  }, 1000);
  return <button onClick={like}>Sevimli</button>;
}

Konsolda:

text
bajarildi, oldin: 0
bajarildi, oldin: 1

Ikki bajarilish: birinchi bosish va 1 100 ms dan keyingisi. Ikkinchisi likes ni 1 deb ko'rdi — eskirgan 0 emas. Sir — "latest ref": har commit'dan keyin latest.current yangi fn ga almashadi (useDeferredValue darsidagi vazifalar qator hodisalari naqshi). Qaytgan funksiya esa useCallback(…, [ms]) — barqaror, memo li bolaga bersangiz ham buzmaydi. Ref'ga render paytida emas, layout effektda yozamiz — tashlangan render ref'ni buzmasin (Render va commit).

4-mashq: vazifalar qadami

perf/statistika-ishchi branch'ida «Vazifalar qadami» dagi besh faylni o'zgartiring. npm run build — dist/assets/ da statistika-ishchi-<hash>.js paydo bo'lishi kerak. Brauzerda 10 000 vazifa bilan DevTools → Performance yozuvida «Batafsil statistika» ni oching: asosiy oqimda batafsilHisobla qolmaganini va alohida "Worker" oqimida ishlaganini toping. Konsolda new Worker("x.js") ni sinang — qanday xato? Commit va PR.

Yechim

Performance yozuvida pastda yangi qator — Worker: statistika (bu nom { name: "statistika" } dan). Hisob shu qatorda, asosiy oqimda esa faqat postMessage (nusxa, bir necha ms) va javob kelgach React renderi. Konsoldagi new Worker("x.js") — CSP Trusted Types tufayli TypeError (Chrome xabarida "TrustedScriptURL" so'zi bo'ladi): siyosat faqat oq ro'yxatdagi ikki manzilni o'tkazadi. Commit xabari — «Tekshiruvlar va commit» bo'limida.

8. Real ishda

  • Qidiruv va avtoto'ldirish: debounce (300–500 ms) + bekor qilish (AbortController) — har marketplace va xarita servisida.
  • Muharrirlar: Google Docs, Figma kabi ilovalar avtosaqlashni debounce, kursor/sudrashni throttle (rAF) bilan qiladi.
  • Workerlar: Excel/CSV tahlili, rasm siqish, PDF yasash, matnni qidiruv indeksiga aylantirish — UI'ni qotirmaslik uchun ishchida. Next.js va Vite ishchilarni bir xil new URL(…, import.meta.url) naqshi bilan qo'llaydi.
  • Intervyu savollari: "Debounce va throttle farqi?" (deyarli har frontend intervyusida, ko'pincha "yozib bering"), "React'da debounce'ni qanday to'g'ri yozasiz?", "Qachon Web Worker?", "Worker bilan ma'lumot almashish narxi?".

Xulosa

  • Debounce — chaqiruvlar to'xtagach oxirgisi; throttle — oraliqda bittasi; Worker — og'ir hisob alohida oqimda; useDeferredValue — og'ir render to'xtatiladigan.
  • React'da debounce renderlar orasida bitta nusxa bo'lsin (useMemo, ref, modul darajasi), unmount'da cancel(); eskirgan closure'ga — argument yoki latest ref.
  • vazifalar: qidiruvda debounce yo'q (xotira), qoralamada bor (kechiktir, hoziroq/bekor).
  • Vite'da Worker: new URL, ?worker, ?worker&url; Trusted Types ostida manzil siyosatdan o'tadi. Javob — id bo'yicha Promise'ga; Comlink buni yashiradi.
  • vazifalar holat 24: panel ochilishidagi 53–54 ms uzun vazifa — yo'q; nusxa narxi 5.9 ms, hisob 26 ms (Node).

Keyingi dars: Keng tarqalgan React xatolari katalogi — 18-qism yarmidagi to'xtash: eng ko'p uchraydigan xatolar va ularning belgilari bir joyda.

Manbalar

  • React: useMemo — "Memoizing a function" — react.dev/reference/react/useMemo
  • use-debounce — github.com/xnimorz/use-debounce
  • Vite: "Web Workers" — vite.dev/guide/features#web-workers
  • MDN: Trusted Types API — developer.mozilla.org/docs/Web/API/Trusted_Types_API
  • Comlink — github.com/GoogleChromeLabs/comlink
  • Kanon: sinov/18/QADAMLAR.md (#24), natijalar/24-oldin.txt, 24-keyin.txt, 24-dev.txt, natijalar/tekshiruv-24.txt
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Og'ir ishni UI'dan ajratish: debounce, throttle va Web Worker — IlmHamroh