Mundarija (37)
- Bu darsda
- 1. Nega bu kerak?
- 2. Kadr nima va brauzer qachon chizadi
- 2.1 Eslab olamiz: kadr va byudjet
- 2.2 Event loop'dagi joyi
- 3. Birinchi animatsiya
- 3.1 requestAnimationFrame(callback)
- 3.2 Callback'ga keladigan vaqt
- 3.3 «Bahor»: buyurtmalar hisoblagichi
- 4. Delta time: harakatni vaqtga bog'lash
- 4.1 "Har kadrda 3 px" — xato yo'l
- 4.2 Qadamma-qadam
- 4.3 O'zingiz ko'ring: sekin kompyuter
- 5. To'xtatish: cancelAnimationFrame
- 5.1 So'rovni bekor qilish
- 5.2 Callback ichidan to'xtatish
- 6. Yashirin tabda rAF to'xtaydi
- 6.1 Brauzer batareyani tejaydi
- 6.2 Bu nimani anglatadi?
- 7. O'qish va yozishni kadrlarga ajratish
- 7.1 Va'da: layout thrashing'ga qarshi
- 8. Qachon rAF kerak emas
- 9. Node'da rAF yo'q
- 10. Ko'p uchraydigan xatolar
- 10.1 Keyingi kadrni so'ramaslik
- 10.2 Funksiyani chaqirib yuborish
- 10.3 Ikkita parallel sikl
- 10.4 Harakatni kadrlar soniga bog'lash
- 10.5 rAF ichida og'ir ish
- 11. Mashqlar
- 1-mashq (oson): Bron progress chizig'i
- 2-mashq (o'rta): Boshlash va to'xtatish
- 3-mashq (qiyin): Qayta ishlatiladigan animateValue
- 4-mashq: Amaliy tajriba — vazifalar hisobi silliq o'zgarsin
- 12. Real ishda
- Xulosa
- Manbalar
JavaScript requestAnimationFrame: animatsiyani ekran kadrlariga moslash
Qisqacha:
requestAnimationFrame(fn)brauzerga "keyingi kadrni chizishdan oldinfnni chaqir" deydi. Animatsiya uchun usetIntervaldan yaxshiroq: callback ekran yangilanishi bilan bir ritmda keladi, yashirin tabda esa umuman chaqirilmaydi. Har kadrdafno'zini qayta so'raydi, harakatni esa o'tgan vaqtga (delta time) qarab hisoblaydi; to'xtatish —cancelAnimationFrame(id).
Bu darsda
requestAnimationFramebilan kadr siklini yoza olasiz va unicancelAnimationFramebilan to'xtata olasiz.- Callback'ga keladigan vaqt belgisidan foydalanib, animatsiyani vaqtga bog'laysiz — sekin kompyuterda ham to'g'ri tezlikda.
setIntervalbilan qilingan animatsiya nega "titrashini" va tabni yashirganda nima bo'lishini tushuntira olasiz.- DOM'ni o'qish va yozishni kadrlar bo'yicha ajrata olasiz.
Oldin bilishingiz kerak: Taymerlar: setTimeout va setInterval, Event loop, Rendering pipeline va animatsiya samaradorligi, Sahifa hayot sikli va Page Visibility.
1. Nega bu kerak?
13-qism boshlandi. Bu qismda brauzerning JavaScript'ga beradigan "asboblari"ni birma-bir o'rganamiz: animatsiya, kuzatuvchilar, fayllar, rasm chizish, oflayn ishlash. Oxirida esa sahifani tez va xavfsiz qilishni.
Birinchi asbob — animatsiya uchun. «Bahor» oshxonasining bosh sahifasida Jasur aka bitta raqam ko'rsatmoqchi: "Bugun 1 284 ta buyurtma". U raqam birdan chiqmasin, 0 dan boshlab "sanab" chiqsin — xuddi do'kon tablosidagi kabi.
Stajyor Sardor birinchi bo'lib Taymerlar darsidagi asbobni oldi: setInterval(..., 16). Har 16 ms da raqamni oshiradi — soniyasiga taxminan 60 marta. Kompyuterida yaxshi ishladi. Jasur akaning telefonida esa raqam "sakrab" bordi, ba'zan bir lahza to'xtab qoldi. Boshqa tabga o'tib qaytsa, animatsiya allaqachon tugagan edi — hech kim ko'rmagan.
Muammo bitta: taymer ekran bilan kelishmagan. Ekran o'z ritmida yangilanadi, taymer esa o'zinikida. Ba'zi kadrlarga ikki o'zgarish tushadi, ba'zilariga birortasi ham. Brauzerda buning uchun maxsus "eshik" bor — requestAnimationFrame. U callback'ni aynan ekran yangilanadigan paytga qo'yadi.
2. Kadr nima va brauzer qachon chizadi
2.1 Eslab olamiz: kadr va byudjet
Rendering pipeline darsida ko'rgan edik: brauzer ekranni kadrlar bilan yangilaydi. Har kadrda konveyer ishlaydi: uslub hisobi (style) → joylashtirish (layout) → bo'yash (paint) → yig'ish (composite). Ekran necha marta yangilanishi uning chastotasiga bog'liq. Chastota gers (Hz) bilan o'lchanadi: 60 Hz — "soniyasiga 60 marta". Oddiy monitor — 60 Hz, ko'p zamonaviy telefonlar — 90 yoki 120 Hz.
Bitta kadrga ajratilgan vaqt — kadr byudjeti — shunchaki 1000 ms ni chastotaga bo'lish:
- 60 Hzoddiy monitor16,7 ms
- 90 Hz11,1 ms
- 120 Hzko'p telefonlar8,3 ms
- 144 Hzo'yin monitori6,9 ms
Manba: Hisob: 1000 ms ÷ ekran chastotasi (Hz)
Nimaga qarang: ekran qanchalik tez bo'lsa, byudjet shunchalik kichik. 120 Hz telefonda sizning JavaScript'ingiz, uslub, layout va bo'yash — hammasi 8 ms ga sig'ishi kerak. Sig'masa, kadr tushib qoladi va foydalanuvchi "qotish" (jank) ni ko'radi.
2.2 Event loop'dagi joyi
Event loop darsida bitta aylanishni ko'rdik: bitta task → hamma microtask'lar → kerak bo'lsa chizish. O'shanda "chizishdan oldin, har kadr uchun ishlaydigan callback ham bor" degan edik. Mana u.
Brauzer kadr chizishga qaror qilganda, konveyerdan oldin bitta ishni bajaradi: shu kadr uchun so'ralgan barcha requestAnimationFrame callback'larini chaqiradi. Keyin uslub, layout va bo'yash keladi.
Nom uzun, shuning uchun matnda va sxemada uni qisqa qilib rAF deb yozamiz — dasturchilar ham shunday aytishadi. Sxemada bir aylanishni kuzating:
flowchart LR
T["Task<br/>(bosish, taymer)"] --> M["Microtask'lar"]
M --> Q{"Chizish<br/>vaqti?"}
Q -- "yo'q" --> T
Q -- "ha" --> R["rAF<br/>callback'lari"]
R --> S["Style va<br/>layout"]
S --> P["Paint va<br/>composite"]
P --> TShuning uchun rAF callback'ida DOM'ga yozgan narsangiz aynan shu kadrda ekranga chiqadi. Taymer callback'i esa kadrning istalgan joyiga tushishi mumkin: ba'zan ikki taymer bitta kadr ichida ishlaydi (biri behuda), ba'zan kadr umuman taymersiz o'tadi (harakat "to'xtaydi").
Tekshirib ko'ring: 60 Hz ekranda
setInterval(fn, 10)ishlatildi. Har kadrga nechtafnchaqiruvi to'g'ri keladi?
Javob
O'rtacha 1,67 ta — ya'ni ba'zi kadrlarga bitta, ba'zilariga ikkita. Ikkinchi chaqiruvning natijasini foydalanuvchi baribir ko'rmaydi: ekran faqat kadr chizilganda yangilanadi. Bu behuda ish va notekis harakat. rAF har kadrga aynan bitta chaqiruv beradi.
3. Birinchi animatsiya
3.1 requestAnimationFrame(callback)
Sintaksis bitta qator:
const id = requestAnimationFrame(callback);callback— keyingi kadr chizilishidan oldin bir marta chaqiriladigan funksiya.- Qaytgan
id— butun son, so'rov raqami. UsetTimeoutning taymer identifikatoriga o'xshaydi va bekor qilish uchun kerak bo'ladi.
E'tibor bering: bir marta. rAF setInterval kabi o'zi takrorlanmaydi. Animatsiya uchun callback har safar keyingi kadrni o'zi so'raydi. Bu naqsh kadr sikli (animation loop) deyiladi:
function step() {
// 1) holatni yangilash, 2) DOM'ga yozish
requestAnimationFrame(step); // keyingi kadrni so'rash
}
requestAnimationFrame(step); // birinchi kadrni so'rashBu rekursiyaga o'xshaydi, lekin stek o'smaydi. Funksiya step keyingi so'rovni qo'yadi va tugaydi. Keyingi chaqiruv yangi kadrda, bo'sh stekdan boshlanadi — xuddi Taymerlar darsidagi o'zini qayta qo'yadigan setTimeout kabi.
3.2 Callback'ga keladigan vaqt
rAF callback'iga bitta argument keladi — vaqt belgisi (timestamp). Bu performance.now() bilan bir xil o'lchovdagi son: sahifa ochilgandan beri o'tgan millisekundlar, kasr qismi bilan. Bu funksiyani Performansni o'lchash darsida ishlatgan edik.
Bu vaqt — kadr boshlangan payt. Shu kadrdagi barcha rAF callback'lari bir xil sonni oladi. Biz Chrome 154 da sinadik: bitta skriptda so'ralgan ikki callback ikkalasi 97.3 ni oldi, ichkaridan so'ralgani esa keyingi kadrda boshqa son oldi.
Kadr sikli shu vaqt bilan ishlaydi. Animatsiya boshlangan vaqtni eslab qolamiz va har kadrda "qancha o'tdi?" deb so'raymiz.
3.3 «Bahor»: buyurtmalar hisoblagichi
Jasur akaning raqami. Animatsiya 1 soniya davom etadi. Har kadrda o'tgan vaqtni davomiylikka bo'lamiz — progress chiqadi. Progress (inglizcha "ilgarilash") — animatsiyaning qancha qismi bajarilgani: 0 — boshlanmadi, 0.5 — yarmi, 1 — tugadi. Xuddi yuklab olishdagi foiz chizig'i kabi, faqat 0–100 emas, 0–1. Raqam esa progress × 1284:
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
.counter { font: 700 3rem/1.2 ui-monospace, monospace; margin: 0; }
button { font: inherit; padding: 0.4rem 0.8rem; }
</style>
<p>Bugun «Bahor»da buyurtmalar:</p>
<p class="counter" id="counter">0</p>
<button id="again" type="button">Qayta sanash</button>
<script>
const counter = document.getElementById("counter");
const target = 1284;
const duration = 1000; // ms
let startTime = null;
function formatNumber(n) {
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, " ");
}
function step(time) {
if (startTime === null) startTime = time;
const progress = Math.min((time - startTime) / duration, 1);
counter.textContent = formatNumber(Math.round(progress * target));
if (progress < 1) {
requestAnimationFrame(step);
}
}
function start() {
startTime = null;
requestAnimationFrame(step);
}
document.getElementById("again").addEventListener("click", start);
start();
</script>Qatorma-qator:
startTime === null— birinchi kadr. Animatsiya boshlangan vaqtni shu kadrning vaqt belgisidan olamiz.performance.now()ni alohida chaqirish shart emas.(time - startTime) / duration— o'tgan ulush. 500 ms o'tgan bo'lsa —0.5.Math.min(..., 1)oxirgi kadrda 1 dan oshib ketmasligi uchun (1016 ms bo'lishi mumkin).progress < 1bo'lsa keyingi kadr so'raladi. Aks holda sikl o'zi to'xtaydi — yangi so'rov yo'q.formatNumber— minglarni bo'sh joy bilan ajratadi (lookahead darsidagi naqsh).toLocaleStringChrome'da vergul qo'yadi, shuning uchun o'zimiz yozdik.
Sahifa ochilishi bilan raqam 1 soniyada 0 dan 1 284 gacha boradi. «Qayta sanash» ni bosing — yana noldan. Biz sinab ko'rdik: 1,2 soniyadan keyin matn aniq 1 284.
Bu animatsiya chiziqli: raqam bir xil tezlikda o'sadi. Oxirida sekinlashsa chiroyliroq — buning uchun progress'ni "egamiz": 1 - (1 - progress) ** 3. Bu CSS'dagi ease-out ning JavaScript'dagi ko'rinishi. Uni 3-mashqda qo'shasiz.
Tekshirib ko'ring:
stepichidagiif (progress < 1)shartini olib tashlab,requestAnimationFrame(step)ni doim chaqirsak nima bo'ladi?
Javob
Ekranda farq ko'rinmaydi — raqam 1 284 da qoladi, chunki Math.min progress'ni 1 da ushlab turadi. Lekin sikl abadiy ishlaydi: har kadrda (soniyasiga 60–120 marta) behuda callback va DOM'ga yozish. Telefon batareyasi uchun bu sezilarli. Animatsiya tugaganda yangi kadr so'ramaslik kerak.
4. Delta time: harakatni vaqtga bog'lash
4.1 "Har kadrda 3 px" — xato yo'l
Endi harakatlanuvchi narsa. «Bahor» sahifasida kichik kuryer belgisi yo'l bo'ylab yuradi. Birinchi xayol: "har kadrda 3 px suraman".
Bu yerda tuzoq bor. 60 Hz ekranda 3 px × 60 = soniyasiga 180 px. 120 Hz telefonda — 360 px, ya'ni ikki baravar tez. Kompyuter band bo'lib, kadrlar tushib qolsa — sekinroq. Harakat tezligi ekranga va kompyuter holatiga bog'lanib qoladi.
To'g'ri yo'l — tezlikni soniyaga berish: "kuryer soniyasiga 180 px yuradi". Har kadrda esa "oxirgi kadrdan beri qancha vaqt o'tdi?" deb so'raymiz. Bu vaqt delta time (dt, "vaqt farqi") deyiladi:
yangi joy = eski joy + tezlik × dtKadr kech kelsa, dt katta bo'ladi va kuryer ko'proq suriladi. Natijada bir soniyadan keyin u doim bir joyda — kadrlar soni qanday bo'lishidan qat'i nazar.
4.2 Qadamma-qadam
Hisobni Node'da ham sinash mumkin: rAF o'rniga kadr vaqtlarini qo'lda beramiz. 48 ms dan keyin bitta kadr tushib qoldi (64 o'rniga 80 da keldi) — qarang, joy baribir to'g'ri:
"Har kadrda 3 px" usulida 80 ms da kuryer 12 px da bo'lardi — tushgan kadr uchun 3 px yo'qotiladi. Kadrlar ko'p tushsa, yo'qotish ham ko'payadi.
4.3 O'zingiz ko'ring: sekin kompyuter
Ikki kuryer bir xil yo'lda. Yuqoridagisi "har kadrda 3 px" bilan, pastdagisi "soniyasiga 180 px" (delta time) bilan yuradi. «Kompyuterni sekinlashtir» tugmasi har kadrga 50 ms og'ir ish qo'shadi — eski telefonni taqlid qiladi:
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
.road { position: relative; height: 2rem; margin: 0.5rem 0;
background: #eef3ee; border-radius: 1rem; overflow: hidden; }
.courier { position: absolute; top: 0.25rem; left: 0;
width: 1.5rem; height: 1.5rem; border-radius: 50%; }
#perFrame { background: #c0392b; }
#perSecond { background: #2e7d32; }
button { font: inherit; padding: 0.4rem 0.8rem;
margin-top: 0.5rem; }
</style>
<p>Har kadrda 3 px:</p>
<div class="road"><div class="courier" id="perFrame"></div></div>
<p>Soniyasiga 180 px (delta time):</p>
<div class="road"><div class="courier" id="perSecond"></div></div>
<button id="slow" type="button" aria-pressed="false">
Kompyuterni sekinlashtir
</button>
<p id="info">Kadrlar soniyasiga: …</p>
<script>
const perFrame = document.getElementById("perFrame");
const perSecond = document.getElementById("perSecond");
const slowButton = document.getElementById("slow");
const info = document.getElementById("info");
const roadWidth = 300; // px, keyin boshiga qaytadi
let x1 = 0;
let x2 = 0;
let previous = null;
let isSlow = false;
let frames = 0;
let secondStart = 0;
function busyWait(ms) {
const end = performance.now() + ms;
while (performance.now() < end) {}
}
function step(time) {
if (previous === null) previous = secondStart = time;
const dt = (time - previous) / 1000;
previous = time;
x1 = (x1 + 3) % roadWidth;
x2 = (x2 + 180 * dt) % roadWidth;
perFrame.style.transform = `translateX(${x1}px)`;
perSecond.style.transform = `translateX(${x2}px)`;
frames++;
if (time - secondStart >= 1000) {
info.textContent = `Kadrlar soniyasiga: ${frames}`;
frames = 0;
secondStart = time;
}
if (isSlow) busyWait(50);
requestAnimationFrame(step);
}
slowButton.addEventListener("click", () => {
isSlow = !isSlow;
slowButton.setAttribute("aria-pressed", String(isSlow));
});
requestAnimationFrame(step);
</script>Nimani ko'rdingiz:
- Oddiy holatda ikki kuryer taxminan bir xil yuradi (60 Hz ekranda). 120 Hz ekranda esa qizili ikki baravar tez — chunki u kadrlarni sanaydi.
- «Kompyuterni sekinlashtir» bosilgach, kadrlar soni soniyasiga taxminan 20 ga tushadi. Qizil kuryer sudralib qoladi. Yashil kuryer kamroq kadr bilan, "sakrab-sakrab" yursa ham, avvalgi tezlikda boradi.
Sinovda (Chrome 154) sekinlashtirilganda kadrlar soniyasiga 20 ga tushdi. Qizil kuryer soniyasiga 20 × 3 = 60 px yurdi, yashili esa avvalgidek 180 px — uch baravar ko'p. Harakat transform bilan — Rendering pipeline darsidagi eng arzon xususiyat.
Maslahat: Tab yashirinib qaytganda
dtjuda katta bo'lishi mumkin (masalan, 30 soniya) va obyekt bir zumda ekrandan "uchib" ketadi. O'yin va fizika animatsiyalaridadtni cheklashadi:Math.min(dt, 0.1).
Tekshirib ko'ring: Kadr sikli boshida
previousni0ga emas,nullga tenglab, birinchi kadrdatimeni yozdik. Agarlet previous = 0;qoldirsak, birinchi kadrda nima bo'ladi?
Javob
Birinchi kadrning vaqt belgisi sahifa ochilganidan beri o'tgan vaqt — masalan, 2 500 ms. Unda dt = 2,5 soniya bo'ladi va kuryer birinchi kadrdayoq 450 px sakraydi. Shuning uchun boshlang'ich vaqt birinchi kadrning o'zidan olinadi.
5. To'xtatish: cancelAnimationFrame
5.1 So'rovni bekor qilish
cancelAnimationFrame(id) — hali bajarilmagan rAF so'rovini bekor qiladi. Xuddi clearTimeout kabi. Kadr siklida esa eng oxirgi so'rovning id si kerak. Shuning uchun har kadrda yangi id ni bitta umumiy o'zgaruvchiga yozib boramiz:
let frameId = null;
function step(time) {
// ... harakat ...
frameId = requestAnimationFrame(step);
}
function start() {
if (frameId !== null) return; // allaqachon ishlayapti
frameId = requestAnimationFrame(step);
}
function stop() {
cancelAnimationFrame(frameId);
frameId = null;
}start dagi tekshiruv muhim. Uni unutsangiz, «Boshlash» ni ikki marta bosish ikkita parallel sikl ochadi. Har kadrda step ikki marta ishlaydi — animatsiya ikki baravar tezlashadi. stop esa faqat oxirgi id ni bekor qiladi, ikkinchi sikl "yetim" bo'lib aylanaveradi. Bu rAF bilan eng ko'p uchraydigan xato.
5.2 Callback ichidan to'xtatish
Ba'zan sikl o'zi to'xtashi kerak — hisoblagichdagi kabi. Buning ikki yo'li bor: yangi kadr so'ramaslik (hisoblagichda shunday qildik) yoki cancelAnimationFrame chaqirish. Birinchisi soddaroq: so'ralmagan kadrni bekor qilish shart emas.
Tekshirib ko'ring:
requestAnimationFrameikki marta ketma-ket chaqirildi:const a = requestAnimationFrame(f); const b = requestAnimationFrame(f);. Keyingi kadrdafnecha marta ishlaydi?cancelAnimationFrame(a)dan keyin-chi?
Javob
Ikki marta — ikkita alohida so'rov, ikkalasi ham shu kadrga tushadi va bir xil vaqt belgisini oladi. Agar cancelAnimationFrame(a) chaqirilsa — bir marta: faqat b qoladi. rAF "bitta kadrga bitta funksiya" degan cheklov qo'ymaydi — takrorlanishning oldini olish sizning vazifangiz.
6. Yashirin tabda rAF to'xtaydi
6.1 Brauzer batareyani tejaydi
Sahifa hayot sikli darsida aytgan edik: tab yashirilganda animatsiya kadrlari to'xtaydi. Endi sababini bilasiz. Yashirin tab ekranga chizilmaydi, demak kadr ham yo'q. Kadr yo'q — rAF callback'i ham yo'q. Kod hech narsa qilmasa ham, protsessor va batareya tejaladi.
setInterval esa yashirin tabda ham ishlashda davom etadi. Chrome uni bir muncha vaqtdan keyin sekinlatadi, lekin to'xtatmaydi. Ekranda hech kim ko'rmaydigan animatsiyani hisoblab, batareyani yeyaveradi.
Sinab ko'ring. Oynada ikki hisoblagich bor: rAF kadrlari va 16 ms li setInterval chaqiruvlari. Boshqa tabga o'ting, 3–4 soniyadan keyin qayting:
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
output { font: 700 1.5rem/1.3 ui-monospace, monospace; }
</style>
<p>rAF kadrlari: <output id="frames">0</output></p>
<p>setInterval chaqiruvlari: <output id="ticks">0</output></p>
<script>
const framesOut = document.getElementById("frames");
const ticksOut = document.getElementById("ticks");
let frames = 0;
let ticks = 0;
let hiddenAt = null;
function step() {
frames++;
framesOut.textContent = frames;
requestAnimationFrame(step);
}
requestAnimationFrame(step);
setInterval(() => {
ticks++;
ticksOut.textContent = ticks;
}, 16);
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
hiddenAt = { frames, ticks };
} else if (hiddenAt) {
console.log(`Yashirin paytda: rAF +${frames - hiddenAt.frames},`
+ ` setInterval +${ticks - hiddenAt.ticks}`);
}
});
</script>Qaytganingizda konsolga shunga o'xshash qator chiqadi: Yashirin paytda: rAF +0, setInterval +190. rAF nol — tab yashirin paytda bitta ham kadr bo'lmadi. Taymer esa ishlashda davom etdi. Biz Chrome 154 da (boshqa tabni oldinga chiqarib) tekshirdik: 2 soniyada rAF +0, setInterval +125. Aniq son kompyuterga bog'liq — +0 esa doim.
6.2 Bu nimani anglatadi?
Ikki xulosa:
- rAF — faqat ko'rinadigan narsa uchun. Ma'lumotni saqlash, serverga yuborish, taymerli eslatma kabi ishlarni rAF'ga qo'ymang — tab yashirinsa, ular bajarilmaydi.
- Tab qaytganda birinchi kadrda
dtkatta bo'ladi (yuqoridagi sinovda — bir necha soniya). Shuning uchun "Delta time" bo'limidagi maslahat:dtni cheklang.
Tekshirib ko'ring: Sardor bron formasidagi izohni har 2 soniyada serverga saqlamoqchi. U vaqtni rAF siklida sanab, 2 soniya o'tganda yuboradi. Mehmon boshqa tabga o'tib, 1 daqiqadan keyin qaytdi. Shu daqiqada nechta saqlash bo'ldi?
Javob
Birorta ham. Tab yashirin paytda kadr yo'q, demak rAF callback'i ham, uning ichidagi sanash ham yo'q. Mehmon tabdan chiqishidan oldin yozgan oxirgi so'zlar shu daqiqa davomida saqlanmay turdi — u shu orada brauzerni yopsa, ular yo'qolardi. Bunday ish uchun taymer kerak (setTimeout yoki setInterval) — yoki keyingi darsdagi debounce.
7. O'qish va yozishni kadrlarga ajratish
7.1 Va'da: layout thrashing'ga qarshi
Ko'p elementni samarali chizish darsida layout thrashing ni ko'rgan edik: DOM'ga yozish va o'lchamni o'qish navbatma-navbat kelsa, brauzer har safar layout'ni qayta hisoblaydi. Yechim — avval hamma o'qishlar, keyin hamma yozishlar. O'shanda "kadrlar bo'yicha ajratadigan maxsus asbob ham bor" degan edik. Bu — rAF.
Murakkab sahifada o'qish va yozish turli modullarda tarqalgan bo'ladi. Ularni qo'lda tartiblash qiyin. Usul: yozishlarni rAF'ga qo'yish. Hozir hamma o'qishlar sinxron bajariladi, yozishlar esa keyingi kadr boshida — bitta guruh bo'lib.
Misol: menyu kartalari bir qatorda turibdi, lekin balandligi har xil. Ularni eng balandi bo'yicha tenglashtiramiz. Buning uchun avval hamma balandliklarni o'qish, keyin hamma kartaga yangi balandlikni yozish kerak:
const cards = document.querySelectorAll(".menu-card");
// O'qish — hozir, hammasi birga (layout bir marta)
const heights = [...cards].map((card) => card.offsetHeight);
const tallest = Math.max(...heights);
// Yozish — keyingi kadr boshida, hammasi birga
requestAnimationFrame(() => {
for (const card of cards) {
card.style.minHeight = `${tallest}px`;
}
});Bu yerda o'qish va yozish ikki xil vaqtga ajraldi. Yozish callback'i kadr boshida ishlaydi, undan keyin brauzer layout'ni bir marta hisoblaydi va chizadi. Bu usulni katta kutubxonalar ham ishlatadi. Rendering konveyeri va thrashing'ni o'lchash bilan to'liq Rendering pipeline va layout thrashing darsida, shu qismda ko'ramiz.
Diqqat: rAF callback'ida o'qish ham qilsangiz va undan oldin shu callback'da yozgan bo'lsangiz — thrashing qaytadi. rAF sehrli emas: u faqat "qachon" degan savolga javob beradi. Ichida ham "avval o'qish, keyin yozish" qoidasi amal qiladi.
8. Qachon rAF kerak emas
rAF har bir harakat uchun emas. Ko'p holatda CSS yaxshiroq:
| Vazifa | Eng yaxshi vosita |
|---|---|
| Hover, ochilish, oddiy o'tish | CSS transition |
| Takrorlanuvchi bezak (yuklanish belgisi) | CSS @keyframes |
| JS'dan boshqariladigan, lekin oddiy animatsiya | Web Animations API (shu qismda) |
Har kadrda hisoblanadigan qiymat: hisoblagich, fizika, o'yin, <canvas> |
requestAnimationFrame |
CSS animatsiyalari (transform va opacity bo'lsa) ko'pincha kompozitor thread'ida ishlaydi. Kompozitor — tayyor qatlamlarni yig'ib ekranga chiqaradigan alohida ishchi (Rendering pipeline darsidagi "yig'ish" qadami). Shuning uchun asosiy thread band bo'lsa ham bunday animatsiya silliq qoladi. rAF esa asosiy thread'da: JavaScript 200 ms band bo'lsa, rAF animatsiyasi ham 200 ms to'xtaydi. Shuning uchun rAF faqat qiymatni har kadrda JavaScript hisoblashi kerak bo'lganda tanlanadi.
Yana bir muhim narsa. Ba'zi odamlarda harakat bosh aylanishi yoki ko'ngil aynishini qo'zg'atadi. Ular tizim sozlamalarida "harakatni kamaytirish"ni yoqishadi. Yaxshi sayt buni hurmat qiladi: bunday foydalanuvchiga hisoblagich animatsiyasiz, darhol yakuniy raqamni ko'rsatadi. JavaScript'dan bu sozlamani tekshirish — matchMedia va foydalanuvchi sozlamalari darsida, to'rt dars keyin.
9. Node'da rAF yo'q
rAF — brauzerning imkoniyati, JavaScript tilining emas. Node'da ekran yo'q, kadr yo'q, demak rAF ham yo'q:
requestAnimationFrame(() => console.log("kadr"));Node 24 da:
ReferenceError: requestAnimationFrame is not definedTarjimasi: "requestAnimationFrame aniqlanmagan". Node bu nomni hech qayerdan topa olmadi. Shuning uchun bu darsdagi rAF misollari jonli natija oynalarida berilgan. Qisqa kod parchalari ostida esa «Ishga tushir» tugmasi yo'q — ular faqat brauzerda ishlaydi. Server tomonidagi kodda (24-qismda) animatsiya bo'lmaydi.
10. Ko'p uchraydigan xatolar
10.1 Keyingi kadrni so'ramaslik
// ❌ faqat bitta kadr — keyin hech narsa
function step() {
box.style.transform = `translateX(${x++}px)`;
}
requestAnimationFrame(step);Element bir piksel siljiydi va to'xtaydi. rAF bir martalik. Tuzatish: step ichida requestAnimationFrame(step) ni chaqiring.
10.2 Funksiyani chaqirib yuborish
// ❌ step() — chaqiruv natijasi (undefined) beriladi
requestAnimationFrame(step());Bu Taymerlar darsidagi tanish xato: step() darhol bajariladi va rAF'ga undefined ketadi. Chrome konsolida:
Uncaught TypeError: Failed to execute 'requestAnimationFrame' on 'Window': parameter 1 is not of type 'Function'.Tarjimasi: "requestAnimationFrame ni bajarib bo'lmadi: 1-parametr funksiya emas". Tuzatish: qavssiz — requestAnimationFrame(step).
10.3 Ikkita parallel sikl
«Boshlash» tugmasi bosilganda tekshiruvsiz requestAnimationFrame(step). Ikki bosish — ikki sikl, animatsiya ikki baravar tez va to'xtamaydi. Tuzatish: frameId bilan "ishlayaptimi?" tekshiruvi ("To'xtatish" bo'limidagi kod).
10.4 Harakatni kadrlar soniga bog'lash
"Har kadrda 3 px" — 120 Hz telefonda ikki baravar tez, sekin kompyuterda sekin. Tuzatish: tezlikni soniyaga bering va dt ga ko'paytiring.
10.5 rAF ichida og'ir ish
rAF callback'i kadr byudjetining bir qismi. Unda 30 ms hisob qilsangiz, 60 Hz da har kadr tushib qoladi. Tuzatish: rAF'da faqat DOM'ga yozish va yengil hisob. Og'ir ish — oldindan yoki boshqa joyda (Web Workers, shu qismda).
11. Mashqlar
1-mashq (oson): Bron progress chizig'i
«Bahor»da bron yuborilganda 2 soniyalik progress chizig'i to'lsin: <div id="bar"> ning width i 0% dan 100% gacha. "Bugungi buyurtmalar" misolidagi naqshni ishlating: startTime, progress = Math.min(o'tgan / 2000, 1), progress < 1 bo'lsa keyingi kadr.
Ishora: foizni `${progress * 100}%` ko'rinishida yozing. Oxirida konsolga "Bron yuborildi" chiqsin.
Yechim
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
.track { height: 0.75rem; background: #eef3ee;
border-radius: 1rem; }
#bar { height: 100%; width: 0; background: #2e7d32;
border-radius: 1rem; }
</style>
<p>Bron yuborilmoqda…</p>
<div class="track"><div id="bar"></div></div>
<script>
const bar = document.getElementById("bar");
const duration = 2000;
let startTime = null;
function step(time) {
if (startTime === null) startTime = time;
const progress = Math.min((time - startTime) / duration, 1);
bar.style.width = `${progress * 100}%`;
if (progress < 1) {
requestAnimationFrame(step);
} else {
console.log("Bron yuborildi");
}
}
requestAnimationFrame(step);
</script>Konsolda:
Bron yuborildiwidth — layout'ni uyg'otadigan xususiyat. Bitta kichik chiziq uchun bu muammo emas. Katta elementlarda transform: scaleX(progress) arzonroq (Rendering pipeline).
2-mashq (o'rta): Boshlash va to'xtatish
Kuryer (#courier) yo'l bo'ylab soniyasiga 120 px yursin (delta time bilan), 300 px dan keyin boshiga qaytsin. Ikki tugma: «Boshlash» va «To'xtatish». Talablar:
- «Boshlash» ni ketma-ket ikki marta bosish tezlikni oshirmasin.
- To'xtatib qayta boshlaganda kuryer sakramasin (qayta boshlashda
previousninullqiling).
Ishora: "To'xtatish" bo'limidagi frameId naqshi va "Delta time" bo'limidagi dt hisobi.
Yechim
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
.road { position: relative; height: 2rem; background: #eef3ee;
border-radius: 1rem; overflow: hidden; margin-bottom: 0.5rem; }
#courier { position: absolute; top: 0.25rem; width: 1.5rem;
height: 1.5rem; border-radius: 50%; background: #2e7d32; }
button { font: inherit; padding: 0.4rem 0.8rem; }
</style>
<div class="road"><div id="courier"></div></div>
<button id="start" type="button">Boshlash</button>
<button id="stop" type="button">To'xtatish</button>
<script>
const courier = document.getElementById("courier");
const speed = 120; // px soniyasiga
let x = 0;
let previous = null;
let frameId = null;
function step(time) {
if (previous === null) previous = time;
const dt = Math.min((time - previous) / 1000, 0.1);
previous = time;
x = (x + speed * dt) % 300;
courier.style.transform = `translateX(${x}px)`;
frameId = requestAnimationFrame(step);
}
document.getElementById("start").addEventListener("click", () => {
if (frameId !== null) return;
previous = null;
frameId = requestAnimationFrame(step);
});
document.getElementById("stop").addEventListener("click", () => {
cancelAnimationFrame(frameId);
frameId = null;
});
</script>frameId !== null — "sikl allaqachon ishlayapti" belgisi. previous = null bo'lmasa, to'xtab turgan vaqt (masalan 5 soniya) birinchi dt ga tushardi. Biz uni yana Math.min(..., 0.1) bilan ham chekladik — tab yashirinib qaytganda ham kuryer sakramaydi.
3-mashq (qiyin): Qayta ishlatiladigan animateValue
animateValue({ from, to, duration, onUpdate }) funksiyasini yozing. U har kadrda onUpdate(qiymat) ni chaqirsin, qiymat from dan to gacha ease-out bilan o'zgarsin: eased = 1 - (1 - progress) ** 3. Funksiya Promise qaytarsin — animatsiya tugaganda bajariladi. Uni hisoblagichda ishlating: 0 → 1 284, 1 soniya, tugagach konsolga "Tayyor: 1284".
Ishora: new Promise((resolve) => { ... }) ichida kadr siklini boshlang, progress === 1 bo'lganda resolve(to).
Yechim
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
#counter { font: 700 3rem/1.2 ui-monospace, monospace; margin: 0; }
</style>
<p id="counter">0</p>
<script>
function animateValue({ from, to, duration, onUpdate }) {
return new Promise((resolve) => {
let startTime = null;
function step(time) {
if (startTime === null) startTime = time;
const progress = Math.min((time - startTime) / duration, 1);
const eased = 1 - (1 - progress) ** 3;
onUpdate(from + (to - from) * eased);
if (progress < 1) {
requestAnimationFrame(step);
} else {
resolve(to);
}
}
requestAnimationFrame(step);
});
}
const counter = document.getElementById("counter");
animateValue({
from: 0,
to: 1284,
duration: 1000,
onUpdate: (value) => {
counter.textContent = Math.round(value);
},
}).then((result) => console.log(`Tayyor: ${result}`));
</script>Konsolda:
Tayyor: 1284Ease-out bilan raqam boshida tez o'sadi va oxirida sekinlashadi: yarim soniyada u allaqachon taxminan 1 124 da (1 - 0.5³ = 0.875). Promise tufayli animatsiyadan keyingi ishni then yoki await bilan yozish oson — masalan, "animatsiya tugagach tugmani yoqish".
4-mashq: Amaliy tajriba — vazifalar hisobi silliq o'zgarsin
vazifalar dagi #hisob (masalan, "3 ta vazifa") qo'shish va o'chirishda birdan o'zgaradi. Kanon kodga tegmasdan, mashqlar papkasida tajriba qiling: kurs/mashqlar/13/01-raf/index.html — bitta <p id="hisob"> va ikki tugma («+1», «−1»). Son o'zgarganda u eski qiymatdan yangisiga 300 ms da silliq o'tsin. 3-mashqdagi animateValue ni ishlating. Live Server bilan oching.
Talab: animatsiya tugamasdan yana bosilsa, eski animatsiya to'xtashi kerak (aks holda ikki sikl bir elementga yozib, son "titraydi").
Yechim
animateValue ga to'xtatish imkonini qo'shamiz: u Promise bilan birga cancel funksiyasini qaytaradi.
<p id="hisob">0 ta vazifa</p>
<button id="plus" type="button">+1</button>
<button id="minus" type="button">−1</button>
<script>
function animateValue({ from, to, duration, onUpdate }) {
let frameId = null;
const done = new Promise((resolve) => {
let startTime = null;
function step(time) {
if (startTime === null) startTime = time;
const p = Math.min((time - startTime) / duration, 1);
onUpdate(from + (to - from) * (1 - (1 - p) ** 3));
if (p < 1) frameId = requestAnimationFrame(step);
else resolve(to);
}
frameId = requestAnimationFrame(step);
});
return { done, cancel: () => cancelAnimationFrame(frameId) };
}
const hisob = document.getElementById("hisob");
let shown = 0; // ekrandagi (oraliq) qiymat
let count = 0; // haqiqiy son
let current = null;
function changeBy(delta) {
count = Math.max(0, count + delta);
current?.cancel();
current = animateValue({
from: shown,
to: count,
duration: 300,
onUpdate: (value) => {
shown = value;
hisob.textContent = `${Math.round(value)} ta vazifa`;
},
});
}
document.getElementById("plus")
.addEventListener("click", () => changeBy(1));
document.getElementById("minus")
.addEventListener("click", () => changeBy(-1));
</script>Yangi animatsiya shown dan — ekrandagi oraliq qiymatdan boshlanadi, shuning uchun tez-tez bosganda ham son sakramaydi. Qator current?.cancel() eski siklni to'xtatadi.
git add 13/01-raf/index.html
git commit -m "13/01: hisobni rAF bilan silliq o'zgartirish"12. Real ishda
- Hisoblagich va grafiklar. Dashboard'lardagi "jami savdo" raqamlari, grafik chiziqlarining chizilib chiqishi — rAF kadr sikli. Chart.js kabi grafik kutubxonalari ichida aynan shu naqsh.
- O'yinlar va
<canvas>. Brauzer o'yinlarining "o'yin sikli" — rAF + delta time. Uni Canvas amaliyotda darsida ishlatamiz. - Kutubxonalar. GSAP, Motion kabi animatsiya kutubxonalari ichida rAF ishlaydi: ular delta time'ni, to'xtatishni va easing'ni (biz qilgan "egish" —
ease-outkabi tezlik egri chizig'i) siz uchun boshqaradi. React'dagi silliq harakat kutubxonalari ham (React — 17-qismda). - Intervyu. "
requestAnimationFramevasetTimeoutfarqi?", "Animatsiyani qanday 60 FPS'da ushlaysiz?", "Delta time nima?" — frontend intervyularida tez-tez so'raladi.
Xulosa
requestAnimationFrame(fn)— "keyingi kadr chizilishidan oldinfnni bir marta chaqir". Sikl uchunfno'zini qayta so'raydi.- Callback'ga kadr boshlangan vaqt keladi; animatsiya shu vaqtga bog'lanadi: progress = o'tgan vaqt / davomiylik.
- Harakatni delta time bilan hisoblang:
x += tezlik × dt. "Har kadrda N px" — ekran va kompyuter tezligiga bog'liq. cancelAnimationFrame(id)— oxirgi so'rovni bekor qiladi; ikkita parallel sikl ochilmasligini tekshiring.- Yashirin tabda rAF umuman chaqirilmaydi,
setIntervalesa ishlashda davom etadi. - Oddiy o'tishlar — CSS; har kadrda JS hisoblaydigan qiymat — rAF.
Keyingi dars: Debounce va throttle — tez-tez keladigan hodisalarni (yozish, scroll, resize) "tinchlantirish" va "sekinlatish": har harfga emas, kerakli paytda bir marta ishlash.
Manbalar
- HTML Living Standard: "Animation frames", "Update the rendering" — html.spec.whatwg.org
- MDN:
Window.requestAnimationFrame(),Window.cancelAnimationFrame()— developer.mozilla.org - web.dev: "Optimize JavaScript execution" (rAF bilan vizual o'zgarishlar) — web.dev
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!