Часто вам все равно, сколько раз рендерится ваш компонент. Вы просто хотите сделать работу. Но иногда вы делаете, и когда вы делаете, вы не знаете, как это сделать. Вы знаете, что вы должны профилировать компонент, копаться в инструментах разработки React и разбираться во всех этих пламенных диаграммах!

Но новичкам все это тоже может показаться слишком сложным. Вы знакомы с console.log() вещами только как с отладкой. Но есть глупый способ, если вы работаете над небольшим или средним приложением. Вы можете буквально подсчитать количество повторных рендерингов вашего компонента! когда вы предпринимаете какие-либо действия, например. страница загружена, кнопка нажата, навигация и т. д.

Как только вы узнаете, сколько раз он отображается и где может возникнуть проблема, вы можете приступить к ее решению.

Вот пример дерева компонентов. Вы вдруг забеспокоились, что ваш <SomeDeeplyNestedComponent> слишком часто рендерится.

//App.js
import React from 'react'
import SomeComponent from "./components/SomeComponent";

function App() {
  return (
    <SomeComponent />
  );
}
export default App;
//SomeComponent.js
import React from 'react'
import SomeNestedComponent from './SomeNestedComponent'

const SomeComponent = () => {
    return (
        <SomeNestedComponent />
    )
}
export default SomeComponent
//SomeNestedComponent.js
import SomeDeeplyNestedComponents from './SomeDeeplyNestedComponents'

const SomeNestedComponent = () => {
    return (
        <SomeDeeplyNestedComponents />
    )
}
export default SomeNestedComponent
//SomeDeeplyNestedComponents.js

const SomeDeeplyNestedComponents = () => {
// I am getting Rendered way too often!!
    return (
        <div>SomeDeeplyNestedComponents</div>
    )
}
export default SomeDeeplyNestedComponents

Вот совет:

  1. Определите переменную count вне функции компонента, но в файле компонента и инициализируйте ее значением 0.
  2. Внутри компонента увеличивается переменная
  3. Зарегистрируйте переменную
  4. Делайте это рекурсивно вплоть до компонента верхнего уровня.

Давай сделаем это:

//SomeDeeplyNestedComponents.js
import React from 'react'

let someDeeplyNestedComponentRenderCount = 0
const SomeDeeplyNestedComponents = () => {
    someDeeplyNestedComponentRenderCount++
    console.log("someDeeplyNestedComponentRenderCount:", someDeeplyNestedComponentRenderCount)
    // I am getting Rendered way too often!!
    return (
        <div>SomeDeeplyNestedComponents</div>
    )
}
export default SomeDeeplyNestedComponents

А потом уровень выше..

//SomeNestedComponent.js

import SomeDeeplyNestedComponents from './SomeDeeplyNestedComponents'

let someNestedComponentRenderCount = 0
const SomeNestedComponent = () => {
    someNestedComponentRenderCount++
    console.log("someNestedComponentRenderCount:", someNestedComponentRenderCount)
    return (
        <SomeDeeplyNestedComponents />
    )
}
export default SomeNestedComponent

Вплоть до высшего уровня…

//App.js
import React from 'react'
import SomeComponent from "./components/SomeComponent";

let appRenderCount = 0
function App() {
  appRenderCount++
  console.log("appRenderCount", appRenderCount)
  return (
    <SomeComponent />
  );
}
export default App;

Если у вас включен <React.StrictMode>, вы можете немного запутаться с выводом. Итак, пока вы привыкаете к этому, я рекомендую вам отключить строгий режим временно.

Перейдите к index.js и временно закомментируйте StrictMode.

//index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  // <React.StrictMode>
  <App />
  // </React.StrictMode>
);

Теперь с этим набором начните с чистого листа и выполните действие, которое отобразит ваш <SomeDeeplyNestedComponents />, например. перезагрузка страницы. Перейдите в консоль вашего браузера, и вы можете увидеть что-то вроде этого

Ваша интуиция была верна, ваш <SomeDeeplyNestedComponents /> отображается 14 раз. В зависимости от того, что делают компоненты, вы можете столкнуться с серьезными проблемами производительности, воспринимаемыми пользователем.

