Mundarija (32)
- Bu darsda
- 1. Nega bu kerak?
- 2. Ko'rinadigan oraliq
- 2.1 Hisob
- 2.2 Bo'sh joy
- 3. React'da virtual ro'yxat
- 3.1 Birinchi variant
- 3.2 Nima uchun key={start + i}
- 3.3 Scroll hodisasini kamaytirish
- 4. Har xil balandlik
- 4.1 Muammo
- 4.2 Prefiks yig'indi va ikkilik qidiruv
- 5. Virtualizatsiyaning narxi
- 5.1 Nima yo'qoladi
- 5.2 Scroll holatini tiklash
- 6. Kutubxonalar
- 7. Vazifalar qadami: useOynaVirtual (holat 23)
- 7.1 Qaror
- 7.2 VazifaRoyxati: ikki xil ro'yxat
- 7.3 VirtualRoyxat
- 7.4 useOynaVirtual: asosiy qismlar
- 7.5 e2e topgan xato
- 7.6 Natija va commit
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): qancha qator?
- 2-mashq (o'rta): aria-setsize va aria-posinset
- 3-mashq (qiyin): indeks bo'yicha o'tish
- 4-mashq: vazifalar qadami
- 10. Real ishda
- Xulosa
- Manbalar
Uzun ro'yxatlar: virtualizatsiya — faqat ko'rinadigan qatorlarni chizish
Qisqacha: 10 000 qatorli ro'yxatni to'liq chizish — 50 000 dan ortiq DOM elementi, yuzlab millisekund va o'nlab megabayt xotira. Ekranda esa bir vaqtda 15 qator ko'rinadi. Virtualizatsiya (windowing) — faqat ko'rinadigan qatorlarni (va bir nechta zaxira) chizish, qolganining o'rnida — bo'sh joy: scroll satri va umumiy balandlik o'zgarmaydi. Hisob oddiy:
scrollTopva qator balandligidan boshlang'ich va oxirgi indeks. Balandlik har xil bo'lsa — o'lchash va prefiks yig'indi.vazifalarda o'z hook'imiz (useOynaVirtual, +0.8 KB): 10 000 vazifa bilan ochilish ~760 → ~170 ms, DOM 50 087 → 154 element, xotira 45 → 11 MB. TanStack Virtual tezlikda teng, lekin +8.3 KB.
Bu darsda
- Ko'rinadigan oraliqni (start, end) hisoblaydigan funksiyani yozasiz.
- React'da oddiy virtual ro'yxat quradigan va DOM'dagi qatorlarni sanaydigan bo'lasiz.
- Har xil balandlikli qatorlar uchun prefiks yig'indi va ikkilik qidiruvni tushunasiz.
- Virtualizatsiyaning narxini bilasiz: Ctrl+F, ekran o'quvchi, scroll holatini tiklash.
- TanStack Virtual, react-window va react-virtuoso dan birini tanlay olasiz — yoki o'zingiz yozasiz.
Oldin bilishingiz kerak: Performance'ni o'lchash, memo, useMemo, useCallback, useSyncExternalStore, ResizeObserver va MutationObserver.
1. Nega bu kerak?
Performance'ni o'lchash darsi ikki raqam bilan tugadi. 10 000 vazifali ro'yxatning birinchi chizilishi (faqat ro'yxat qismi) — 163 ms. Qidiruvda "v" yozilganda hamma qator haqiqatan o'zgaradi — 285 ms. memo bu yerda yordam bermaydi: ish kerakli, shunchaki juda ko'p.
Lekin bu ishning katta qismi hech kimga ko'rinmaydi. Telefon ekranida (412×800 piksel) bir vaqtda taxminan 15 ta vazifa ko'rinadi. Qolgan 9 985 tasi — ekrandan pastda, DOM'da bor, xotirani egallaydi, har qidiruvda qayta chiziladi.
Hayotiy o'xshatish: «Bahor»ning yillik buyurtmalar daftari — 10 000 sahifa. Jasur aka uni stolga butunlay yoyib qo'ymaydi: faqat ochiq turgan ikki betni ko'radi. Varaqlasa — keyingi betlar ochiladi. Virtualizatsiya ham shu: "ochiq betlar" — ekran, qolgani — yopiq kitob, lekin uning qalinligi (scroll uzunligi) to'g'ri.
2. Ko'rinadigan oraliq
2.1 Hisob
Hamma qator bir xil balandlikda (masalan, 40 px) bo'lsa, hisob — bo'lish:
function visibleRange(scrollTop, viewport, rowHeight, count, over) {
const first = Math.floor(scrollTop / rowHeight);
const last = Math.ceil((scrollTop + viewport) / rowHeight);
return {
start: Math.max(0, first - over),
end: Math.min(count, last + over),
};
}
console.log(visibleRange(0, 400, 40, 10000, 3));
console.log(visibleRange(20000, 400, 40, 10000, 3));
console.log(visibleRange(399600, 400, 40, 10000, 3));Konsolda:
{ start: 0, end: 13 }
{ start: 497, end: 513 }
{ start: 9987, end: 10000 }first— tepadagi qator: 20 000 px pastga aylantirilgan → 20 000 / 40 = 500-qator.last— ekran pastidagi qator: (20 000 + 400) / 40 = 510.over— overscan (zaxira): ekrandan tashqarida ham bir nechta qator chiziladi. Tez aylantirganda React yangi qatorlarni chizguncha bo'sh joy ko'rinmasin.Math.max/Math.min— boshida manfiy, oxirida mavjud bo'lmagan indeks bo'lmasin.
10 000 dan 16 qator. Qolgan hamma narsa — shu indekslar bilan ishlash.
2.2 Bo'sh joy
Faqat 16 qatorni chizsak, scroll satri qisqarib qoladi — brauzer ro'yxat 640 px deb o'ylaydi. Shuning uchun konteynerga umumiy balandlik beriladi (10 000 × 40 = 400 000 px), ko'rinadigan qatorlar esa o'z joyiga qo'yiladi. Ikki usul bor:
- Konteyner
position: relative, har qatorposition: absolute; top: indeks × 40px. - Ko'rinadigan qatorlardan oldin va keyin ikkita bo'sh "to'ldiruvchi" element (
height: start × 40pxva(count − end) × 40px).
flowchart TD
A["Yuqori bo'sh joy: 497 × 40 px"] --> B["Qator 497 (zaxira)"]
B --> C["Qator 500–509: ekranda"]
C --> D["Qator 510–512 (zaxira)"]
D --> E["Pastki bo'sh joy: 9 487 × 40 px"]Diagrammaga qarang: DOM'da 5 qism, lekin brauzer uchun ro'yxat to'liq 400 000 px balandlikda. Scroll satri, Page Down, sichqoncha g'ildiragi — hammasi oddiydek ishlaydi. vazifalar ikkinchi usulni tanladi — keyinroq ko'ramiz.
3. React'da virtual ro'yxat
3.1 Birinchi variant
scrollTop — state. Har scroll hodisasida yangilanadi va React faqat oraliqdagi qatorlarni chizadi. Natija oynasida aylantirishni taqlid qilamiz: «O'rtasi» tugmasi konteynerni 200 000 px ga aylantiradi (haqiqiy brauzerda konteynerni sichqoncha bilan ham aylantirib ko'ring):
import { useLayoutEffect, useRef, useState } from "react";
const ROW = 40;
const VIEW = 200;
const ORDERS = Array.from(
{ length: 10000 },
(_, i) => `Buyurtma №${i + 1}`,
);
function VirtualList({ items }) {
const [top, setTop] = useState(0);
const box = useRef(null);
const start = Math.max(0, Math.floor(top / ROW) - 2);
const end = Math.min(
items.length,
Math.ceil((top + VIEW) / ROW) + 2,
);
useLayoutEffect(() => {
const lis = box.current.querySelectorAll("li");
const text = (li) => li.textContent;
console.log(
`DOM'da: ${lis.length} ta <li>,`,
`${text(lis[0])} … ${text(lis[lis.length - 1])}`,
);
});
function jump(y) {
box.current.scrollTop = y;
setTop(y);
}
return (
<div>
<button onClick={() => jump(0)}>Boshi</button>
<button onClick={() => jump(200000)}>O'rtasi</button>
<div
ref={box}
onScroll={(e) => setTop(e.currentTarget.scrollTop)}
style={{ height: VIEW, overflowY: "auto" }}
>
<ul
style={{ height: items.length * ROW, position: "relative" }}
>
{items.slice(start, end).map((text, i) => (
<li
key={start + i}
style={{ position: "absolute", top: (start + i) * ROW }}
>
{text}
</li>
))}
</ul>
</div>
</div>
);
}
function App() {
return <VirtualList items={ORDERS} />;
}DOM'dagi <li> soniga qarang — 10 000 emas. Birinchi qator — sahifa ochilganda, ikkinchisi — «O'rtasi» dan keyin:
Konsolda:
DOM'da: 7 ta <li>, Buyurtma №1 … Buyurtma №7
DOM'da: 9 ta <li>, Buyurtma №4999 … Buyurtma №500710 000 buyurtmadan DOM'da 7 tasi (boshida — 5 ta ko'rinadigan va 2 ta pastki zaxira), o'rtada — 9 tasi (2 + 5 + 2). Hisob: 200 000 / 40 = 5 000 — ekranda №5001 dan №5005 gacha. Lekin ORDERS massivi to'liq xotirada: virtualizatsiya ma'lumotni emas, DOM'ni qisqartiradi.
3.2 Nima uchun key={start + i}
Har qatorning key i — uning ro'yxatdagi haqiqiy indeksi, slice dagi o'rni emas. Aylantirganda №5001 ekranning tepasidan pastroqqa sursa, React o'sha <li> ni saqlaydi va faqat top ni o'zgartiradi; chiqib ketgan qatorlarni olib tashlab, yangilarini qo'shadi (Reconciliation). Haqiqiy ilovada — ma'lumotdagi id (key={order.id}).
3.3 Scroll hodisasini kamaytirish
Har piksel aylantirish — setTop — butun ro'yxatning qayta chizilishi. 60 kadr/soniyada scroll hodisalari juda tez keladi. Ikki odatiy yechim:
- Ko'rinadigan oraliq o'zgarmagan bo'lsa — renderni o'tkazib yuborish (state'ga piksel emas,
startindeksini yozish — bir xil qiymat bailout beradi, Qayta render sabablari). - Scroll'ni qadamlarga bo'lish:
vazifalardagi hook har 50 px dan keyingina yangilanadi (qadam —QADAM = 50). Qadam ichidagi siljishni zaxira qatorlar yopadi.
Ko'pchilik bu yerda throttle ni eslaydi (Og'ir ishni UI'dan ajratish — keyingi dars). Lekin throttle'dan keyin oxirgi holat kechikib keladi — tez to'xtaganda bo'sh joy bir lahza ko'rinadi. Qadam usuli har hodisani darhol ko'radi, faqat keraksiz render qilmaydi.
4. Har xil balandlik
4.1 Muammo
Vazifa matni uzun bo'lsa, qator ikki qatorga o'tadi — 61 px emas, 85 px. Hamma qator bir xil balandlikda degan taxmin buziladi: scrollTop / balandlik endi to'g'ri indeksni bermaydi. Uch narsa kerak:
- Chizilgan qatorni o'lchash (
offsetHeight, ref callback yokiResizeObserver). - Hali chizilmagan qatorlar uchun taxmin.
- Har qatorning boshlanish joyini tez topish — prefiks yig'indi.
4.2 Prefiks yig'indi va ikkilik qidiruv
joylar[i] — i-qatorning tepadan boshlanish joyi: oldingi hamma balandliklar yig'indisi. joylar[n] — umumiy balandlik. Bu bir marta O(n) da hisoblanadi. "Qaysi qator y pikselda?" savoli esa — tartiblangan massivda ikkilik qidiruv, O(log n) (Binary search asoslari). vazifalar dagi sof funksiyalar (src/virtual-oraliq.ts, holat 23), aynan:
/**
* O'rinlar: joylar[i] — i-qatorning boshi (ro'yxat ichida, px),
* joylar[n] — jami balandlik. O'lchanmagan qatorga — taxmin. O(n)
*/
export function joylarniHisobla(
kalitlar: readonly number[],
balandliklar: ReadonlyMap<number, number>,
taxmin: number,
): Float64Array {
const joylar = new Float64Array(kalitlar.length + 1);
let y = 0;
for (let i = 0; i < kalitlar.length; i++) {
const kalit = kalitlar[i];
y +=
(kalit === undefined ? undefined : balandliklar.get(kalit)) ??
taxmin;
joylar[i + 1] = y;
}
return joylar;
}
/** y nuqtasini o'z ichiga olgan qator (ikkilik qidiruv, O(log n)) */
function qatorIndeksi(joylar: Float64Array, y: number): number {
let past = 0;
let yuqori = joylar.length - 1;
while (past < yuqori) {
const orta = (past + yuqori) >> 1;
if ((joylar[orta + 1] ?? 0) <= y) {
past = orta + 1;
} else {
yuqori = orta;
}
}
return past;
}
export type Oraliq = {
readonly boshi: number; // birinchi chiziladigan indeks
readonly oxiri: number; // oxirgisi (kirmaydi)
readonly yuqori: number; // ustidagi bo'sh joy (px)
readonly pastki: number; // ostidagi bo'sh joy (px)
};
/**
* Ko'rinadigan oraliq + zaxira (ekrandan tashqarida ham chiziladigan
* qatorlar — tez aylantirganda bo'sh joy ko'rinmasin)
* @param dan ko'rinadigan qismning boshi (ro'yxat ichidagi
* koordinata)
* @param gacha oxiri
*/
export function korinadiganOraliq(
joylar: Float64Array,
dan: number,
gacha: number,
zaxira: number,
): Oraliq {
const n = joylar.length - 1;
const jami = joylar[n] ?? 0;
const boshi = Math.max(0, qatorIndeksi(joylar, dan) - zaxira);
const oxiri = Math.min(n, qatorIndeksi(joylar, gacha) + 1 + zaxira);
return {
boshi,
oxiri,
yuqori: joylar[boshi] ?? 0,
pastki: jami - (joylar[oxiri] ?? jami),
};
}
// 6 ta vazifa; 2- va 3-si o'lchangan (61 va 85 px),
// qolgani — taxmin
const kalitlar = [11, 12, 13, 14, 15, 16];
const balandliklar = new Map([[12, 61], [13, 85]]);
const joylar = joylarniHisobla(kalitlar, balandliklar, 61);
console.log(Array.from(joylar).join(" "));
console.log(korinadiganOraliq(joylar, 130, 250, 1));Konsolda:
0 61 122 207 268 329 390
{ boshi: 1, oxiri: 5, yuqori: 61, pastki: 61 }Bitta JSDoc izohi telefon uchun ikkiga bo'lindi, qolgani — kanondagidek. Oxirgi qatorlar (6 ta vazifa bilan sinov) — kanonda yo'q, biz qo'shdik (kanonda shu funksiyalar uchun tekshiruv/virtual-oraliq.test.js da 5 ta test bor). Natijani o'qiymiz:
joylar: uchinchi qator (13, 85 px) 122 dan boshlanib 207 da tugaydi — undan keyingilar 85 ga siljigan.- Ko'rinadigan qism 130–250 px: 130 — uchinchi qator ichida (indeks 2), 250 — to'rtinchi (indeks 3). Zaxira 1:
boshi = 2 − 1 = 1,oxiri = 3 + 1 + 1 = 5. yuqori: 61— 0-qator o'rnidagi bo'sh joy;pastki: 61— oxirgi qator o'rnidagi.
Float64Array (Typed arrays) — oddiy massivdan ixcham va tez: 10 000 qatorda hisob ~0.1 ms. Funksiyalar React va DOM'siz — node:test bilan sinash oson (Kichik React loyihani tashkil qilish dagi "sof mantiq alohida" qoidasi).
Tekshirib ko'ring: Nega
qatorIndeksioddiy sikl (forbilan birinchi mos qatorni topish) emas, ikkilik qidiruv?
Javob
Har scroll qadamida chaqiriladi. 10 000 qatorda oddiy sikl o'rtacha 5 000 qadam, ikkilik qidiruv — ~14 qadam (log₂ 10 000). joylar o'sib boruvchi (tartiblangan) massiv, shuning uchun ikkilik qidiruv mumkin. Bu Algoritmlar darsidagi g'oya haqiqiy UI'da.
5. Virtualizatsiyaning narxi
5.1 Nima yo'qoladi
DOM'da yo'q qator — brauzer uchun mavjud emas:
| Narsa | Nima bo'ladi | Yechim |
|---|---|---|
| Ctrl+F (sahifada qidirish) | ko'rinmagan qatorlarni topmaydi | ilova ichidagi qidiruv; kichik ro'yxat — virtualizatsiyasiz |
| Ekran o'quvchi (Malika) | "ro'yxat, 15 element" deydi | aria-setsize, aria-posinset |
| Fokus (Bobur, Tab) | fokusdagi qator ekrandan chiqsa — o'chiriladi | fokusli qatorni doim chizish yoki fokusni ko'chirish |
| Chop etish | faqat chizilganlar | chop etishda to'liq ro'yxat |
| e2e testlar | querySelectorAll("li") — 15 ta |
test kichik ro'yxatda yoki aylantirib |
Shuning uchun virtualizatsiya chegara bilan: kam qatorda — to'liq ro'yxat. vazifalar da chegara — 200.
5.2 Scroll holatini tiklash
Foydalanuvchi 5 000-buyurtmaga aylantirdi, boshqa sahifaga o'tdi, "orqaga" bosdi. Brauzer scroll joyini odatda o'zi tiklaydi (history.scrollRestoration). Lekin virtual ro'yxatda bir muammo bor: tiklash paytida ro'yxat hali chizilmagan yoki balandligi taxminiy bo'lishi mumkin. Brauzer 200 000 px ga aylantirmoqchi, sahifa esa hali 1 000 px.
Amaliy qoidalar:
- Umumiy balandlik darhol bo'lsin — birinchi renderdayoq taxmin bilan to'liq bo'sh joy. Shunda brauzer tiklashga joy topadi.
- Piksel emas, indeks saqlang — balandliklar har xil bo'lsa, "5 000-qator" piksellardan ishonchliroq. Kutubxonalarda: TanStack'da
initialOffset/scrollToIndex, react-virtuoso'dainitialTopMostItemIndexva holatni saqlash. - Saqlash joyi —
sessionStorageyokihistory.state(History API).
6. Kutubxonalar
| Kutubxona | Usul | Kuchli tomoni |
|---|---|---|
TanStack Virtual (@tanstack/react-virtual) |
headless hook: useVirtualizer, useWindowVirtualizer |
markup sizniki, dinamik balandlik, gorizontal va grid |
| react-window (2.x) | tayyor List/Grid komponentlari |
kichik, oddiy, qat'iy balandlikda eng sodda |
| react-virtuoso (4.x) | tayyor komponent, balandlikni o'zi o'lchaydi | chat (pastdan yuqoriga), guruhlar, cheksiz yuklash |
Versiyalar — 2026-10 holati. Uchalasining peer'i React 18 va 19 ni qo'llaydi (React 18 dan 19 ga dagi tekshiruv). TanStack Virtual — headless naqsh (Headless komponentlar darsida batafsil): hook faqat raqamlarni beradi (getVirtualItems(), getTotalSize()), qanday <li> chizish — sizning ishingiz.
Qachon kutubxona, qachon o'zingiz? Kutubxona — sinovdan o'tgan qirralar: o'lcham o'zgarishi, gorizontal, grid, scrollToIndex, RTL. O'zingizniki — faqat kerakli qism, nol bog'liqlik va to'liq nazorat. vazifalar ikkalasini o'lchab tanladi.
7. Vazifalar qadami: useOynaVirtual (holat 23)
7.1 Qaror
Yangi branch: perf/virtual-royxat. Kanon ikki variantni qurdi va o'lchadi: TanStack Virtual 3.14.13 bilan va o'z hook'i bilan (natijalar/23-*, 10 000 vazifa, oyna 412×800, Chrome headless):
| holat 22 | TanStack 3.14.13 | o'zimizniki (kanon) | |
|---|---|---|---|
| ochilish (load) | 752–770 ms | 180–218 ms | 163–181 ms |
DOM element / <li> |
50 087 / 10 000 | 154 / 15 | 154 / 15 |
| JS heap | 45 MB | 12 MB | 11 MB |
| JS+CSS gzip | 86 041 B | +8 290 B | +827 B |
- holat 22: to'liq ro'yxatDOM 50 087759 ms
- TanStack Virtual 3.14.13+8.3 KB200 ms
- useOynaVirtual (kanon)+0.8 KB171 ms
Manba: O'lchandi: vazifalar holat 22/23, olchov/katta-royxat.mjs, Chrome headless, 412×800, har biri 3 marta (natijalar/23-oldin.txt, 23-tanstack.txt, 23-ozimiz.txt), 2026-10-13
Tezlik — teng (farq shovqin ichida). Hajm — 10 barobar farq. TanStack Virtual'ning eng yangi versiyasi (3.14.18) o'lchov kuni chiqqan edi — kanon bir oylik 3.14.13 ni oldi (Ta'minot zanjiri xavfsizligi: yangi chiqqan paketni darhol olmaslik). Qaror: o'zimizniki, chunki bizga oyna scroll'i va vertikal ro'yxat kerak, xolos.
7.2 VazifaRoyxati: ikki xil ro'yxat
/** Shundan ko'p bo'lsa — faqat ko'rinadigan qatorlar chiziladi */
export const VIRTUAL_CHEGARA = 200;
// Qator balandligi taxmini (px): haqiqiysi chizilgach o'lchanadi —
// uzun matn ikki qatorga o'tsa ham to'g'ri. Zaxira — ekrandan
// tashqaridagi qatorlar (tez aylantirganda bo'sh joy ko'rinmasin)
// 2.75rem tugma + 2 × 0.5rem + 1px chegara
const TAXMINIY_BALANDLIK = 61;
const ZAXIRA = 8;
…
export const VazifaRoyxati = memo(function VazifaRoyxati(props: Props) {
// Ikki xil komponent: hooklar shart ichida chaqirilmaydi (17/#25)
return props.vazifalar.length > VIRTUAL_CHEGARA ? (
<VirtualRoyxat {...props} />
) : (
<ToliqRoyxat {...props} />
);
});Bu bo'limdagi kanon parchalarida izohlar telefon uchun qayta bo'lindi (TAXMINIY_BALANDLIK izohi uning ustiga ko'chdi), kod — aynan. 200 tagacha — avvalgi to'liq ro'yxat (Ctrl+F, ekran o'quvchi, e2e — hammasi o'zgarmadi). Ko'p bo'lsa — VirtualRoyxat. Ikki alohida komponent, chunki virtual variantda hook bor, hooklar esa shart ichida chaqirilmaydi (Hooklar qoidalari). Chegaradan o'tilganda type o'zgaradi va ro'yxat qayta yaratiladi — Reconciliation darsida buni ko'rgan edik.
7.3 VirtualRoyxat
function VirtualRoyxat({ vazifalar, sorov, tahrirId }: Props) {
const kalitlar = useMemo(
() => vazifalar.map((v) => v.id),
[vazifalar],
);
// Destrukturizatsiya shart: boshniBelgila ref prop'ga beriladi
// va React Compiler lint'i butun obyektni "ref" deb biladi —
// yuqori ni render'da o'qish "Cannot access refs during render"
// (o'lchandi)
const { boshi, oxiri, yuqori, pastki, olchov, boshniBelgila } =
useOynaVirtual(kalitlar, TAXMINIY_BALANDLIK, ZAXIRA);
return (
<>
<li
ref={boshniBelgila}
className="joy"
aria-hidden="true"
style={{ height: yuqori }}
/>
{vazifalar.slice(boshi, oxiri).map((vazifa, i) => (
<VazifaQatori
key={vazifa.id}
vazifa={vazifa}
sorov={sorov}
tahrirda={vazifa.id === tahrirId}
joy={boshi + i}
jami={vazifalar.length}
olchov={olchov}
/>
))}
<li
className="joy"
aria-hidden="true"
style={{ height: pastki }}
/>
</>
);
}«Bo'sh joy» bo'limidagi ikkinchi usul: ikkita bo'sh <li class="joy" aria-hidden> — ustida va ostida. aria-hidden — ekran o'quvchi ularni qator deb o'qimasin. style={{ height }} — CSSOM orqali, CSP'ning style-src i bilan mos (Brauzer xavfsizlik modeli: CSP). key={vazifa.id} — haqiqiy id.
Izohdagi tuzoq — React Compiler lint'i (react-hooks/refs, React Compiler). useOynaVirtual(...) natijasini bitta obyektga olib, oyna.yuqori ni o'qisa — lint xato berdi: obyektdagi boshniBelgila ref'ga beriladi, demak butun obyekt "ref". Destrukturizatsiya bilan har qiymat alohida o'zgaruvchi — xato yo'qoldi.
VazifaQatori ga joy, jami, olchov — uchta primitiv yoki barqaror props. Obyekt emas: { indeks, jami, olchov } har renderda yangi bo'lib, memo ni buzardi (memo). Obyekt VazifaQatori ichida yasaladi.
7.4 useOynaVirtual: asosiy qismlar
Hook oyna (window) scroll'iga ulanadi — ro'yxat alohida aylanadigan quti emas, butun sahifa aylanadi:
// Scroll — tashqi manba (useSyncExternalStore, 17/#38). Snapshot —
// 50 px lik qadam: har piksel emas, faqat qadam o'zgarganda qayta
// chiziladi
const QADAM = 50;
…
const scrollQadami = () =>
`${Math.floor(scrollY / QADAM)}:${Math.ceil(innerHeight / QADAM)}`;Scroll — React'dan tashqaridagi manba, shuning uchun useSyncExternalStore. Snapshot — satr, "qadam:oyna balandligi". Bir xil satr — Object.is teng — render yo'q. Shunday qilib, 50 px ichida aylantirish React'ni umuman bezovta qilmaydi («Scroll hodisasini kamaytirish» bo'limidagi ikkinchi usul).
O'lchash — ref callback: chizilgan har <li> balandligi state'dagi Map ga yoziladi (o'zgargan bo'lsa — yangi Map, State III). Ro'yxatning sahifadagi boshi — ResizeObserver (ResizeObserver): ro'yxat ustidagi forma yoki statistika balandligi o'zgarsa, qayta o'lchanadi.
7.5 e2e topgan xato
Birinchi variantda o'lchanmagan qatorlar uchun taxmin — 49 px edi. Brauzer tekshiruvi (e2e-18) qizarib ketdi: haqiqiy qator 61 px. Aylantirilganda har yangi o'lchov ro'yxat pastidagi bloklarni siljitdi — «Batafsil statistika» tugmasi qochib ketdi va bosish tegmadi. Tuzatish — kanon izohida:
// O'lchanmagan qatorga — o'lchanganlarning o'rtachasi (bo'lmasa
// — taxmin). Aks holda har yangi o'lchov ro'yxatdan keyingi
// bloklarni siljitadi: taxmin 49 px, haqiqiy qator 61 px —
// aylantirilganda «Batafsil statistika» tugmasi qochib ketdi va
// bosish tegmadi (e2e topdi)Taxmin endi doimiy son emas — o'lchanganlarning o'rtachasi. TAXMINIY_BALANDLIK ham 61 ga to'g'rilandi va hisobi izohda: tugma balandligi + paddinglar + chegara. Dinamik balandlikdagi eng ko'p uchraydigan xato — layout siljishi (CLS); uni faqat haqiqiy brauzer testi ko'rsatdi.
Malika uchun: har qatorda aria-posinset (1 dan) va aria-setsize (jami) — DOM'da 15 ta <li> bo'lsa ham, ekran o'quvchi "5 001-element, jami 10 000" deydi. Bobur uchun: belgilangan qator uzoqqa ko'chsa (bajarildi → pastga), fokus qo'shni qatorga o'tadi (useRef II dagi zaxira).
7.6 Natija va commit
| holat 22 | holat 23 | |
|---|---|---|
| katak 1 000 / 10 000 (mediana) | 32 / 120 ms | 24 / 40 ms |
| qidiruv 10 000 (eng uzun) | 200 ms | 24 ms |
| JS heap (10 000) | 45 MB | 11 MB |
Qidiruv 200 → 24 ms: "v" yozilganda endi 10 000 emas, ~15 qator qayta chiziladi. useTransition darsidagi boshlang'ich 1 184 ms dan — 24 ms. Testlar: node 169 (+5 — virtual-oraliq.test.js), Vitest 39. Brauzer: 159/159, e2e-18 40/40. Diff: 10 fayl, +373 −21.
perf: 200 dan ko'p vazifa — virtual ro'yxat (useOynaVirtual)8. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
| aylantirganda bo'sh oq joy | zaxira yo'q yoki kichik | overscan 3–8 qator |
| scroll satri "sakraydi" | taxmin haqiqiy balandlikdan uzoq | o'lchanganlar o'rtachasi, yaxshiroq taxmin |
| pastdagi tugma qochadi, bosish tegmaydi | o'lchovlar sahifani siljitadi (CLS) | o'rtacha taxmin; e2e bilan tekshiring |
| har pikselda butun ro'yxat chiziladi | scrollTop piksel state'da |
indeks yoki qadam state'da |
| qatordagi yozilgan matn aylantirganda yo'qoladi | qator DOM'dan chiqib, qayta yaratildi | holatni ro'yxat darajasida saqlang |
key={i} bilan qatorlar aralashadi |
slice ichidagi indeks |
haqiqiy id |
9. Mashqlar
Mashqlar kurs/mashqlar/18/23-virtualizatsiya/ da. Har yechimni «Ishga tushir» bilan sinang.
1-mashq (oson): qancha qator?
Qator 50 px, oyna 600 px, zaxira 2, jami 5 000 qator.
scrollTop = 0: start [:0], end [:14]scrollTop = 10000: start [:198], end [:214]
Yechim
scrollTop = 0: first = 0, last = ceil(600 / 50) = 12; start = max(0, 0 − 2) = 0, end = 12 + 2 = 14. scrollTop = 10000: first = 200, last = ceil(10 600 / 50) = 212; start = 198, end = 214. DOM'da 14 va 16 qator — 5 000 o'rniga.
2-mashq (o'rta): aria-setsize va aria-posinset
«Birinchi variant» dagi VirtualList ni Malika uchun yaxshilang: har <li> ga aria-posinset (1 dan boshlanadigan haqiqiy o'rni) va aria-setsize (jami) qo'shing. «O'rtasi» dan keyin birinchi chizilgan qator uchun konsolga №4999: 4999 / 10000 chiqsin (aria-posinset / aria-setsize).
Yechim
import { useLayoutEffect, useRef, useState } from "react";
const ROW = 40;
const VIEW = 200;
const ORDERS = Array.from(
{ length: 10000 },
(_, i) => `Buyurtma №${i + 1}`,
);
function VirtualList({ items }) {
const [top, setTop] = useState(0);
const box = useRef(null);
const start = Math.max(0, Math.floor(top / ROW) - 2);
const end = Math.min(
items.length,
Math.ceil((top + VIEW) / ROW) + 2,
);
useLayoutEffect(() => {
const li = box.current.querySelector("li");
const pos = li.getAttribute("aria-posinset");
const size = li.getAttribute("aria-setsize");
console.log(`№${pos}: ${pos} / ${size}`);
});
function jump(y) {
box.current.scrollTop = y;
setTop(y);
}
return (
<div>
<button onClick={() => jump(200000)}>O'rtasi</button>
<div
ref={box}
onScroll={(e) => setTop(e.currentTarget.scrollTop)}
style={{ height: VIEW, overflowY: "auto" }}
>
<ul
style={{ height: items.length * ROW, position: "relative" }}
>
{items.slice(start, end).map((text, i) => (
<li
key={start + i}
aria-posinset={start + i + 1}
aria-setsize={items.length}
style={{ position: "absolute", top: (start + i) * ROW }}
>
{text}
</li>
))}
</ul>
</div>
</div>
);
}
function App() {
return <VirtualList items={ORDERS} />;
}Konsolda:
№1: 1 / 10000
№4999: 4999 / 10000aria-posinset — indeks + 1 (ekran o'quvchi 1 dan sanaydi), aria-setsize — items.length, DOM'dagi <li> soni emas. Malika endi "4 999-element, jami 10 000" eshitadi. vazifalar dagi Qator.tsx aynan shunday qiladi (joy.indeks + 1, joy.jami).
3-mashq (qiyin): indeks bo'yicha o'tish
VirtualList ga «№7500 ga o'tish» tugmasini qo'shing: bosilganda ro'yxat shunday aylansinki, №7500 ekranning eng tepasida bo'lsin. Konsolga birinchi ko'rinadigan (zaxira emas) qatorni chiqaring: tepada: Buyurtma №7500. Ishora: indeksdan piksel — indeks × ROW; zaxirasiz birinchi qator — Math.floor(top / ROW).
Yechim
import { useLayoutEffect, useRef, useState } from "react";
const ROW = 40;
const VIEW = 200;
const ORDERS = Array.from(
{ length: 10000 },
(_, i) => `Buyurtma №${i + 1}`,
);
function VirtualList({ items }) {
const [top, setTop] = useState(0);
const box = useRef(null);
const first = Math.floor(top / ROW);
const start = Math.max(0, first - 2);
const end = Math.min(
items.length,
Math.ceil((top + VIEW) / ROW) + 2,
);
useLayoutEffect(() => {
console.log("tepada:", items[first]);
});
function scrollToIndex(index) {
const y = index * ROW;
box.current.scrollTop = y;
setTop(y);
}
return (
<div>
<button onClick={() => scrollToIndex(7499)}>
№7500 ga o'tish
</button>
<div
ref={box}
onScroll={(e) => setTop(e.currentTarget.scrollTop)}
style={{ height: VIEW, overflowY: "auto" }}
>
<ul
style={{ height: items.length * ROW, position: "relative" }}
>
{items.slice(start, end).map((text, i) => (
<li
key={start + i}
style={{ position: "absolute", top: (start + i) * ROW }}
>
{text}
</li>
))}
</ul>
</div>
</div>
);
}
function App() {
return <VirtualList items={ORDERS} />;
}Konsolda:
tepada: Buyurtma №1
tepada: Buyurtma №7500№7500 — indeks 7 499 (0 dan). scrollToIndex — kutubxonalardagi o'sha nomli funksiyaning eng sodda ko'rinishi. Balandliklar har xil bo'lsa, index × ROW o'rniga joylar[index] («Prefiks yig'indi» bo'limi) — va hali o'lchanmagan qatorlar taxminiy bo'lgani uchun kutubxonalar o'tgandan keyin yana bir marta tuzatadi. Scroll holatini tiklashda ham aynan shu funksiya ishlatiladi: indeksni saqlab, qaytganda scrollToIndex.
4-mashq: vazifalar qadami
perf/virtual-royxat branch'ida «Vazifalar qadami» dagi o'zgarishlarni kiriting: virtual-oraliq.ts (+ 5 ta node:test), useOynaVirtual.ts, VazifaRoyxati (chegara, ToliqRoyxat/VirtualRoyxat), VazifaQatori/Qator (joy, aria-*), CSS. 10 000 vazifa bilan DevTools → Elements'da #royxat ichidagi <li> larni sanang, Performance'da sahifa ochilishini yozing. Commit va PR.
Yechim
document.querySelectorAll("#royxat li").length — 15–20 atrofida (ikki joy elementi bilan; aniq soni oyna balandligiga bog'liq). Aylantirganda son deyarli o'zgarmaydi, aria-posinset lar esa o'sadi. 200 tadan kam vazifada — hamma <li> joyida, joy elementlari yo'q. Ochilish — kanonda ~170 ms (holat 22 da ~760). Agar e2e'dagi «Batafsil statistika» bosilmasa — «e2e topgan xato» bo'limidagi o'rtacha taxminni tekshiring.
10. Real ishda
- Jadvallar: admin panel, logistika, bank ko'chirmalari — minglab qator; TanStack Table + TanStack Virtual — keng tarqalgan juftlik.
- Chat va lenta: Telegram'ga o'xshash chatlar pastdan yuqoriga virtualizatsiya (react-virtuoso), cheksiz yuklash bilan.
- Mobil: React Native'da
FlatList— sukut bo'yicha virtual ro'yxat; g'oya o'sha. - Intervyu savollari: "10 000 qatorli ro'yxatni qanday tezlashtirasiz?", "Virtualizatsiya qanday ishlaydi?", "Har xil balandlikli qatorlar bilan nima qilasiz?", "Virtualizatsiyaning a11y narxi?".
Xulosa
- Virtualizatsiya — faqat ko'rinadigan qatorlar + zaxira; qolgani — bo'sh joy, umumiy balandlik o'zgarmaydi. Ma'lumot xotirada to'liq, DOM esa kichik.
- Qat'iy balandlik:
start = floor(scrollTop / h) − overscan,end = ceil((scrollTop + oyna) / h) + overscan. - Har xil balandlik: o'lchash (ref callback,
ResizeObserver), taxmin (o'lchanganlar o'rtachasi), prefiks yig'indi va ikkilik qidiruv. - Narxi: Ctrl+F, ekran o'quvchi, fokus, chop etish — chegara (200) va
aria-setsize/aria-posinset; scroll tiklashda indeks saqlang. vazifalar10 000: ochilish ~760 → ~170 ms, DOM 50 087 → 154, heap 45 → 11 MB, qidiruv 200 → 24 ms; o'z hook'i +0.8 KB (TanStack +8.3 KB, tezlik teng).
Keyingi dars: Og'ir ishni UI'dan ajratish: debounce, throttle va Web Worker — statistika hisobi asosiy oqimdan alohida oqimga chiqadi.
Manbalar
- TanStack Virtual — tanstack.com/virtual
- react-window — github.com/bvaughn/react-window
- react-virtuoso — virtuoso.dev
- MDN:
aria-setsize,aria-posinset— developer.mozilla.org/docs/Web/Accessibility/ARIA - web.dev: "Virtualize large lists" — web.dev/articles/virtualize-long-lists-react-window
- Kanon:
sinov/18/QADAMLAR.md(#23),natijalar/23-oldin.txt,23-ozimiz.txt,23-tanstack.txt,natijalar/tekshiruv-23.txt
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!