Галоўная / Артыкулы / Какі элементы Render'уецца ў React на асновнай вялікі

Какі элементы Render'уецца ў React на асновнай вялікі

Render versus commit у React: чаму невидзімая робота render таксама конкуруе з вводам на галоўнай вялі, і што на самае рэальна захоўвае мемуізацыя.

1665 слоў

Функцыя “render” у React сама па сабе не зменяе тое, што ап’являецца ў браузеры. Компанента, якая выконваецца адна раз, і тая ж самая компанента, якая выконваецца сто разоў, можа здавацца ідэнтычныя для чалавека, які викорыстоўвае дапамогу. Яны здаюцца ідэнтычныя таму, што браузер апдэйтуецца толькі тады, калі на стадыі commit выканана змена ў DOM, а сама функцыя render ніколі не гарантуе такой змены. Тады чаму столькі рэкамендацый ўжо па кантролі прыдатнасці React сфокусавана на абходзеўцы функцый render — useCallback (зберагчы ідэнтычнасць функцыі между выконаннямі), useMemo (зберагчы вырахаваную значэння между выконаннямі), React.memo (праявіць абходзеўку выконання дзецячых компанет, калі пропсы не зменіліся), а таксама на скорачэнні змян стану?

Гэты прытлак рэальны: можа здавацца, што оптымізуецца ўсё тое, чаго корыстнік ніколі не зазначыў бы.

Якшо функцыя render ніколи не паўтарае дзейства ў браузеры, на шта ж насправды вона витрачае ресурсы?

Две фазы, якія знаходзяцца ўнутрь адного процеса выканання

Людзі часта вжываюць тэрмін “выканання” для пазначэння цэлага циклу апдэйта. React дзельніць гэты цикл на две фазы з рознымі завданнямі. У аднай з фаз ствараецца опис наступнага інтерфейсу. У другай фазе выявляецца, чы рэшта гэты опис у вастоўныя змены DOM.

Тыя ж чатыры прычыны запускаюць обе фазы: апдэйт стану, змена пропаў, павторны процес выканання родніка або змена значэння контексту (значэнне з Context API, якое чытаецца без праходжання через пропы). Разлік межуе ў тым, што кожная з фаз рабіць далей — і якія ў яе затраты.

Што відбываецца ўнутрь першай фазы, той, якая завжды запускаецца, калі React плануе апдэйт?

Разбіранне фазы выканання

Калі React выявляе, што патрэбна апдэйтаванне, ён зноў вызывае функцыю компонента з верху. Выканаюцься всі оператыўныя запісы ў яе тэле: вычысленні, циклы, стварэнне об’ектаў у формате inline.

Потым React ацэнюе JSX пасля return. JSX (<div>...</div> синтаксіс) не ёсць HTML і ніколі сама по сабе не доходзіць да браузера. У момент складання програмы Babel або кампайлер TypeScript ператварае яго ў вызывы функцый — ранейша гэта быў React.createElement, а сёння часта викорыстоўваецца сучасны функцыйны інструмент jsx(). Саме гэтыя вызывы ствараюць опис компонента.

Рэзультат зазвычай называецца віртуальным DOM; унутраняе назва React — дрэва элементаў. Это звычайны об’ект JavaScript, які описвае планаваны інтерфейс — не рэальны HTML, не жывы вузол DOM.

Разглянем мінімальны компонент:

function Greeting({ name }) {
  const message = `Hello, ${name}`;
  return (
    <div>
      <h3>{message}</h3>
    </div>
  );
}

Змена значэння name выклікае у React паўтарчыце вызначэнне Greeting. Строка-шаблон для message таксама выканана зноў. Пасля кампіляцыі JSX ствараецца дрэва об’екта, падобнае да наступнага:

{
  "type": "div",
  "props": {
    "children": {
      "type": "h3",
      "props": { "children": "Hello, Akshat" }
    }
  }
}

Этот об’ект — аднавярэны віртуальны DOM функцыі Greeting. Структура застаўся ў памяці — не было жадных змян у DOM, аплявленняў элементаў чы пераранжавання. Што будзе выкарыстоўваць гэтае дрэва далей?

Разбіранне фазы зберагаўання змян

React параўнае новае дрэва элементаў з пярэднім — гэта процес супраўжнення, які выявляе тыя змены, якія насправды адбыліся. Толькі тыя разніцы, якія былі знайдзены пад час параўнання, запішуюцца у рэальны DOM. Якщо нічога не змянілася, нічого не запішываецца.

