Галоўная > Браўзерны Event loop: micro and macro tasks, call stack, render queue: layout, paint, composite (2 частка)

Браўзерны Event loop: micro and macro tasks, call stack, render queue: layout, paint, composite (2 частка)

js
Event
loop

Пераклад артыкула Browser Event loop: micro and macro tasks, call stack, render queue: layout, paint, composite (2 частка)

Першую частку можна пачытаць тут.

Што выконваецца ў чарзе візуалізацыі (render queue)?

Візуалізацыя кадраў (frames) - гэта не адна аперацыя. Візуалізацыя кадра мае некалькі этапаў. Кожны этап можна падзяліць на падэтапы. Вось базавая схема візуалізацыі новага кадра:

RequestAnimationFrame

Разглядзім кожны этап больш падрабязна:

Request Animation Frame (RAF)

Браўзэр гатовы пачаць рэндэрынг, мы можам падпісацца на яго і разлічыць або падрыхтаваць кадр да кроку анімацыі. Гэты callback добра падыходзіць для працы з анімацыяй ці планаванні некаторых змен у DOM непасрэдна перад адмалёўкай кадра.

✍️ Некаторыя цікавыя факты пра RAF:

  1. RAF-callback мае аргумент DOMHighResTimeStamp, які з'яўляецца колькасцю мілісекунд, якія прайшлі з моманту "time origin", які з'яўляецца пачаткам жыцця дакумента. Вы можаце не выкарыстоўваць performance.now() у callback, ён у вас ужо ёсць.
  2. RAF вяртае дэскрыптар (id), таму вы можаце адмяніць RAF-callback з дапамогай cancelAnimationFrame (падобна на setTimeout).
  3. Калі карыстальнік зменіць укладку або згарне браўзер, у вас не будзе паўторнай візуалізацыі, што азначае, што ў вас таксама не будзе RAF.
  4. JS-код, які змяняе памер элементаў або счытвае ўласцівасці элементаў, можа прымусіць выканацца RAF.
  5. У Safari RAF выклікаецца пасля візуалізацыі кадра. Гэта адзіны браўзер з іншымі паводзінамі.
  6. Як праверыць, як часта браўзэр адлюстроўвае кадры? Гэты код можа дапамагчы:
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();

Вось прыклад выкарыстання:

check RequestAnimationFrame

Пераразлік стыляў (recalculation)

styles' recalculation

✍️ Браўзер вылічвае стылі, якія трэба прымяніць. На гэтым этапе таксама разлічваецца, якія медыя-запыты будуць актыўнымі.

Вылічванне стыляў ўключае як прамыя змены a.styles.left = '10px', так і змены, апісаныя ў файлах CSS, такіх як element.classList.add('my-styles-class'). Усе яны будуць пералічаны з пункту гледжання CSSOM і Render tree.

Калі вы запусціце прафайлер і адкрыеце вэб-сайт hashnode.com, тут вы можаце знайсці час, затрачаны на стылі:

profiler example

Layout (кампаноўка)

✍️ Разлік слаёў, размяшчэння элементаў, іх памераў і ўзаемаўплыву адзін на аднаго. Чым больш элементаў DOM на старонцы, тым складаней аперацыя.

layout

Layout - даволі балючая аперацыя для сучасных сайтаў. Переразлік адбываецца кожны раз, калі вы:

  1. Чытаеце уласцівасці, звязаныя з памерам і становішчам элемента (offsetWidth, offsetLeft, getBoundingClientRect і г.д.)
  2. Выкарыстоўваеце ўласцівасці, звязаныя з памерам і становішчам элементаў, за выключэннем некаторых з іх (напрыклад, transform і will-change). transform працуе ў працэсе кампазіцыі. will-change сігналізуе браўзеру, што змяненне ўласцівасці павінна быць разлічана на этапе кампазіцыі. Тут вы можаце праверыць сапраўдны спіс прычын гэтага.

