React Hooks делают нашу жизнь намного лучше почти во всех отношениях . Но если производительность становится проблемой, все становится немного сложнее. Вы можете писать невероятно быстрые приложения с помощью хуков, но прежде чем вы это сделаете, вам следует знать кое-что.
Стоит ли запоминать?
Реагировать достаточно быстро для большинства случаев использования. Если ваше приложение достаточно быстрое и у вас нет проблем с производительностью, эта статья не для вас. Решение воображаемых проблем с производительностью - это реальная задача, поэтому, прежде чем начинать оптимизацию, убедитесь, что вы знакомы с React Profiler
.
Если вы определили сценарии, в которых рендеринг идет медленно, лучше всего подойдет мемоизация.
React.memo
- инструмент оптимизации производительности, компонент высшего порядка. Он похож на React.PureComponent
, но для функциональных компонентов, а не для классов. Если ваш функциональный компонент отображает тот же результат при тех же реквизитах, React запомнит, пропустит рендеринг компонента и повторно использует последний обработанный результат.
По умолчанию он будет только поверхностно сравнивать сложные объекты в объекте props. Если вы хотите контролировать сравнение, вы также можете предоставить настраиваемую функцию сравнения в качестве второго аргумента.
Нет мемоизации
Давайте рассмотрим пример, в котором мы не используем мемоизацию и почему это может вызвать проблемы с производительностью.
function List({ items }) { log('renderList'); return items.map((item, key) => ( <div key={key}>item: {item.text}</div> )); } export default function App() { log('renderApp'); const [count, setCount] = useState(0); const [items, setItems] = useState(getInitialItems(10)); return ( <div> <h1>{count}</h1> <button onClick={() => setCount(count + 1)}> inc </button> <List items={items} /> </div> ); }
Пример 1: Живая демонстрация
Каждый раз, когда нажимается inc
, регистрируются и renderApp
, и renderList
, даже если для List
ничего не изменилось. Если дерево достаточно большое, оно легко может стать узким местом в производительности. Нам нужно уменьшить количество рендеров.
Простая мемоизация
const List = React.memo(({ items }) => { log('renderList'); return items.map((item, key) => ( <div key={key}>item: {item.text}</div> )); }); export default function App() { log('renderApp'); const [count, setCount] = useState(0); const [items, setItems] = useState(getInitialItems(10)); return ( <div> <h1>{count}</h1> <button onClick={() => setCount(count + 1)}> inc </button> <List items={items} /> </div> ); }
Пример 2: Живая демонстрация
В этом примере мемоизация работает правильно и сокращает количество отрисовок. Во время монтирования регистрируются renderApp
и renderList
, но при щелчке по inc
регистрируется только renderApp
.
Воспоминание и обратный звонок
Внесем небольшие изменения и добавим кнопку inc
ко всем List
элементам. Остерегайтесь, передача обратного вызова мемоизированному компоненту может вызвать небольшие ошибки.
function App() { log('renderApp'); const [count, setCount] = useState(0); const [items, setItems] = useState(getInitialItems(10)); return ( <div> <div style={{ display: 'flex' }}> <h1>{count}</h1> <button onClick={() => setCount(count + 1)}> inc </button> </div> <List items={items} inc={() => setCount(count + 1)} /> </div> ); }
Пример 3: Живая демонстрация
В этом примере наша мемоизация не работает. Поскольку мы используем встроенную лямбду, для каждого рендера создается новая ссылка, что делает React.memo
бесполезным. Нам нужен способ запоминания самой функции, прежде чем мы сможем запоминать компонент.
4. useCallback
К счастью, в React для этого есть два встроенных хука: useMemo
и useCallback
. useMemo полезен для дорогостоящих вычислений, useCallback полезен для передачи обратных вызовов, необходимых для оптимизированных дочерних компонентов.
function App() { log('renderApp'); const [count, setCount] = useState(0); const [items, setItems] = useState(getInitialItems(10)); const inc = useCallback(() => setCount(count + 1)); return ( <div> <div style={{ display: 'flex' }}> <h1>{count}</h1> <button onClick={inc}>inc</button> </div> <List items={items} inc={inc} /> </div> ); }
Пример 4: Живая демонстрация
В этом примере наша мемоизация снова не работает .renderList
вызывается каждый раз, когда inc
нажимается. По умолчанию useCallback
вычисляет новое значение всякий раз, когда передается новый экземпляр функции. Поскольку встроенные лямбды создают новый экземпляр во время каждого рендеринга, useCallback
с конфигурацией по умолчанию здесь бесполезен.
5. useCallback с вводом
const inc = useCallback(() => setCount(count + 1), [count]);
Пример 5: Живая демонстрация
useCallback
принимает второй аргумент, массив входных данных, и только если эти входные данные изменятся, useCallback
вернет новое значение. В этом примере useCallback
будет возвращать новую ссылку каждый раз, когда count
изменяется. Поскольку count
изменяется во время каждого рендеринга, useCallback
будет возвращать новое значение во время каждого рендеринга. Этот код также не запоминается.
useCallback с вводом пустого массива
const inc = useCallback(() => setCount(count + 1), []);
Пример 6: Живая демонстрация
useCallback
может принимать в качестве входных данных пустой массив, который вызовет внутреннюю лямбду только один раз и запомнит ссылку для будущих вызовов. Этот код запоминает, один renderApp
будет вызываться при нажатии любой кнопки, основная inc
кнопка будет работать правильно, но внутренние inc
кнопки будут перестать работать правильно .
Счетчик увеличится с 0
до 1
и после этого остановится. Лямбда создается один раз, но вызывается несколько раз. Поскольку при создании лямбда-выражения count
равно 0
, он ведет себя точно так же, как приведенный ниже код:
const inc = useCallback(() => setCount(1), []);
Основная причина нашей проблемы в том, что мы пытаемся читать и писать из состояния и в одно и то же время. Нам нужен API, предназначенный для этой цели. К счастью для нас, React предоставляет два способа решения проблемы:
useState с функциональными обновлениями
const inc = useCallback(() => setCount(c => c + 1), []);
Пример 7: Живая демонстрация
Сеттеры, возвращаемые useState
, могут принимать функцию в качестве аргумента, где вы можете прочитать предыдущее значение данного состояния. В этом примере мемоизация работает правильно, без ошибок.
useReducer
const [count, dispatch] = useReducer(c => c + 1, 0);
Пример 8: Живая демонстрация
useReducer
мемоизация в этом случае работает точно так же, как useState
. Поскольку dispatch
гарантированно имеет одну и ту же ссылку во всех отрисовках, useCallback
не требуется, что делает код менее подверженным ошибкам для ошибок, связанных с мемоизацией.
useReducer против useState
useReducer
больше подходит для управления объектами состояния, которые содержат несколько вложенных значений или когда следующее состояние зависит от предыдущего. Общий шаблон использования useReducer
- с useContext
, чтобы избежать явной передачи обратных вызовов в большом дереве компонентов.
Я рекомендую эмпирическое правило: в основном использовать useState
для данных, которые не покидают компонент, но если требуется нетривиальный двусторонний обмен данными между родителем и потомками, useReducer
- лучший выбор.
Подводя итог, React.memo
и useReducer
- лучшие друзья, React.memo
и useState
- братья и сестры, которые иногда ссорятся и создают проблемы, useCallback
- ближайший сосед, с которым всегда следует быть осторожным.