Галоўная / Артыкулы / Спачатку зменшыце колькасць работы: Кантрольны список для павышэння каркаснасці React пры выкарыстоўванні мемаізацыі

Спачатку зменшыце колькасць работы: Кантрольны список для павышэння каркаснасці React пры выкарыстоўванні мемаізацыі

Зменіце вартасць аплікацыі React, утримваючыся ад непатрэбной працы: выкорыстоўваючы алгорытм debounce для пошуку, паградаванне дадзеных, аднаўленне статаусу, стабільныя ключы, перанесенне обробкі на серверную сторону і атрыбут lazy-load, а таксама мемазаванне, як толькі гэта неабходна.

2118 слоў

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

Ключоўны вопыт у всім гэтым прост: чы можна цю работу абсалютна ужо не выканаць?

Зменшыць колькасць запитоў: адключыць дэбаунс для поля пошуку

Поля пошуку ёсць класычным джерелам зайвых запитоў. Неэфектывае рашэнне выканае запыт да API пасля кожных змян:

const handleSearch = (value) => {
  fetchUsers(value);
};

Увод слова “React” у гэта поле стварае по аднаму запыту на кожны натиск клавішы:

R
Re
Rea
Reac
React

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

const handleSearch = debounce((value) => {
  fetchUsers(value);
}, 300);

З інтервалам у 300 мс шырокія спэцыясты запускаюць толькі адны запит у канцы. Цэўкае ся трафік сеті, навантажэння сервера і обробка адхілёных рэспансаў на стороне кліента. Зауважыце, што тут нічога не стосуецца рэндарынгу; выгода выходзіць чыста з таго, што не трэба выканваць дадатковую роботу.

Адзін застерэжны момент, калі вы викорыстоўваете такі памочны прыемак унутрь складовай: якщо debounce(...) вызываецца безпосередна ў тэле складовай, то з кожным перрендаром ствараецца новая функцыя з дэбаусінгам (і новы таймер), што наражае празьбу дэбаусінгу. Яе трэба стварыць толькі раз, напрыклад, за дапамою useMemo або useRef, альбо выкорыстаць патэрн на базе эфекта, паказаны ў нижчым прыкладзе.

Самодостатня складовая для пошуку з дэбаусінгам

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

