Към съдържанието
wannacode

Бързина: кога се рисува наново

Накратко: В React компонентът се рисува наново при промяна на състоянието си или на родителя; първо се мери, после memo, useMemo и useCallback спират излишното рисуване.

Проблем

Всяка буква рисува целия списък

Горе има поле за търсене и бутон „Търси“. Списъкът се филтрира едва след „Търси“. ProductList пише в конзолата всеки път, когато се рисува. Напиши „мляко“ в полето и гледай конзолата.

React

Пет букви, пет нови реда „рисувам ProductList“. Списъкът е същият: още не си натиснал „Търси“. Тук продуктите са осем и не се усеща. В истинския каталог са 2000, всеки с картинка и цена, и полето започва да изостава след пръстите.

Идеята

Кога компонентът се рисува наново

Компонентът се рисува наново, когато се сменят данните, които показва. Но кое значи „данните му“, двете библиотеки решават различно. И двете дават верен резултат; разликата е колко работа вършат.

React рисува наново компонента, чието състояние се смени, и всичките му деца, по цялото дърво надолу. По подразбиране не проверява дали props на детето са същите. Тук draft е в App, затова всяка буква рисува App, ProductList и всяка карта.

Това обикновено е бързо: React сравнява резултата със старата страница и сменя в DOM само разликата. Скъпо става, когато поддървото е голямо или сметката в него е тежка.

Първо мери, после поправяй. Брояч в конзолата, както тук, е най-простото мерене. React DevTools имат раздел Profiler: записваш, пишеш в полето и виждаш кой компонент колко пъти се е рисувал и за колко милисекунди. Поправяш само рисуването, което е и излишно, и бавно.

Виж го

Виж кой светва

const ProductList = memo(function ProductList({ products, onAdd }) {
console.log(`рисувам ProductList: ${++renders}`);
return products.map((p) => <ProductCard key={p.id} product={p} onAdd={onAdd} />);
});
function App() {
const [draft, setDraft] = useState('');
const [query, setQuery] = useState('');
const [count, setCount] = useState(0);
const visible = useMemo(() => filterByName(PRODUCTS, query), [query]);
const add = useCallback(() => setCount((c) => c + 1), []);
return (
<>
<input value={draft} onChange={(e) => setDraft(e.target.value)} />
<button onClick={() => setQuery(draft)}>Търси</button>
<ProductList products={visible} onAdd={add} />
</>
);
}
Стъпка по стъпка, с думи
  1. App пази текста в полето (draft) и търсенето (query). ProductList рисува картите. Конзолата брои колко пъти се рисува списъкът.
  2. Всеки клавиш сменя draft. React рисува наново App, а с него и всяко дете: ProductList и всяка карта, макар че списъкът е същият.
  3. memo(ProductList) сравнява новите props със старите. useMemo пази същия масив, докато query е същото, а useCallback пази същата add.
  4. Сега клавишът рисува само App и полето. Props на ProductList са същите, затова той е пропуснат, а броячът стои.
  5. „Търси“ сменя query. Масивът е нов, затова ProductList се рисува. Това рисуване е нужно: списъкът вече показва два продукта.
  6. Първо мери: брояч в конзолата или Profiler в DevTools. Поправяй само рисуванията, които са и излишни, и бавни.
React може да прескочи дете само ако му кажеш да сравни props: това е memo. А сравнението има смисъл само ако props наистина са същите: масивът и функцията трябва да се пазят между рисуванията.

Пробвай

Само „Търси“ рисува списъка

Направи така, че писането в полето и „Добави“ да не рисуват ProductList наново. Само „Търси“ трябва да го рисува, защото само тогава списъкът се сменя. Проверката пише „мляко“, добавя два продукта, натиска „Търси“ и брои редовете „рисувам ProductList“ в конзолата.

React

Пробвай

Намери грешката

ProductList е в memo, а списъкът пак се рисува при всяка буква. Кой ред е виновен?

Избери реда с грешката.

Пробвай

Попълни

Как скъпият списък остава същият между рисуванията?

const visible = (
  () => PRODUCTS.filter((p) => p.name.includes(query)),
  [query],
);

Идеята

Дълъг списък: рисувай по-малко

Най-бързият елемент е този, който не е нарисуван. Човек вижда 10–20 продукта на екрана, а не 2000. Затова дългият списък рисува част:

  • „Покажи още“: първите 20, после още 20 при клик. Помни се само колко са показани.
  • Страници: „1, 2, 3 …“, често с номера в адреса (/?page=2), за да се споделя.
  • Виртуален списък (windowing): рисува само редовете, които се виждат, и ги сменя при превъртане. Пише се трудно, затова се ползва библиотека: TanStack Virtual или react-window.

Ключовете от урока за списъците помагат и тук: с key={p.id} добавените 20 продукта са нови елементи, а старите остават, без да се правят наново.

Идеята

Мързеливо зареждане

Не всяка част от страницата трябва веднага. Отзивите за продукта, картата с магазините, редакторът на профила: може да ги зареди едва когато човекът ги отвори. Така първото отваряне тегли по-малко код.

import { lazy, Suspense } from 'react';
const Reviews = lazy(() => import('./Reviews.jsx'));
<Suspense fallback={<p>Зареждам отзивите…</p>}>
<Reviews />
</Suspense>

import('./Reviews.jsx') е функция, която зарежда файла и връща Promise. lazy прави от нея компонент. Докато файлът идва, Suspense показва fallback.

React

Отвори конзолата: редът от reviews.js излиза едва след клика. Честно казано, в пясъчника всички файлове вече са в страницата, затова тук import() само пуска файла по-късно. В истинския проект Vite слага всеки такъв файл отделно и браузърът го тегли от сървъра при първия клик. Същото важи за страниците: с React Router всяка страница може да е lazy.

По-точно, за любопитните

React Compiler. Ако проектът ползва React Compiler, той слага такова запомняне сам при сглобяването на кода, и ръчните memo, useMemo и useCallback стават рядко нужни. Документацията на React го препоръчва за нов код и оставя useMemo и useCallback като начин за точен контрол.

Състоянието надолу. Друга поправка без memo: полето със своя draft става отделен компонент SearchBox, който съобщава нагоре само при „Търси“ (onSearch(draft)). Тогава буквите сменят състояние в SearchBox и App изобщо не се рисува. Това е правилото „толкова високо, колкото трябва, и не по-високо“ от урока за повдигането.

StrictMode. В режим на разработка <StrictMode> рисува всеки компонент по два пъти, за да хване нечисти компоненти. Броячът тогава расте с по две. В събраната версия това го няма.

useMemo е подсказка. React може да изхвърли запомнения резултат и да смята наново. Кодът трябва да е верен и без него.

Режимът на разработка е по-бавен. Предупрежденията и проверките струват време. Мери бързината на събраната версия (vite build и vite preview), а не само при разработка.

Упражнение

Покажи още

Каталогът има 10 продукта и рисува всичките. Покажи първите 4, а под тях „Показани 4 от 10“ и бутон „Покажи още“, който добавя още 4. Когато всички са показани, бутонът изчезва. Проверката брои продуктите след всеки клик и гледа текста и бутона.

React

Тест

Провери се

1. Страницата с продуктите е бавна. Кое е първото нещо, което правиш?
2. ProductList е в memo. App подава products={PRODUCTS.filter(…)} и onAdd={() => add()}. Пишеш в полето на App. Рисува ли се ProductList?
3. Каталогът има 5000 продукта и страницата се отваря бавно. Какво помага най-много?
4. Какво дава мързеливото зареждане на компонент (lazy / defineAsyncComponent) в истинския проект?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·