Браўзерны Event loop: micro and macro tasks, call stack, render queue: layout, paint, composite (2 частка)
Пераклад артыкула Browser Event loop: micro and macro tasks, call stack, render queue: layout, paint, composite (2 частка)
Першую частку можна пачытаць тут.
Што выконваецца ў чарзе візуалізацыі (render queue)?
Візуалізацыя кадраў (frames) - гэта не адна аперацыя. Візуалізацыя кадра мае некалькі этапаў. Кожны этап можна падзяліць на падэтапы. Вось базавая схема візуалізацыі новага кадра:
Разглядзім кожны этап больш падрабязна:
Request Animation Frame (RAF)
Браўзэр гатовы пачаць рэндэрынг, мы можам падпісацца на яго і разлічыць або падрыхтаваць кадр да кроку анімацыі. Гэты callback добра падыходзіць для працы з анімацыяй ці планаванні некаторых змен у DOM непасрэдна перад адмалёўкай кадра.
✍️ Некаторыя цікавыя факты пра RAF:
- RAF-callback мае аргумент
DOMHighResTimeStamp
, які з'яўляецца колькасцю мілісекунд, якія прайшлі з моманту "time origin", які з'яўляецца пачаткам жыцця дакумента. Вы можаце не выкарыстоўвацьperformance.now()
у callback, ён у вас ужо ёсць. - RAF вяртае дэскрыптар (id), таму вы можаце адмяніць RAF-callback з дапамогай cancelAnimationFrame (падобна на setTimeout).
- Калі карыстальнік зменіць укладку або згарне браўзер, у вас не будзе паўторнай візуалізацыі, што азначае, што ў вас таксама не будзе RAF.
- JS-код, які змяняе памер элементаў або счытвае ўласцівасці элементаў, можа прымусіць выканацца RAF.
- У Safari RAF выклікаецца пасля візуалізацыі кадра. Гэта адзіны браўзер з іншымі паводзінамі.
- Як праверыць, як часта браўзэр адлюстроўвае кадры? Гэты код можа дапамагчы:
const checkRequestAnimationDiff = () => { let prev; function call() { requestAnimationFrame((timestamp) => { if (prev) { console.log(timestamp - prev); // It should be around 16.6 ms for 60FPS } prev = timestamp; call(); }); } call(); } checkRequestAnimationDiff();
Вось прыклад выкарыстання:
Пераразлік стыляў (recalculation)
✍️ Браўзер вылічвае стылі, якія трэба прымяніць. На гэтым этапе таксама разлічваецца, якія медыя-запыты будуць актыўнымі.
Вылічванне стыляў ўключае як прамыя змены a.styles.left = '10px'
, так і змены, апісаныя ў файлах CSS, такіх як element.classList.add('my-styles-class')
. Усе яны будуць пералічаны з пункту гледжання CSSOM і Render tree.
Калі вы запусціце прафайлер і адкрыеце вэб-сайт hashnode.com, тут вы можаце знайсці час, затрачаны на стылі:
Layout (кампаноўка)
✍️ Разлік слаёў, размяшчэння элементаў, іх памераў і ўзаемаўплыву адзін на аднаго. Чым больш элементаў DOM на старонцы, тым складаней аперацыя.
Layout - даволі балючая аперацыя для сучасных сайтаў. Переразлік адбываецца кожны раз, калі вы:
- Чытаеце уласцівасці, звязаныя з памерам і становішчам элемента (
offsetWidth
,offsetLeft
,getBoundingClientRect
і г.д.) - Выкарыстоўваеце ўласцівасці, звязаныя з памерам і становішчам элементаў, за выключэннем некаторых з іх (напрыклад,
transform
іwill-change
).transform
працуе ў працэсе кампазіцыі.will-change
сігналізуе браўзеру, што змяненне ўласцівасці павінна быць разлічана на этапе кампазіцыі. Тут вы можаце праверыць сапраўдны спіс прычын гэтага.
Layout адказны за:
- Разлік макетаў
- Узаемаразмяшчэнне элементаў на пласце (layer)
✍️ Layout (з ці без RAF або стыляў) можа быць пералічаны, калі js змяніў памеры элементаў або ўласцівасці чытання. Гэты працэс называецца force layout
. Поўны спіс уласцівасцяў, якія выклікаюць прымусовае аднаўленне layout тут.
✍️ Калі Layout аднаўляецца прымусова, браўзер прыпыняе асноўны паток JS, нягледзячы на тое, што стэк выклікаў не пусты.
Паглядзім гэта на прыкладзе:
div1.style.height = "200px"; // Change element size var height1 = div1.clientHeight; // Read property
Браўзер не можа вылічыць clientHeight
нашага div1
без пераліку яго рэальнага памеру. У гэтым выпадку браўзер прыпыняе выкананне JS і запускае: Style, каб праверыць, што трэба змяніць, і Layout, каб пералічыць памеры. Layout разлічвае не толькі элементы, якія размешчаныя перад нашым div1
, але і элементы пасля. Сучасныя браўзеры аптымізуюць вылічэнне так, каб не пералічваць усё дрэва DOM, але ў некаторых кепскіх выпадках гэта адбываецца. Працэс пераразліку называецца Layout Shift
. Вы можаце праверыць гэта на скрыншоце і ўбачыць, што ў вас ёсць спіс элементаў, якія будуць змененыя і зрушаныя падчас кампаноўкі:
Браўзэры намагаюцца не фарсіраваць layout кожны раз. Таму яны групуюць аперацыі:
div1.style.height = "200px"; var height1 = div1.clientHeight; // <-- layout 1 div2.style.margin = "300px"; var height2 = div2.clientHeight; // <-- layout 2
У першым радку браўзер плануе змяненне вышыні. У другім радку браўзер атрымлівае запыт на чытанне ўласцівасці. Паколькі ў нас чакаюцца змены вышыні, браўзеру трэба прымусова змяніць layout. Такая ж сітуацыя ў нас у 3 + 4 радках. Каб зрабіць гэта лепш для браўзераў, мы можам згрупаваць аперацыі чытання і запісу:
div1.style.height = "200px"; div2.style.margin = "300px"; var height1 = div1.clientHeight; // <-- layout 1 var height2 = div2.clientHeight;
Групуючы элементы, мы пазбаўляемся ад другога Layout, таму што калі браўзер даходзіць да 4-га радка, у ім ужо ёсць усе даныя.
Наш цыкл падзей муціруе з аднаго цыкла ў некалькі, паколькі мы можам прымусова кампанаваць як задачы, так і этапы мікразадач:
Некалькі парад па аптымізацыі макета:
- Паменшыць колькасць вузлоў DOM
- Групуйце аперацыі чытання / запісу, каб пазбавіцца ад непатрэбных layouts (кампановак)
- Заменіце аперацыі
force layout
аперацыяміforce composite
Paint
✍️ У нас ёсць элемент, яго становішча ў акне прагляду і памер. Цяпер мы павінны ўжыць колер, фон, гэта азначае «намаляваць» яго.
Гэтая аперацыя звычайна не займае шмат часу, аднак падчас першага рэндарынга час адмалёўкі можа павялічыцца. Пасля гэтага кроку мы можам "фізічна" намаляваць frame (кадр). Апошняя аперацыя - "Composition".
Composition
✍️ Composition - гэта адзіны этап, які па змаўчанні працуе на графічным працэсары (GPU). На гэтым этапе браўзер выконвае толькі пэўныя стылі CSS, такія як transform
.
Важная заўвага:
transform: translate
не «ўключае» рэндэр на графічным працэсары. Такім чынам, калі ў вашай базе кода ёсцьtransform: translateZ(0)
для перамяшчэння рэндэра на GPU, гэта не працуе такім чынам. Гэта памылковае меркаванне.
Сучасныя браўзеры могуць самастойна перанесці частку аперацый на графічны працэсар. Я не знайшоў актуальнага спісу для гэтага, таму лепш праверыць зыходны код.
✍️ transform
- лепшы выбар для складаных анімацый:
- Мы не робім прымусовую кампаноўку кадра, мы экономім час працэсара (CPU)
- Гэтыя анімацыі не змяшчаюць у сабе "мыла" - невялікіх затрымак, якія можна назіраць, калі на сайце анімацыя рэалізаваная праз
top
,bottom
,right
,left
Як аптымазаваць render (рэндэр)?
✍️ Самая складаная аперацыя для рэндэрынгу frame (кадра) - гэта кампаноўка (layout). Калі ў вас складаная анімацыя, кожная візуалізацыя можа запатрабаваць зрушэння ўсіх элементаў DOM, што неэфектыўна, так як вы патраціце 13-20 мс (ці нават больш). Вы страціце кадры і, такім чынам, прадукцыйнасць вашага сайта.
Каб палепшыць прадукцыйнасць, вы можаце прапусціць некаторыя этапы рэндэрынгу:
✍️ Мы можам прапусціць этап кампаноўкі (layout), калі зменяем колеры, фонавыя малюнкі і г.д.
✍️ Мы можам адмовіцца ад Кампаноўкі (layout) і Малявання (Paint), калі выкарыстоўваем transform
і не чытаем уласцівасці нашых элементаў DOM. Вы можаце кэшаваць іх і захоўваць у памяці.
✍️ Падсумоўваючы, вось некалькі парад:
- Перанясіце анімацыю з JS у CSS. Запуск дадатковага кода JS не "бескаштоўны".
- Выкарыстоўвайце
transform
для "рухомых" аб'ектаў. - Выкарыстоўвайце уласцівасць
will-change
. Гэта дазволіць браўзерам "падрыхтаваць" DOM элементы для змен ўласцівасцяў. Гэтая ўласцівасць дапамагае браўзеру пабачыць, што распрацоўшчык збіраецца яе змяніць. Тут падрабязней. - Выкарыстоўвайце пакетныя змены для DOM.
- Выкарыстоўвайце
requestAnimationFrame
, каб спланаваць змены ў наступным кадры. - Спалучайце аперацыі чытання і запісу ўласцівасцей CSS элемента і выкарыстоўвайце мемаізацыю.
- Звярніце ўвагу на ўласцівасці, якія фарміруюць layout.
- Пры ўзнікненні нетрывіяльнай сітуацыі лепш запусціць прафіліроўшчык і праверыць частату і таймінгі. Гэта дасць вам даныя аб тым, што фаза працуе павольна.
- Аптымізуйце крок за крокам, не спрабуйце зрабіць усё адразу.
Як выглядае Event Loop у канчатковым выніку:
Калі мы адчынім https://github.com/w3c/longtasks/blob/loaf-explainer/loaf-explainer.md#the-current-situation, то ўбачым код, які ўяўляе сабой цыкл падзей сучасных браўзэраў:
while (true) { const taskStartTime = performance.now(); // It's unspecified where UI events fit in. Should each have their own task? const task = eventQueue.pop(); if (task) task.run(); if (performance.now() - taskStartTime > 50) reportLongTask(); if (!hasRenderingOpportunity()) continue; invokeAnimationFrameCallbacks(); while (needsStyleAndLayout()) { styleAndLayout(); invokeResizeObservers(); } markPaintTiming(); render(); }
Каментары
(Каб даслаць каментар залагуйцеся ў свой уліковы запіс)