import { useEffect, useState } from "react";
function UserSearch() {
  const [search, setSearch] = useState("");
  const [users, setUsers] = useState([]);

Эфект запускаецца ўсё час, калі змінюецца search. У замест на негаўны запрос, ён плануе виконанне задачі за дапамою setTimeout. Унутрь калебэка пусты або толькі з пробеламі запит чыстае рэзультаты і завершае работу без выканання запросу.

useEffect(() => {
    const timer = setTimeout(async () => {
      if (!search.trim()) {
        setUsers([]);
        return;
      }

Для рэальнага запытку калебак-функцыя прасіла падходзячых корыстнікаў, кодуючы тэрмін пошуку, каб спецыяльныя знакі не паспелі зламаць URL:

const response = await fetch(
        `/api/users?search=${encodeURIComponent(search)}`
      );

Пасля чаго выканана практыка JSON і зберажаны рэзультат, усё гэта за час 300 мс:

const data = await response.json();
      setUsers(data);
    }, 300);

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

return () => clearTimeout(timer);
  }, [search]);

У падсумку компонент атрыбутуе інпут, які керуецца зменным search:

return (
    <div>
      <input
        value={search}
        onChange={(e) => setSearch(e.target.value)}
        placeholder="Search users..."
      />

І ён паказвае корыстнікаў, прыорантаваных па ўсіх іх ID:

{users.map((user) => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}

Кожны выведзеный символ перазначае таймер, і запыт адправляецца толькі пасля 300 мс тышыні. Хавайце на увазе, што метод дебаунсінга зменшывае колькість адправлёных запытоў, але не гарантуе, што яны буду выконваны па порядку; медленная адпаведнае на запыт можа прымусіць замесіць новейшую адпаведнае. Якщо гэта важліва для вашага інтерфейсу, выкорыстоўваеце яго разам з анулюванням запытоў, як пісана ў стацыі пра усуненні ситуацый канкурэнцыі, якія не можна рашыць за дапамогою дебаунсінга ў інтерфейсах пошуку.

Шырэйшы урок: запобежчанне виконанню роботы зазвычай краща, чым прышвартаванне існуючай роботы.

Зяўляйце толькі тыя данні, якія патрэбны экрану

Іншым частым факторам выкарыстання ресурса є завантажэнне значна большайшага колькасці дадзейна, чым паказваецца. Прыпустім, канцэнтр падачы дадзейна вяртае 10 000 корыстувачаў, тады калі адображэнне паказвае 20 корыстувачаў за раз. Браузер усё рава должен завантажыць, апрацаваць і зберагчы ў памяці ўсіх іх, а React должен працаваць з набрамі дадзейна, якія є значна большымі, чым трэба.

Як толькі це можліва, выкарыстоўвайце сторнаванне, каб кожны запит несў толькі адну сторунку:

API
 ↓
20 users
 ↓
Browser
 ↓
Display

Кожны раз, калі корыстувач пераходзіць да наступнай сторунки, запрашайце наступны фрагмент дадзейна. Цэе зменшае выкарыстанне сеті, спрабоў памяці, обробку на стороне кліента і колькасць дадзейна, якая праходзіць через вашы компоненты. Принцып той самы і для безканечнага прасування та API, якія выкарыстоўваюць курсор. Змена спосабу запрашэння дадзейна часта дае значна большыя рэзультаты, чым налагоджэнне аднарозовага компонента.

Зберагчы стан, які часта зміняецца, поблізу тых, хто яго выкарыстоўвае

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

function Dashboard() {
  const [search, setSearch] = useState("");
return (
    <>
      <SearchBox value={search} onChange={setSearch} />
      <Analytics />
      <UserTable />
    </>
  );
}

Кожны натык клавішы апдэйтуе Dashboard, таму Analytics і UserTable таксама перзапрацоўваныя, хоча ніхто з іх не выкарыстоўвае значэнне пошуку. На большым дашбордзе гэта накапліваецца. Якщо стан значыць толькі для невялікай часткі UI, яго перанесенне ў тую частку (тут — у сам SearchBox) обмежвае апдэйты там, дзе яны неабходны, і спрыяе лепшаму розумэнню структуры компонентаў.

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

Даць элементам дынамічнага списку стабільныя ключы

У співаках дробныя деталі маюць вельмы значны эфект. Часта практыка — выкорыстоўваць індэкс масівы як ключ:

{users.map((user, index) => (
  <UserCard key={index} user={user} />
))}

Індэксы-ключы не завжды ўскладненныя. Для статычнага співака, чый порядак ніколи не змінюецца, яны падходзяць. Для дынамічнага співака стабільны ID з дадзеных дае кожнам элементу трывалую ідэентычнасць:

{users.map((user) => (
  <UserCard key={user.id} user={user} />
))}

Разлік заключаецца у тым, што представляе ключ:

index → position
id    → identity

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

Спытваюцеся пра процэс обробкі, а не толькі пра яго швальнасць

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

const filteredUsers = users
  .filter((user) => user.isActive)
  .sort((a, b) => a.name.localeCompare(b.name))
  .map((user) => ({
    ...user,
    displayName: user.name.toUpperCase()
  }));

Пры колькіх тысяч записаў павтарэнне гэтых дзеянь на кожным адрасаванні стае дужа дорогім. Альтернатыва — зберагчыць рэзультаты, але спачатку трэба выясніць, чы вообща кліенту патрэбна такая праця. Часта сам эндпункт можа фільтруваць толькі актыўных корыстнікаў, база дадзенаў може адпрацавваць сортаванне за дапамогою індэксаў, што робіць гэта дышуцельным, а пагінацыя можа зменшыць размер набора дадзенаў, так што застатковая обробка стане простаю. Зменшэнне вхідных дадзенаў часта ёсць кращым спосабам, чым праспяшэнне вычысленняў.

Завантажаць функцыі за патрэбай

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

const Reports = lazy(() => import("./Reports"));

За дапамою React.lazy модуль запрашываецца першы раз, калі рэндаруецца компонент, і гэта павінна выканацься ў межах Suspense, які паказвае альтернатыўны варыянт пакуль він завантажваецца. Гэта ўсё болей корыстна ў дапрыемах з большам чысла маршрутаў, вялікімі функцыональнымі роздзеламі, важкімі залежнасцямі, такімі як бібліятэкі для стварэння графікаў чы редактораў, або экранамі, якія відвідае мало корыстувачаў.

Трэба чытка адразу з’ясаваць, што гэта дае: меньшы пачатковы размер пакета JavaScript і шырэйшы першы завантажэння. Гэта не прыводзіць да таго, што код унутры ляжнага компонента будзе выкананы шырэйша пасля завантажэння, і гэта дадае невялікую затрывку пад час завантажэння першы раз, калі адкрываецца даная функцыя.

Мемізаванне з прычыной

Пашчэрстаная памылка — адразу застосавляць API для оптымізацыі ў всім коде, напрыклад, пакрываючы кожны хэндлер:

const handleClick = useCallback(() => {
  setSelectedUser(id);
}, [id]);

Або кожную вырахаваную значэння:

const data = useMemo(() => {
  return processData(users);
}, [users]);

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

  • Чы рэальна вычысленне є даскладным?
  • Чы яно выканаецца часта?
  • Чы рэзультат викорыстоўваецца знова пад час рэндараў?
  • Чы мемазаваны падзец або эфект залежыць ад стабільнага кансэрту?
  • Чы змяна дае значны эфект?

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

Спіс для перагляду

Працаваць над гэтымі пытаннямі трэба пад час аудыту функцый React:

  1. Непатрэбныя запыткі? Шукаць вызовы на кожны натиск клавішы, дубліруемыя запыткі і запыткі да дадзэнняў, якія зараз не паказваюцца.
  2. Занадта многія дадзеныя? Выводзіць інфармацыю па сторанкам, фільтруваць на сервере і відкладаць запыткі да дадзэнняў пакуль яны не будуць патрэбныя.
  • Стан занадта высокі? Пераканаўцеся, чы неякое адчыненне вымушвае пераробляць большую частку дрэва, чым трэба.
  • Нестабільныя ключы ліста? Іспользуйце стабільныя ID там, дзе элементы можа ўскорасці аб змяніць свой статус.
  • Занадта вялікі обрабоцкі час? Пераканаўцеся, чы роботу можна перадаць на сервер аб прыхіліць да ёй перад тым, як яе оптымаізаваць.
  • Непатрэбныя функцыі? Завантажвайце вялікія аб рэдка выкарыстоўваныя модулі пасля патрэбнасці.
  • Ненякоскладаная мемаізацыя? Не даджайце useMemo, useCallback або React.memo толькі таму, што яны існуюць.
  • Прыдатнасць — це канвейер, а не проста пераробка

    Прыдатнасць React стосуецца не толькі самага React. Затраты накапліваюцца на всій лініі виконання роботы:

    API calls
       ↓
    Amount of data
       ↓
    State updates
       ↓
    Component structure
       ↓
    Data processing
       ↓
    Rendering
       ↓
    Bundle size
    

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

    Ключовыя выводы

    • Адзьёмленае завада (запиты, байты, апдэйты, код) зазвычай дае большыя рэзультаты, чым проста прышвыдчэнне выконання.
    • Абмежыце запиты, якія ініціююцца з вводу, і соедыніце абмежэнне з анулюванням, калі важна парадоксальнае выконання.
    • Дазвольце серверу фільтрацыяваць, сортаваць і ствараць сторніцы; надасце кліенту толькі тое, што ён адображае.
    • Разместіце стан на найнижшым рэвэлі, які служыць усім чытачам, і прыоритэзаваць дынамічныя спісы па ідэнтыфікаторах.
    • Вяршыце завантажэнне толькі тады, калі цяперашняе завантажэнне є лёгкім, і вжывайце мемаізаванне толькі тады, калі конкрэтныя затраты гэта правераны.
  • Перш чым спытацца, як аптимізаваць што-небудзь, спытайцеся, чы вообща трэба гэта робіць.
  • Спаднёйшыя матэрыялы