Почему это работает?

React создает одностраничное приложение. Когда ваше приложение загружается впервые, оно запускает функцию, определенную в index.js, т.е. App.js. Компонент <App/> встречает <SomeComponent/> в своем ответе, поэтому react пытается визуализировать эти компоненты, и этот процесс продолжается до тех пор, пока не останется рендеринга. Этот процесс загружает в память все переменные счетчика, определенные в отдельных файлах компонентов.

Компоненты - это просто функции. Библиотека реагирования, вызывающая функцию компонента, и компонент, который снова визуализируется, эквивалентны. В функциях Javascript известны переменные, определенные вне их. При первом вызове функции реакцией переменные счетчика, определенные вне функций, увеличиваются на 1. Каждый последующий рендеринг, т. е. каждый последующий вызов функции, обновляет соответствующую переменную счетчика, доступную в памяти. Результат: вы получаете количество рендеров, подсчитывая, сколько раз функция Component была вызвана React.

Как анализировать логи?

Помните, что в React компонент перерисовывается тогда и только тогда, когда происходит одно из трех событий. Это самая важная часть.

  1. Состояние компонентов изменилось
  2. Свойство компонента изменено
  3. Родительский компонент компонентов повторно визуализируется.

Правила настолько просты для запоминания, но зачастую серьезность их важности недооценивается.

Каждый дочерний компонент будет отображаться столько раз, сколько раз отображается его родительский компонент (если не применяется какой-либо особый метод оптимизации)

<SomeDeeplyNestedComponents /> будет отображаться как минимум столько раз, сколько <SomeNestedComponent/> (в этом примере 8)

Дополнительный рендеринг (14–8=6) может исходить от самого <SomeDeeplyNestedComponents />. Это может быть связано с тем, что состояние, определенное в самом компоненте, изменяется чаще, чем необходимо, из-за плохого кода. Внимательно изучите все вызовы обновления состояния; (состояние - это все состояние, определенное либо useState, useReducer, либо значением с отслеживанием состояния, полученным из вашего собственного customHook или библиотечного хука (например, useSelector()).

Таким образом, вы проверяете, есть ли ненужное обновление состояния внутри самого компонента.

Далее вы поднимаетесь на уровень выше и обнаруживаете, что <SomeNestedComponent /> рендерится 8 раз, то есть в 4 раза больше, чем <SomeComponent/>. Вы применяете одни и те же принципы рекурсивно до компонента верхнего уровня <App/>, пока не будете удовлетворены.

Теперь вы можете получить такой вывод:

Как на самом деле свести к минимуму проблему рендеринга, выходит за рамки этой статьи. Вы найдете много онлайн-ресурсов, чтобы сделать это. На мой взгляд, оптимизация в смысле минимизации повторного рендеринга в целом делится на две категории.

  1. Используйте специальные API-интерфейсы оптимизации, предоставляемые библиотекой React.

К счастью, их всего несколько: useCallback(), useMemo(), React.memo().

Обратите внимание: эти API вступают в игру, когда вы знаете, что делаете. Вы знакомы с использованием инструментов разработчика React, вы профилировали компоненты и измерили время, необходимое для повторного рендеринга. Ваша цель здесь — сэкономить последние миллисекунды. Вы знаете о затратах на производительность при использовании API-интерфейсов оптимизации (да! вы правильно прочитали, оптимизация требует затрат на производительность :-)).

А вот вторая категория интереснее! Люди часто обращаются к первой категории под предлогом того, что они кажутся мудрыми и старшими, в то время как у них есть более фундаментальная или глупая проблема.

2. Используйте стандартные API, такие как useState(), useEffect() и т. д., по назначению, при правильном использовании и понимании предостережений относительно неправильного использования.

Список огромен в этом случае. Но я выделю несколько, которые мне больше всего нравятся.

  1. Ненужное использование useEffect()