Верніцеся да Greeting і заўважыце, што name пераходзіць з адной строкі ў іншую. У пярэйшай структуре стары тэкст прывітання знаходзіўся ў элементе h3; у новай структуре — апошнены тэкст. Структура застаецца такой жа — div, які абгортае h3 — таму для фіксацыі змян фіксуецца толькі адна разліка: сам этап тэксту. Этап комітавання апошневае толькі гэты тэкст; раштат структуры застаецца некінульным.

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

Ёсць ключоўы прыклад для падтрымкі рэшты аргументаў. Якбы name не змяніўся пад час паўтарной актывацыі, новая дрэва структуры будзе адпавядаць старай, процес супраставлення знайдзе нуль разлікоў, і каміт не будзе мяць жадных наследкав. Ніякога збірання дадзеных у DOM, ніякага пераскладвання элементаў, ніякога атрыбутавання — нічага, што моглы б пазначыць вочы корыстніка. Аднак фаза атрыбутавання — вызов функцыі, перысчытанне message, стварэнне новай дрэва об’ектаў — все ж такі завершылася часткова раней.

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

Фаза атрыбутавання може працаваць і ўсё ж нічога не змяніць

Хтоцэ тут дыялог становіцца логічным. Рэндары, які дае ідэнтычны выхад, все равно спалюе рэальныя рычасці: адбываецца рэальны вызов функцыі, рэальна выдача памяці для кожнага вузла ў дрэве, рэальная праця па супароўненні, якая пераглядае дрэва прычымоўна, перш чым выйсці да заключэння, што нічога не змянілася. Нічага з гэтага не праказваецца на экране. Але ўсё гэта все равно адбываецца.

Гэты прыемак — рэальная праця, якая застаецца невідмовной — і ўсё тое, чырэз што можа быць правама як тверджэнне, што «рэндары не маюць значэння», так і тверджэнне, што «рэндары маюць вельмі вялікое значэння», пры разных аспектах. Рэндары сапраўды не вялікі ў тым, калі саме будзе бачыць корыстнік; гэта вялікі ў рэштыце заходзеў. Рэндары маюць значэння для чагосьці іншага, якое не мае нічынага з пікселямі.

Што ж гэта за ўсё іншае?

Галоўны поток не пакушаецца ведаць, чы робота ўвіднаецца

Этай «чыжын» — галоўны поток JavaScript: адна спяльная черга, якая выконвае задачы по адной. Той самы поток запускае ваш JavaScript, вырахоўвае макет і обробляе запускаваныя зместы — клікі, прасування, натысканне клавіш.

Браузеры ствараюць новую рамку прыбліжна кожныя 16,7 мілісекунды, ўрабатваючы 60 рамакоў за секунду. Усё, што трэба для паведамленай рамкі — логіка фазы адрасавання, супрацоўка данных, запіс у DOM, формаванне макета, атрыбуты візуальнага выканання — павінна поместыцца ў гэты трымач часу; фаза адрасавання не отрымае прыорітету толькі таму, што ўсё, што яна створыла, можа быць знехтавана. Яна выкарыстоўвае тую самую чергу, якую корыстнік відчувае пад час інтеракцыі.

Будзьце точны: не кожны крок браузера для фрама выконваецца на тым жа потоку. Растерызацыя (ператворэнне команд на малюванне ў пікселі) і складанне (з’едынэнне шароў у фінальны фрам) часта выконваюцца ў іншым месцы, таму шар, які ўжо растерызаваны, можа заставацца на месцы пад час, калі главны поток зайняты. Сама фаза адрасавання, а таксама запускаючыя яе зместы, все равно выконваюцца на главным потоку. Аддзельныя потакі для складання адчыняюць частку павышэння плавнасці пад навантажэнням; але яны не зменшуюць витраты, якія ўсё равно є пад час фазы адрасавання.

Адна марная процедура адрасавання можа коштаць частку мілісекунды. Калі гэта стае ўсведамленным для пользователя?

Чаму фаза адрасавання все равно мае значэнне

Таму што яна рэдкая выконваецца толькі адназнач. Калі «родны» элемент зноў адрасаваецца, React за замовчаннем выконвае фазу адрасавання для кожнага «дзецячага» элемента, нават калі ў ягох параметрах не было змян, за выключэнням случаю, калі такі элемент абгортаваны ў React.memo.