Layout адказны за:

  1. Разлік макетаў
  2. Узаемаразмяшчэнне элементаў на пласце (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. Вы можаце праверыць гэта на скрыншоце і ўбачыць, што ў вас ёсць спіс элементаў, якія будуць змененыя і зрушаныя падчас кампаноўкі:

rerender layout

Браўзэры намагаюцца не фарсіраваць 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-га радка, у ім ужо ёсць усе даныя.

Наш цыкл падзей муціруе з аднаго цыкла ў некалькі, паколькі мы можам прымусова кампанаваць як задачы, так і этапы мікразадач:

EventLoop

Некалькі парад па аптымізацыі макета:

  1. Паменшыць колькасць вузлоў DOM
  2. Групуйце аперацыі чытання / запісу, каб пазбавіцца ад непатрэбных layouts (кампановак)
  3. Заменіце аперацыі force layout аперацыямі force composite

Paint

Paint

✍️ У нас ёсць элемент, яго становішча ў акне прагляду і памер. Цяпер мы павінны ўжыць колер, фон, гэта азначае «намаляваць» яго.

Paint

Гэтая аперацыя звычайна не займае шмат часу, аднак падчас першага рэндарынга час адмалёўкі можа павялічыцца. Пасля гэтага кроку мы можам "фізічна" намаляваць frame (кадр). Апошняя аперацыя - "Composition".

Composition

Composition

✍️ Composition - гэта адзіны этап, які па змаўчанні працуе на графічным працэсары (GPU). На гэтым этапе браўзер выконвае толькі пэўныя стылі CSS, такія як transform.

Важная заўвага: transform: translate не «ўключае» рэндэр на графічным працэсары. Такім чынам, калі ў вашай базе кода ёсць transform: translateZ(0) для перамяшчэння рэндэра на GPU, гэта не працуе такім чынам. Гэта памылковае меркаванне.

Сучасныя браўзеры могуць самастойна перанесці частку аперацый на графічны працэсар. Я не знайшоў актуальнага спісу для гэтага, таму лепш праверыць зыходны код.

composite layer

✍️ transform - лепшы выбар для складаных анімацый:

  1. Мы не робім прымусовую кампаноўку кадра, мы экономім час працэсара (CPU)
  2. Гэтыя анімацыі не змяшчаюць у сабе "мыла" - невялікіх затрымак, якія можна назіраць, калі на сайце анімацыя рэалізаваная праз top, bottom, right, left

Як аптымазаваць render (рэндэр)?

✍️ Самая складаная аперацыя для рэндэрынгу frame (кадра) - гэта кампаноўка (layout). Калі ў вас складаная анімацыя, кожная візуалізацыя можа запатрабаваць зрушэння ўсіх элементаў DOM, што неэфектыўна, так як вы патраціце 13-20 мс (ці нават больш). Вы страціце кадры і, такім чынам, прадукцыйнасць вашага сайта.

Каб палепшыць прадукцыйнасць, вы можаце прапусціць некаторыя этапы рэндэрынгу:

render layout

✍️ Мы можам прапусціць этап кампаноўкі (layout), калі зменяем колеры, фонавыя малюнкі і г.д.

skip layout layer

✍️ Мы можам адмовіцца ад Кампаноўкі (layout) і Малявання (Paint), калі выкарыстоўваем transform і не чытаем уласцівасці нашых элементаў DOM. Вы можаце кэшаваць іх і захоўваць у памяці.

✍️ Падсумоўваючы, вось некалькі парад:

  1. Перанясіце анімацыю з JS у CSS. Запуск дадатковага кода JS не "бескаштоўны".
  2. Выкарыстоўвайце transform для "рухомых" аб'ектаў.
  3. Выкарыстоўвайце уласцівасць will-change. Гэта дазволіць браўзерам "падрыхтаваць" DOM элементы для змен ўласцівасцяў. Гэтая ўласцівасць дапамагае браўзеру пабачыць, што распрацоўшчык збіраецца яе змяніць. Тут падрабязней.
  4. Выкарыстоўвайце пакетныя змены для DOM.
  5. Выкарыстоўвайце requestAnimationFrame, каб спланаваць змены ў наступным кадры.
  6. Спалучайце аперацыі чытання і запісу ўласцівасцей CSS элемента і выкарыстоўвайце мемаізацыю.
  7. Звярніце ўвагу на ўласцівасці, якія фарміруюць layout.
  8. Пры ўзнікненні нетрывіяльнай сітуацыі лепш запусціць прафіліроўшчык і праверыць частату і таймінгі. Гэта дасць вам даныя аб тым, што фаза працуе павольна.
  9. Аптымізуйце крок за крокам, не спрабуйце зрабіць усё адразу.

Як выглядае Event Loop у канчатковым выніку:

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();
}
loveJS, 2023-07-03
Каментары

    (Каб даслаць каментар залагуйцеся ў свой уліковы запіс)

    ;