Это, безусловно, главная причина, по которой компонент часто отображает больше, чем он предназначен. В течение многих лет useEffect() оставался полной загадкой, и только настоящие эксперты могли отследить, что на самом деле происходит с useEffect(). Проблема настолько узкая, что когда команда React переписала документы React, у них был специальный раздел для этого: Возможно, вам не нужен useEffect()

Непонимание проблемы с замыканиями при использовании useEffect() также является важной подтемой в этом заголовке.

2. Экстравагантное использование глобального состояния через Redux

В большинстве средних и малых приложений очень немногие состояния должны быть глобальными. Данные часто всегда обновляются для создания представления. У некоторых людей есть понятие "Поместить все в Redux Store" и "Получить все из Store". Часто они передают данные в хранилище и используют из хранилища из одного и того же компонента и только из этого компонента. Состояние, которое передается для сохранения, не используется вне этого компонента.

Это может быстро превратиться в беспорядок. Сложная структура хранилища и логика редьюсера — это только поверхностная проблема.

Лед под водой не понимает отношения неизменяемых обновлений состояния к хранилищу избыточности и тому, как работает подписка useSelector(). Я имею в виду, как сравнение строгого равенства (’===’) терпит неудачу при извлечении не примитивных значений (например, объектов и массивов) из хранилища через useSelector(). Золотое правило здесь заключается в следующем: «Извлекайте из хранилища только наименьшую возможную часть состояния (если возможно, только примитивное значение) и воздерживайтесь от ограничения значения ссылочного типа, где это возможно»

например, если у вас есть объект user {} в магазине. В компоненте самого верхнего уровня вам нужно только свойство role из пользовательского объекта для отображения ваших маршрутов. Скорее всего, у вас есть код, похожий на этот:

//AppRoutes.js  
import { useSelector } from "react-redux"  

const AppRoutes = () => {    
//Bad Code!!     
const user = useSelector(store => store.user)     
 return ( 
<> 
<Routes> 
    <Route 
      path="/" 
      element={<Navigate replace to={user.roleId === 'admin' ? '/home/admin' : '/home/user'} />}/>            
   </Routes>      
   </>     
)}

export default AppRoutes

Что произойдет здесь, так это то, что любое обновление для хранения, независимо от того, какую часть хранилища вы обновляете, но поскольку вы неизменно обновляете хранилище, объект хранилища является новой копией. useSelector() повторно запускается на новой копии и просматривает часть состояния, которая нужна этому компоненту user{} в данном случае. Теперь он пытается угадать/определить, изменился пользовательский объект или нет, сравнивая его со старой копией. В Javascript мы знаем, что

let obj1 = {
    name: 'Aadil',
}
let obj2 = {
    name: 'Aadil'
}

obj1 === obj2 //false

useSelector() выполняет строгую проверку на равенство (’===’) и приходит к выводу, что объекты не совпадают, поэтому компонент необходимо перерисовать. :-(

Каждый раз, когда обновление отправляется в хранилище, даже из компонента, находящегося в самом низу иерархии компонентов, этот компонент верхнего уровня повторно отображается, что фактически приводит к повторному отображению всего приложения и запускает повторный рендеринг его подкомпонентов, включая те, которые не нуждался в повторном рендеринге. Вы сделали что-то близкое к полной перезагрузке страницы.

Если бы мы извлекли примитивное значение, такое как роль String, строгая проверка на равенство прошла бы как true, и useSelector никогда бы не уведомил компонент о повторном рендеринге.

//AppRoutes.js

import { useSelector } from "react-redux"


const AppRoutes = () => {

    const role = useSelector(store => store.user?.role)

    return (
        <>
            <Routes>
                <Route
                    path="/"
                    element={<Navigate replace to={role === 'admin' ? '/home/admin' : '/home/user'} />}
                />
            </Routes>
        </>
    )
}

Чтобы немного облегчить эту проблему, существует библиотека под названием reselect, которая позволяет нам писать селекторы, которые в этом отношении осторожны.

Это все от меня сегодня!

Избегайте этих ловушек и наслаждайтесь созданием действительно быстрых и реактивных приложений!