function Dashboard() {
  const [searchTerm, setSearchTerm] = useState('');
  const products = useProducts(); // 200 items
  return (
      <div>
        <input
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
        />
        <ProductList products={products} />
      </div>
    );
  }

function ProductList({ products }) {
  return (
    <div>
      {products.map((product) => (
        <ProductRow key={product.id} product={product} />
      ))}
    </div>
  );
}

Увайдзіце адзін симвал у поле пошуку, і searchTerm будзе адкорэгаваны. Апэкты кансоль выконвальнага палаты зноў змяніліся. ProductList і, пад якім, усе 200 компонентаў ProductRow таксама зноў запускаюцца, не таму што ў яных змяніліся параметры (надакладкі клавіш не маюць звязку з данымі продуктав), а таму што дзецявы элементы пераранжуюцца, калі першыя змянююцся, якщо толькі што-небудзь не спяшае гэты процес.

Кожны з тых 200 вызоў яшчэ є рэальным выкананнем функцыі, рэальным дрэвам элементаў па рядку і рэальным супрацоўкам, які выявляе, што ніякі з рядкаў не патрабуе зберагання ў DOM. Одна надакладка клавішы — це дробны крок. Поле жывога пошуку пры нормальной швалі набора тексту выклікае гэты процес колькісцю разоў у секунду на той жа поток, які должен обрабатваць наступную надакладку клавішы.

Гэта сцэнарый, які ўсунуць useMemo, useCallback і React.memo — тады чаго насправды яны захоўваюць?

Чаго насправды захоўваюць useMemo, useCallback і React.memo

React.memo абгортае компонент і прыпінае его рэндаруванне, калі новыя пропсы є паверхнева-роўныя паканунешням (=== для кожнага пропса). Абгортаўшы ProductRow, нажатыя клавішы на панелі керування больш не выклікаюць 200 вызоў рэндарування; React пораўнюе пропсы адна раз на кожны ряд і зупіняецца, калі рэферэнс product не змяніўся.

useMemo зберагае результаты вычыслаў між перрадзіраннямі, таму даскладныя задачы ў тэле компонента выкананыя зноў толькі тады, калі зменяюцца перыядычныя залежнасці. useCallback рабіць тое ж самае для аўтентычнасці функцый. Яго наявнасць зазвычай служыць не столькі для заўсёды эканоміі ресурсоў на стварэнне функцый, сколькі для захавання мемузаванага падкомпонента: кожна новая функцыя пасля перрадзірання ўтварае новую рэферэнцю, а новая рэферэнцыя наражае на паўстановку простага пераканання React.memo ў тым, што атрыбут быў пераданы.

Ні адна з трох методаў не выклікае змян у фазе запісу. Запіс вже контролюецца процэсам супароўнення, які выявляе рэальныя разлікі. Якщо нічога і так не будзе праказана на экране, гэтыя інструменты не вплываюць на тое, чы будзе адбыцца запіс у DOM. Яны эканомяюць час, які витрачаецца на вычыслення пад час перрадзірання — час на главнай сцыяле, які витрачаецца нават тады, калі рэзультат застаецца ідэнтычным.

Яны таксама не ў бесплатнасці. Порэйнаванне, якое выкарыстоўвае React.memo, і пошук у кэшы, які выкарыстоўвае useMemo, кожны раз ствараюць додатковыя витраты, таму абгортаванне дешавога, рэдка выкарыстоўванага компонента можа стаць чыстай втратай: платіце додатковыя зусилля за захаванне роботы, якая ніколі не была дорогай.

Калі гэта мае значэнне для таго, хто выкарыстоўвае продукт?

Відрэштаванне коштуе тых ресурсоў, якія фактычна відчувае корыстнік

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

Render застаецца некалькім безкоштовным, таму што ён невызначальны. Це справжня праца ў той самай однопотной черзі, як і рэндарыванне лейауту, збережэння змян у DOM і кожны клік, прасуванне та натыск клавіш. Адсутнае выканання роботы на стадіўцы render у той час, калі ведацья змяны распространяюцца праз глыбокі список дзецячых элементаў, або калі набіранне тексту і прасуванне шыбка спрычыняюць змяны, — гэта спосаб вярнуць мілісекунды, якія патрэбны для взаімадзеі.

Люди рэдка кажу пра тую тыхую частку. Менш рэндароў ніколі не было канечным цялевым пунктам. Мета — залишыць достатню частку спакульнае квоты у ~16,7 мс для роботы з змянамі і обробкі вводу, якія ўжо можа пазначыць корыстнік.