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

Rendering pipeline va layout thrashing: JavaScript brauzerni qachon majburan "o'lchatadi"

Qisqacha: Brauzer DOM o'zgarishini darhol hisoblamaydi — "iflos" deb belgilab, kadr chizishdan oldin bir marta style → layout → paint → composite qiladi. Lekin JavaScript o'zgarishdan keyin o'lcham o'qisa (offsetHeight, getBoundingClientRect(), innerText), brauzer layout'ni shu zahoti qilishga majbur bo'ladi — bu majburiy reflow. Sikl ichida "yoz — o'qi — yoz — o'qi" — layout thrashing: 500 ta kartada 500 ta layout. Davosi: avval hamma o'qishlar, keyin hamma yozishlar; animatsiyada transform va opacity.

Bu darsda

  • Brauzer konveyerining to'rt bosqichini JavaScript nuqtai nazaridan tushuntira olasiz: qaysi o'zgarish qaysi bosqichni uyg'otadi.
  • Qaysi o'qishlar layout'ni majburlashini bilasiz va ro'yxatni o'zingiz tekshira olasiz.
  • Layout thrashing'ni Chrome'da o'lchab, Performance yozuvida topa olasiz.
  • O'qish va yozishni guruhlash, requestAnimationFrame, kuzatuvchilar va transform bilan uni tuzata olasiz.

Oldin bilishingiz kerak: Rendering pipeline va animatsiya samaradorligi, Ko'p elementni samarali chizish, Element o'lchamlari va scroll, requestAnimationFrame, Performansni o'lchash.

1. Nega bu kerak?

Web Components bilan «Bahor» menyusi chiroyli bo'ldi. Endi Jasur aka yangi narsa so'radi: menyu sahifasida kartalar bir qatorda turganda, balandligi har xil bo'lib qolyapti — biri qisqa tavsifli, biri uzun. "Hammasi bir xil balandlikda bo'lsin".

Sardor tez yozdi: har bir kartaga eng baland kartaning balandligini beradigan funksiya. Kompyuterida ishladi. Jasur akaning arzon telefonida esa «Tenglash» bosilganda sahifa yarim soniya qotib qoldi. Kartalar 500 ta ham emas edi.

Kod "to'g'ri" edi. Muammo — u brauzerni qanday ishlatganida. Sardorning siklida har kartaga yozish va o'lcham o'qish navbatma-navbat kelardi. Har o'qishda brauzer butun sahifaning joylashuvini qaytadan hisoblashga majbur bo'ldi.

Bu hodisani oldin ham uchratdik: Renderni nima to'xtatadi darsida qoida sifatida, Ko'p elementni samarali chizish darsida birinchi o'lchov bilan, requestAnimationFrame darsida esa "yozishni kadrga qo'yish" usuli bilan. Bugun hammasini bir joyga yig'amiz: brauzer ichida aniq nima bo'ladi, qaysi o'qishlar xavfli, buni qanday o'lchaymiz va qanday tuzatamiz.

2. Konveyer — JavaScript nuqtai nazaridan

2.1 Besh qadam

Rendering pipeline darsida konveyerni CSS tomonidan ko'rdik. JavaScript bilan birga u besh qadamdan iborat:

flowchart LR
  J["JavaScript<br/>DOM'ni o'zgartiradi"] --> S["Style<br/>qaysi qoida qayerga"]
  S --> L["Layout<br/>o'lcham va joy"]
  L --> P["Paint<br/>piksellar ro'yxati"]
  P --> C["Composite<br/>qatlamlarni yig'ish"]
  • JavaScript — skriptingiz DOM'ni yoki stilni o'zgartiradi: classList.add, style.width, append.
  • Style (uslub hisobi) — brauzer har bir element uchun qaysi CSS qoidalari ishlashini va yakuniy qiymatlarni hisoblaydi. Chrome yozuvlarida bu bosqich Recalculate Style deb ataladi.
  • Layout (joylashtirish) — har elementning o'lchami va sahifadagi joyi. Uning eski nomi — reflow ("qayta oqim"). Ikkalasi bir narsa.
  • Paint (bo'yash) — nimani qanday chizish kerakligi ro'yxati: matn, rang, soya. Eski nomi — repaint.
  • Composite (yig'ish) — tayyor qatlamlarni ekranda bir-birining ustiga qo'yish. Ko'pincha videokarta bajaradi.

Bosqich nomlarini inglizcha eslab qoling: DevTools'da ular aynan shunday yoziladi.

2.2 Brauzer dangasa — va bu yaxshi

Muhim g'oya: card.style.width = "301px" yozilganda brauzer hech narsani hisoblamaydi. U faqat belgi qo'yadi: "bu element uslubi eskirdi, layout ham eskirdi". Bu holat iflos (dirty) deyiladi.

Keyin skript ishlashda davom etadi. 500 ta karta o'zgarsa, 500 ta belgi qo'yiladi. Skript tugab, kadr chizish vaqti kelganda brauzer bir marta style va layout qiladi — hamma o'zgarish uchun birdaniga. Xuddi ofitsiant buyurtmalarni bittalab oshxonaga yugurib olib bormasdan, stoldagi hammaning buyurtmasini yozib olib, bir marta borishi kabi.

2.3 Qaysi o'zgarish qaysi bosqichni uyg'otadi

Konveyer zanjir: oldingi bosqich ishlasa, keyingilari ham ishlaydi. Shuning uchun o'zgarishning narxi u qaysi bosqichdan boshlashiga bog'liq:

JS'da o'zgartirdingiz Ishlaydigan bosqichlar Narxi
width, height, top, left, font-size, matn, element qo'shish Style → Layout → Paint → Composite eng qimmat
color, background, box-shadow Style → Paint → Composite o'rtacha
transform, opacity Style → Composite eng arzon

Buni JavaScript animatsiyasida o'lchadik. Bir soniyalik rAF animatsiyasi doirani 300 px suradi: bir marta style.left bilan, bir marta style.transform bilan.

O'lchov uchun Performance yozuvi (trace) oldik. Bu — brauzer bir necha soniya ichida nima qilganining vaqt bo'yicha batafsil jurnali: qaysi skript, qaysi Layout, qaysi Paint, qachon. Xuddi samolyotning parvoz jurnali kabi. DevTools'ning Performance paneli aynan shu jurnalni chizib ko'rsatadi. Chrome 154 dagi natija:

  • left bilan — har kadrda bitta Layout va ikkita Paint.
  • transform bilan — butun animatsiya davomida bitta Layout va ikkita Paint. Qolgan hamma kadrlarda faqat Style va Composite.

Kadrlar soni sinovda soniyasiga 100 dan ortiq edi, aniq soni ekranga bog'liq. Farq esa doim bir xil: left — har kadr layout, transform — deyarli hech qachon.

Tekshirib ko'ring: Kartaga sichqoncha kelganda JavaScript uning background rangini o'zgartiradi. Konveyerning qaysi bosqichlari ishlaydi? box-shadow qo'shilsa-chi? margin-top: 4px qo'shilsa-chi?

Javob

background — Style, Paint, Composite (layout yo'q: o'lcham o'zgarmadi). box-shadow — xuddi shunday: soya joy egallamaydi, faqat chiziladi. margin-top esa o'lchamga ta'sir qiladi — kartaning o'zi va undan keyingi hamma narsa suriladi. Unda Layout ham ishlaydi, eng qimmat yo'l.

3. Majburiy reflow

3.1 O'qish dangasalikni buzadi

Endi asosiy tuzoq. Brauzer o'zgarishlarni yig'ib, keyin bir marta hisoblamoqchi edi. Lekin skript yozgandan keyin o'lcham so'raydi:

js
card.style.width = "301px";     // yozish — layout "iflos"
const h = card.offsetHeight;    // o'qish — javob hozir kerak!

offsetHeight ning to'g'ri javobi yangi layout'ga bog'liq: karta torayganda matn qayta o'raladi, balandlik o'zgaradi. Eski qiymatni berib bo'lmaydi. Brauzerda bitta yo'l qoladi: kadr oxirini kutmasdan, shu zahoti style va layout qilish. Skript bu vaqtda to'xtab turadi.

Bu majburiy sinxron layout (forced synchronous layout) yoki qisqacha majburiy reflow (forced reflow) deyiladi. Bitta majburiy reflow — muammo emas. Muammo — ularning ko'pligi.

3.2 Thrashing: sikl ichida

js
// ❌ har kartada: yoz → o'qi → yoz → o'qi …
for (const card of cards) {
  card.style.width = `${newWidth}px`;
  card.dataset.height = card.offsetHeight;
}

Birinchi aylanish: yozish → layout iflos → o'qish → majburiy layout. Ikkinchi aylanish: yozish → layout yana iflos → o'qish → yana majburiy layout. 500 ta karta — 500 ta to'liq layout. Bu layout thrashing ("layout'ni titratish").

sequenceDiagram
  participant JS as Skript
  participant B as Brauzer
  JS->>B: 1-karta: width yoz
  JS->>B: 1-karta: offsetHeight?
  B->>B: Style + Layout (majburiy)
  B-->>JS: 48
  JS->>B: 2-karta: width yoz
  JS->>B: 2-karta: offsetHeight?
  B->>B: Style + Layout (yana)
  Note over JS,B: … 500 marta

3.3 Qaysi o'qishlar layout'ni majburlaydi

Har o'qish ham xavfli emas. Biz Chrome 154 da tekshirdik: sikl ichida 50 ta kartaning kengligini o'zgartirib, keyin bitta xususiyatni o'qidik va brauzerning ichki hisoblagichlarini (LayoutCount, RecalcStyleCount) sanadik:

O'qish Majburiy layout (50 dan)
offsetHeight, offsetTop, clientWidth 50
getBoundingClientRect() 50
scrollTop, window.scrollY 50
innerText 50
getComputedStyle(el).height 50
element.focus(), document.elementFromPoint() 50
getComputedStyle(el).color 0 (faqat style)
textContent, className, dataset 0

Ikki narsaga e'tibor bering.

getComputedStyle ikki xil. Rang so'ralsa — brauzerga faqat style kerak, layout emas. Balandlik so'ralsa — layout ham. classList va stillar darsida "getComputedStyle ba'zan sahifani qayta o'lchaydi" degan edik — mana aniq javob: o'lchamga bog'liq xususiyat so'ralganda.

element.focus() — o'qish emas, lekin u ham layout so'raydi: fokus olgan elementni ko'rinadigan joyga aylantirish uchun brauzer uning joyini bilishi kerak.

To'liq ro'yxatni Paul Irish'ning "What forces layout / reflow" maqolasida topasiz — dasturchilar uni ko'p yillardan beri ma'lumotnoma sifatida ishlatadi.

3.4 Iflos bo'lmasa — majburlash ham yo'q

Majburiy layout faqat layout iflos bo'lganda yuz beradi. Buni ikki sinov bilan tekshirdik:

  • Sikl ichida faqat rang klassini almashtirib (classList.toggle("hot")), keyin offsetHeight o'qidik. Natija: 500 ta majburiy style, lekin 0 ta layout. Rang o'lchamga ta'sir qilmaydi — layout iflos bo'lmadi, brauzer eski o'lchamni bemalol berdi. Bu ancha arzon: taxminan 5 ms.
  • Sikl boshida hech narsa yozmay, faqat o'qisak — layout bir marta ham majburlanmaydi: oldingi kadrdan qolgan toza layout ishlatiladi.

Demak, xavfli kombinatsiya aniq: layout'ga ta'sir qiladigan yozish + layout'ga bog'liq o'qish, navbatma-navbat.

Tekshirib ko'ring: Sikl ichida card.classList.add("tanlangan") (u faqat border-color ni o'zgartiradi) va card.getBoundingClientRect() keladi. Thrashing bormi?

Javob

Layout thrashing yo'q: border-color o'lchamga ta'sir qilmaydi, layout iflos bo'lmaydi. Lekin har aylanishda majburiy style hisobi bo'ladi — brauzer klass o'zgargach qaysi qoidalar ishlashini tekshirishi kerak. Bu arzonroq, lekin bepul emas. Eng yaxshisi baribir: avval hamma o'qishlar, keyin hamma yozishlar. Agar o'sha klass border-width ni o'zgartirsa — o'lcham o'zgaradi va to'liq thrashing boshlanadi.

4. O'lchaymiz

4.1 O'zingiz sinab ko'ring

Oynada 300 ta karta. «Aralash» — Sardorning sikli: yoz va o'qi navbatma-navbat. «Guruhlangan» — avval hamma balandliklarni o'qiydi, keyin hamma kengliklarni yozadi. Ikkalasi ham ish bir xil:

html
<style>
  body { font: 0.9rem/1.4 system-ui, sans-serif; margin: 1rem; }
  #list { height: 9rem; overflow: auto; border: 1px solid #ccc; }
  .card { border: 1px solid #c8d6c8; padding: 0.3rem;
    margin: 0.2rem; }
  button { font: inherit; padding: 0.3rem 0.7rem;
    margin: 0.5rem 0.2rem; }
</style>
<button id="mixed" type="button">Aralash</button>
<button id="batched" type="button">Guruhlangan</button>
<p id="result">Tugmani bosing.</p>
<div id="list"></div>
<script>
  const list = document.getElementById("list");
  for (let i = 1; i <= 300; i++) {
    const card = document.createElement("div");
    card.className = "card";
    card.textContent = `${i}-taom: guruch, sabzi, go'sht, ziravor`;
    list.append(card);
  }
  const cards = [...list.children];
  let width = 300;

  function mixed() {
    for (const card of cards) {
      card.style.width = `${width}px`;
      card.dataset.height = card.offsetHeight; // majburiy layout
    }
  }

  function batched() {
    const heights = cards.map((card) => card.offsetHeight);
    cards.forEach((card, i) => {
      card.style.width = `${width}px`;
      card.dataset.height = heights[i];
    });
  }

  function measure(name, fn) {
    width = width === 300 ? 299 : 300; // har safar haqiqiy o'zgarish
    const start = performance.now();
    fn();
    const ms = performance.now() - start;
    const text = `${name}: ${ms.toFixed(1)} ms`;
    document.getElementById("result").textContent = text;
    console.log(text);
  }

  document.getElementById("mixed")
    .addEventListener("click", () => measure("Aralash", mixed));
  document.getElementById("batched")
    .addEventListener("click", () => measure("Guruhlangan", batched));
</script>

Har tugmani bir necha marta bosing. «Aralash» o'nlab millisekund ko'rsatadi, «Guruhlangan» — bir millisekunddan kam. Aniq son kompyuteringizga bog'liq. Bizning sinovda (avtomatik, 5 marta) «Aralash» mediana taxminan 55 ms, «Guruhlangan» taxminan 1 ms chiqdi — farq bir necha o'n baravar.

Qatorma-qator muhimlari:

  • width === 300 ? 299 : 300 — har bosishda kenglik haqiqatan o'zgarsin. Bir xil qiymatni qayta yozsangiz, brauzer layout'ni iflos qilmaydi va o'lchov yolg'on chiqadi.
  • batched dagi heights — avval hamma o'qishlar, o'zgaruvchiga saqlab. Keyin faqat yozishlar. Sikl ichida hech qanday o'qish qolmadi.
  • measure funksiyasi faqat skript vaqtini o'lchaydi. «Guruhlangan» da yagona layout skript tugagandan keyin, kadr chizishda bo'ladi — o'lchovga kirmaydi. Lekin u bitta, 300 ta emas. Bu adolatli farq: foydalanuvchi uchun aynan shu muhim.

4.2 Avtomatik aniq o'lchov

Bir marta bosish — tasodifiy son. Performansni o'lchash darsidagi qoida: ko'p marta o'lchab, mediana olish. Biz 500 ta karta bilan har usulni 9 marta bajardik. Har o'lchov oldidan bitta kadr kutdik, shunda layout toza holatdan boshlandi. Brauzerning ichki hisoblagichlarini (LayoutCount — nechta layout bo'lgani) avtomatik sinov skriptida page.metrics() orqali oldik. Skript Chrome'ni Node'dan boshqaradi — bunday vositalarni E2E testlarda (22-qism) o'rganasiz:

js
// sinov skripti (Node 24) — asosiy qismi; page — boshqariladigan tab
const before = await page.metrics();
const ms = await page.evaluate(() => window.mixed());
const after = await page.metrics();
console.log(ms, after.LayoutCount - before.LayoutCount);

Natijalar (mediana, 9 o'lchov):

500 ta karta: bitta yangilash vaqti (mediana)
  • Aralash500 ta majburiy layout94,8 ms
  • Guruhlangan0 ta majburiy layout0,5 ms
  • innerText siklda500 ta majburiy layout92,6 ms
  • textContent siklda00,3 ms

Manba: O'lchov: Chrome 154 headless, Intel Core i5-12500H, Windows 11, 2026-10-05; har usul 9 marta, mediana

Nimaga qarang:

  • Aralash — mediana taxminan 95 ms; boshqa sinov kunlarida 95–130 ms orasida chiqdi. Har o'lchovda aniq 500 ta layout — har bir karta uchun bittadan.
  • Guruhlangan — yarim millisekund atrofida, majburiy layout 0. Farq 150 baravardan ortiq.
  • innerText va textContent — xuddi shu manzara. Sikl ichida kenglikni o'zgartirib, keyin innerText o'qisak — 500 ta layout. textContent — layout'siz.

Arzon telefonni taqlid qilish uchun Chrome'ning protsessorni 4 baravar sekinlashtirish rejimini yoqdik (DevTools'dagi "4x slowdown" bilan bir xil). Unda «Aralash» mediana 550–800 ms ga chiqdi, «Guruhlangan» — taxminan 5 ms. Yarim soniyadan ko'p qotish — Jasur akaning telefonidagi manzara aynan shu.

4.3 Performance yozuvida qanday ko'rinadi

Xuddi shu skript bilan Performance yozuvi ham oldik. «Aralash» bajarilganda trace'da 500 ta Layout hodisasi bor edi. Ularning har birida JavaScript stek izi bor: qaysi funksiyaning qaysi qatori layout'ni chaqirgani. «Guruhlangan» da esa bitta Layout, stek izisiz — u skriptdan keyin, brauzerning o'z navbatida bo'ldi.

DevTools'da xuddi shu narsani ko'rasiz:

  1. DevTools → Performance → yozishni boshlang → «Aralash» ni bosing → to'xtating.
  2. Main qatorida bitta uzun vazifa. Uning ichida yuzlab ingichka siyohrang Layout bloklari.
  3. Vazifa ustida qizil burchak. Pastdagi panelda Forced reflow ogohlantirishi va layout'ni chaqirgan funksiyaga havola — mixed qatori.

Rendering pipeline darsida "Forced reflow ogohlantirishini chaqiradigan kodni keyinroq yozamiz" degan edik — mana u. Performance panelini to'liq DevTools: Performance, Memory va Network darsida o'rganamiz.

5. Davolash usullari

5.1 Avval o'qish, keyin yozish

Eng oddiy va eng kuchli usul — guruhlash. «Bahor» kartalarini tenglash, to'g'ri yo'l bilan:

js
function equalizeCards(cards) {
  // 1) O'qish — hammasi birga (layout toza bo'lsa, bepul)
  const heights = cards.map((card) => card.offsetHeight);
  const max = Math.max(...heights);

  // 2) Yozish — hammasi birga (layout faqat iflos belgisini oladi)
  for (const card of cards) {
    card.style.minHeight = `${max}px`;
  }
}

Bitta nozik joy. Ko'p elementni samarali chizish darsida "avval yozing, keyin bir marta o'qing" degan edik. Renderni nima to'xtatadi darsida esa "avval hamma o'qishlar, keyin hamma yozishlar". Qarama-qarshi emasmi? Yo'q — asosiy qoida bitta: aralashtirmang. "Avval yozish, keyin bitta o'qish" — bitta majburiy layout. "Avval o'qish, keyin yozish" — nol majburiy layout: o'qishlar oldingi kadrdan qolgan toza layout'ni ishlatadi. Imkon bo'lsa, ikkinchisi yaxshiroq.

Aytgancha, kartalarni tenglash uchun JavaScript umuman kerak emas: CSS Grid'da bir qatordagi kartalar o'zi bir xil balandlikda bo'ladi. Eng yaxshi layout kodi — yozilmagan kod.

5.2 Yozishni keyingi kadrga qo'yish

Katta ilovada o'qish va yozish turli modullarda bo'ladi. Ularni qo'lda tartiblash qiyin. requestAnimationFrame darsidagi usul: o'qish hozir, yozish — keyingi kadr boshida:

js
const heights = cards.map((card) => card.offsetHeight); // hozir

requestAnimationFrame(() => {
  cards.forEach((card, i) => {
    card.dataset.height = heights[i];
    card.style.width = "50%";
  }); // keyingi kadr boshida — keyin bitta layout
});

Bu g'oyani tizimlashtirgan kutubxona bor — FastDOM. U ikki navbat yuritadi: "o'lchash" va "o'zgartirish". Kadr boshida avval hamma o'lchashlarni, keyin hamma o'zgartirishlarni bajaradi. Uning soddalashtirilgan versiyasini «Mini FastDOM» mashqida o'zingiz yozasiz.

Diqqat: rAF callback'i ichida ham qoida amal qiladi. Callback avval yozib, keyin o'qisa — majburiy reflow shu yerda. rAF faqat "qachon" degan savolga javob beradi, "qanday tartibda" degan savolni siz hal qilasiz.

5.3 O'lchamni so'ramang — xabar kuting

Ba'zan o'lchamni har safar o'qishning hojati yo'q. Brauzerning o'zi layout'dan keyin xabar beradi:

  • ResizeObserver — element o'lchami o'zgarganda, layout tugagach, yangi o'lchamni beradi. resize hodisasida offsetWidth o'qishning o'rniga.
  • IntersectionObserver — element ko'rinish maydoniga kirganini aytadi. scroll hodisasida getBoundingClientRect() o'qishning o'rniga.

Ikkalasi ham callback'ni tayyor raqamlar bilan, layout'ni majburlamasdan chaqiradi. Scroll paytida har kadrda o'lcham o'qish — thrashing'ning eng keng tarqalgan manbalaridan biri.

O'zgarmaydigan o'lchamni esa bir marta o'qib, keshlang (o'zgaruvchiga saqlang). Masalan, sarlavha balandligi sahifa davomida o'zgarmasa — uni har scroll da qayta o'qish shart emas.

5.4 Animatsiyada — transform va opacity

JavaScript'dan harakat qilsangiz, left/top/width o'rniga transform ishlating. «Qaysi o'zgarish qaysi bosqichni uyg'otadi» bo'limidagi o'lchovni eslang: left har kadrda layout, transform — butun animatsiyada bitta.

Ko'p xususiyatni birma-bir yozish o'rniga bitta klass almashtirish ham foydali: card.classList.add("ochiq"). Brauzer uchun farq kichik (yozishlar baribir yig'iladi), lekin kod tozaroq va o'qish bilan aralashib ketish ehtimoli kam.

5.5 will-change — JavaScript qo'yadi va olib tashlaydi

Rendering pipeline darsida will-change ni ko'rdik: "brauzer, bu element tez orada harakatlanadi — oldindan alohida qatlam tayyorla". O'shanda "animatsiya tugagach o'chirish — JavaScript ishi" degan edik. Mana u:

js
const panel = document.querySelector(".savat-panel");

function openPanel() {
  panel.style.willChange = "transform"; // oldindan ogohlantirish
  requestAnimationFrame(() => {
    panel.classList.add("ochiq"); // CSS transition: transform
  });
}

panel.addEventListener("transitionend", () => {
  panel.style.willChange = ""; // tugadi — qatlam xotirasi bo'shaydi
});

Har bir qatlam videokarta xotirasini egallaydi. will-change ni doimiy qoldirish — xotirani behuda band qilish. Qoida: harakatdan oldin qo'ying, harakatdan keyin olib tashlang.

5.6 Layout maydonini kichraytirish

Thrashing'ni to'liq yo'q qilib bo'lmasa, har layout'ni arzonlashtirish mumkin. Rendering pipeline darsidagi contain: layout va content-visibility: auto brauzerga "bu blok ichidagi o'zgarish tashqariga ta'sir qilmaydi" yoki "ko'rinmaydigan qismni hisoblama" deydi. Shunda bitta karta o'zgarganda butun sahifa emas, kichik qism qayta hisoblanadi. Bu davolash emas, yengillashtirish — avval baribir o'qish va yozishni ajrating.

Tekshirib ko'ring: «Bahor» sahifasida har scroll hodisasida kod sarlavhaning offsetHeight ini o'qiydi, keyin menyu paneliga style.top yozadi. Sarlavha balandligi sahifa davomida o'zgarmaydi. Qaysi ikki usul bilan bu kodni yengillashtirasiz?

Javob

Birinchisi — keshlash: sarlavha balandligi o'zgarmaydi, uni sahifa ochilganda bir marta o'qib, o'zgaruvchiga saqlang. Shunda scroll paytida o'qish umuman qolmaydi. Ikkinchisi — yozishni rAF'ga qo'yish: bitta kadrda bir nechta scroll hodisasi kelsa ham, style.top kadr boshida bir marta yoziladi. Agar maqsad "sarlavha ko'rinmay qoldimi?" bo'lsa, IntersectionObserver o'lcham o'qishni butunlay olib tashlaydi.

6. Ko'p uchraydigan xatolar

6.1 Sikl ichida o'lcham o'qish

Eng klassik xato. Belgisi: kod to'g'ri ishlaydi, lekin element ko'paygan sari sahifa chiziqli emas, keskin sekinlashadi. Tuzatish: o'qishlarni sikldan oldin bitta massivga yig'ing.

6.2 Yashirin o'qishlar

innerText, focus(), scrollIntoView(), window.scrollY — "o'lcham" degan so'zi yo'q, lekin layout so'raydi. Matn kerak bo'lsa, innerText o'rniga textContent (Kontentni o'zgartirish darsidagi va'da: innerText nega sekinroq — mana sababi). Tuzatish: jadvaldagi ro'yxatni yodda tuting, shubha bo'lsa — Performance panelida tekshiring.

6.3 Funksiya ichida yashiringan o'qish

js
// ❌ sikl o'zi toza ko'rinadi …
for (const card of cards) {
  card.style.width = "50%";
  updateBadge(card); // … lekin ichida offsetWidth o'qiladi
}

Sikl ichidagi har funksiya chaqiruvi o'qish qilishi mumkin. Bu ko'pincha boshqa dasturchi yozgan yoki kutubxona funksiyasi bo'ladi. Tuzatish: Performance panelidagi Forced reflow havolasi aynan shu funksiyani ko'rsatadi.

6.4 Bir martalik o'lchovga ishonish

Bitta performance.now() o'lchovi — tasodif. Kompyuter boshqa ish bilan band bo'lishi, birinchi ishga tushirish sekinroq bo'lishi mumkin. Tuzatish: 5+ marta, mediana. Va albatta sekinlashtirilgan protsessor bilan ham — foydalanuvchilaringizning ko'pi siznikidan kuchsizroq telefonda.

6.5 Har narsani transform ga aylantirish

transform bilan surilgan element layout'da eski joyida qoladi — qo'shnilari surilmaydi. Ro'yxatdagi element balandligi haqiqatan o'zgarishi kerak bo'lsa (masalan, akkordeon ochilishi), transform: scaleY() matnni cho'zib yuboradi. Tuzatish: layout o'zgarishi kerak bo'lgan joyda layout o'zgaradi — faqat uni sikl ichida o'qish bilan aralashtirmang.

7. Mashqlar

1-mashq (oson): Thrashing qaysi birida?

Har kod uchun ayting: sikl ichida majburiy layout bormi? Nega?

js
// A
for (const card of cards) {
  card.classList.add("narx-yangi"); // faqat color o'zgaradi
  total += card.textContent.length;
}

// B
for (const card of cards) {
  card.style.padding = "12px";
  total += card.getBoundingClientRect().height;
}

// C
const rects = cards.map((card) => card.getBoundingClientRect());
cards.forEach((card, i) => {
  card.style.transform = `translateY(${rects[i].height}px)`;
});
Yechim
  • A — yo'q. textContent layout ham, style ham so'ramaydi. Rang klassi yozishlari skript oxirida bir marta hisoblanadi.
  • B — ha, 100% thrashing. padding o'lchamni o'zgartiradi (layout iflos), getBoundingClientRect() uni darhol so'raydi. Har aylanishda to'liq layout.
  • C — yo'q. Avval hamma o'qishlar (toza layout'dan), keyin hamma yozishlar. Yana bonus: yozilgan transform layout'ni umuman iflos qilmaydi.

2-mashq (o'rta): Kartalarni tenglang

Quyidagi funksiya to'g'ri ishlaydi, lekin thrashing qiladi. Uni guruhlangan usulda qayta yozing va oynada ikkalasini solishtiring: har biri tugagach konsolga eng katta balandlik va kartalarning minHeight lari bir xilmi — chiqsin.

js
function equalizeSlow(cards) {
  let max = 0;
  for (const card of cards) {
    card.style.minHeight = ""; // eski qiymatni tozalash
    max = Math.max(max, card.offsetHeight);
    for (const other of cards) other.style.minHeight = `${max}px`;
  }
}

Ishora: uch bosqich — hamma minHeight ni tozalash (yozish), hamma balandlikni o'qish, hammasiga yangi minHeight (yozish). Tozalashdan keyingi o'qishlar bitta majburiy layout qiladi — bu normal.

Yechim
html
<style>
  body { font: 0.95rem/1.4 system-ui, sans-serif; margin: 1rem; }
  .row { display: flex; gap: 0.5rem; align-items: flex-start; }
  .card { flex: 1; border: 1px solid #c8d6c8; padding: 0.5rem; }
</style>
<div class="row">
  <div class="card">Osh</div>
  <div class="card">Manti: qovoqli, bug'da pishgan, qatiq bilan</div>
  <div class="card">Lag'mon: qo'lda cho'zilgan xamir</div>
</div>
<script>
  function equalizeCards(cards) {
    // 1) yozish: eski qiymatni tozalash
    for (const card of cards) card.style.minHeight = "";
    // 2) o'qish: bitta majburiy layout
    const heights = cards.map((card) => card.offsetHeight);
    const max = Math.max(...heights);
    // 3) yozish: hammasiga bir xil balandlik
    for (const card of cards) card.style.minHeight = `${max}px`;
    return max;
  }

  const cards = [...document.querySelectorAll(".card")];
  const max = equalizeCards(cards);
  const target = `${max}px`;
  const same = cards.every((card) => card.style.minHeight === target);
  console.log("Hammasi bir xil:", same);
  console.log("Eng baland karta:", max > 0 ? "topildi" : "yo'q");
</script>

Konsolda:

text
Hammasi bir xil: true
Eng baland karta: topildi

Aniq piksel sonini chiqarmadik: u shriftga va oyna kengligiga bog'liq. Sekin variantda ichki sikl tufayli har o'qishdan oldin hamma kartaga yozilardi — 3 ta kartada 3 ta majburiy layout, 300 tasida 300 ta. Yangi variantda kartalar soniga qaramay bitta: tozalashdan keyingi birinchi o'qishda. Qolgan o'qishlar toza layout'dan oladi.

3-mashq (qiyin): Mini FastDOM

measure(fn) va mutate(fn) funksiyalarini yozing. Ular ishni darhol bajarmaydi — navbatga qo'yadi. Keyingi kadr boshida (rAF) avval hamma measure lar, keyin hamma mutate lar bajarilsin. Bitta kadrda rAF faqat bir marta so'ralsin.

Sinov: uch karta uchun aralash tartibda chaqiring — measure, mutate, measure, mutate, … Har funksiya konsolga nima qilayotganini yozsin. Natijada konsolda avval hamma "o'qish", keyin hamma "yozish" chiqishi kerak.

Ishora: ikki massiv (reads, writes), bitta scheduled bayrog'i. flush ichida massivlarni bo'shatishni unutmang — aks holda keyingi kadrda eski ishlar qayta bajariladi.

Yechim
html
<div class="card">Osh</div>
<div class="card">Manti</div>
<div class="card">Somsa</div>
<script>
  const reads = [];
  const writes = [];
  let scheduled = false;

  function schedule() {
    if (scheduled) return;
    scheduled = true;
    requestAnimationFrame(flush);
  }

  function flush() {
    scheduled = false;
    const nowReads = reads.splice(0);   // navbatni olib, bo'shatamiz
    const nowWrites = writes.splice(0);
    nowReads.forEach((fn) => fn());
    nowWrites.forEach((fn) => fn());
  }

  function measure(fn) { reads.push(fn); schedule(); }
  function mutate(fn) { writes.push(fn); schedule(); }

  for (const card of document.querySelectorAll(".card")) {
    let height = 0;
    measure(() => {
      height = card.offsetHeight;
      console.log(`o'qish: ${card.textContent}`);
    });
    mutate(() => {
      card.style.minHeight = `${height + 10}px`;
      console.log(`yozish: ${card.textContent}`);
    });
  }
</script>

Konsolda:

text
o'qish: Osh
o'qish: Manti
o'qish: Somsa
yozish: Osh
yozish: Manti
yozish: Somsa

Kod aralash tartibda chaqirdi, lekin bajarilish guruhlangan — shuning uchun majburiy layout yo'q. splice(0) massivning hamma elementlarini olib, uni bo'shatadi: flush paytida yangi measure qo'shilsa, u keyingi kadrga qoladi. Haqiqiy FastDOM shu g'oyaga xatolarni ushlash va bekor qilishni qo'shadi.

4-mashq: Amaliy tajriba — Performance panelida Forced reflow

kurs/mashqlar/13/27-thrashing/index.html ga «O'zingiz sinab ko'ring» bo'limidagi oynaning kodini ko'chiring, kartalar sonini 1 000 ga oshiring. Live Server bilan oching.

  1. DevTools → Performance → tishli g'ildirak → CPU: 4x slowdown.
  2. Yozishni boshlang, «Aralash» ni bosing, to'xtating. Main qatoridagi vazifani toping va uni tanlang.
  3. Pastdagi panelda Forced reflow ni toping va havolani bosing — u qaysi qatorga olib bordi?
  4. Xuddi shuni «Guruhlangan» uchun qiling. Layout bloklari nechta?
Yechim

1–3. Vazifa juda uzun (sekinlashtirilgan protsessorda bir soniyadan oshishi mumkin), ichida yuzlab siyohrang Layout bloklari. Forced reflow havolasi mixed funksiyasidagi card.dataset.height = card.offsetHeight; qatoriga olib boradi — biz izohda "majburiy layout" deb belgilagan qator.

  1. «Guruhlangan» — qisqa vazifa, ichida majburiy Layout yo'q. Bitta Layout bloki vazifadan keyin, kadr chizishda keladi.

Xulosani kurs/mashqlar/13/27-thrashing/XULOSA.md ga ikki qatorda yozing: qaysi qator va qanday tuzatildi.

bash
git add 13/27-thrashing
git commit -m "13/27: layout thrashing'ni Performance panelida topish"

8. Real ishda

  • Scroll va resize ishlovchilari. Har scroll da getBoundingClientRect() o'qib, style yozish — real saytlardagi qotishlarning eng ko'p uchraydigan sababi. Kuzatuvchilar (IntersectionObserver, ResizeObserver) va rAF bilan almashtiriladi.
  • Kutubxonalar. FastDOM g'oyasi ko'p animatsiya va virtual ro'yxat kutubxonalari ichida bor. React ham DOM'ga yozishlarni yig'ib, bir marta bajaradi — lekin uning ichidagi o'zingiz yozgan o'lchash kodi baribir thrashing qilishi mumkin (React — 17-qismda).
  • Audit. Performance panelidagi "Forced reflow" ogohlantirishi — performans auditida birinchi qaraladigan joylardan biri. Uning foydalanuvchi his qiladigan javob tezligiga (INP) ta'sirini keyingi darsda o'lchaymiz.
  • Intervyu. "Reflow va repaint farqi?", "Layout thrashing nima, qanday oldini olasiz?", "Animatsiya uchun nega transform?", "offsetHeight o'qish nega sekin bo'lishi mumkin?" — frontend intervyularining doimiy savollari.

Xulosa

  • Konveyer: JavaScript → Style → Layout → Paint → Composite. Layout (reflow) — eng qimmat bosqich; transform/opacity uni chetlab o'tadi.
  • Brauzer dangasa: yozishlarni "iflos" deb belgilaydi va kadr oldidan bir marta hisoblaydi.
  • Layout iflos paytida o'lcham o'qish (offsetHeight, getBoundingClientRect(), innerText, scrollY, focus()) — majburiy reflow. Sikl ichida navbatma-navbat — layout thrashing.
  • O'lchovimizda 500 ta karta: aralash — taxminan 100 ms va 500 ta layout, guruhlangan — 1 ms dan kam va 0 ta majburiy layout.
  • Davo: avval o'qish, keyin yozish; yozishni rAF'ga; o'lchamni kuzatuvchilardan olish va keshlash; harakatga transform; will-change ni vaqtincha.

Keyingi dars: Performance API va Core Web Vitals — performance.mark/measure, PerformanceObserver va foydalanuvchi his qiladigan tezlik: LCP, INP, CLS.

Manbalar

  • web.dev: "Avoid large, complex layouts and layout thrashing", "Rendering performance" — web.dev
  • Paul Irish: "What forces layout / reflow" — gist.github.com/paulirish
  • Chrome DevTools hujjati: "Performance features reference" (Forced reflow) — developer.chrome.com
  • HTML Living Standard: "Update the rendering" — html.spec.whatwg.org
  • FastDOM — github.com/wilsonpage/fastdom
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Rendering pipeline va layout thrashing: JavaScript brauzerni qachon majburan "o'lchatadi" — IlmHamroh