Как оптимизировать производительность веб-приложений с помощью React
Согласно исследованию Portent, более высокая скорость загрузки сайта предполагает увеличение потенциального дохода. Оптимизация производительности приложений имеет большое значение для разработчиков, которым важно предоставлять пользователям положительный опыт применения.
Преимущества оптимизации:
- Улучшение пользовательского опыта
- Усовершенствованное SEO
- Снижение уровня отказов
- Экономия затрат за счет уменьшения использования серверных ресурсов и повышения эффективности кода
- Конкурентное преимущество
Основные принципы оптимизации производительности в React
Компоненты и рендеринг
Для приложений на основе React можно обеспечить быстрый пользовательский интерфейс при правильной настройке и оптимизации. Однако по мере роста приложения можно столкнуться со снижением производительности.
Компоненты позволяют структурировать интерфейс на независимые части, которые можно многократно использовать. Компоненты работают аналогично стандартным функциям JavaScript, принимая различные аргументы (props) и возвращая React-элементы.
Если состояние компонента изменилось, React создаёт виртуальное DOM-дерево и сравнивает его с предыдущим состоянием. Это позволяет минимизировать прямые изменения в реальном DOM, что ускоряет повторный рендеринг страниц. Однако сложные структуры могут замедлить выполнение программы.
Рендеринг в DOM. Допустим, в вашем HTML-файле есть <div>:
<div id="root"></div>
Содержимым управляет DOM, поэтому его можно назвать корневым узлом (как правило, корневой компонент здесь один). При встраивании React рендерить можно в любом количестве независимых корневых элементов.
Вызов ReactDOM.render() для отрисовки элемента в корневой узел:
const element = <h1>Hello, world</h1>; ReactDOM.render(element, document.getElementById('root'));
На странице будет написано "Hello, world".
Использование ключей в списках
Ключи (keys) помогают идентифицировать и управлять списком элементов. Ключи должны быть заданы внутри массива, чтобы обеспечить им постоянный идентификатор. Обычно в качестве ключей используются уникальные идентификаторы элементов.
const numbers = [1, 2, 3, 4, 5] const listItems = numbers.map((number) => ( <li key={number.toString()}>{number}</li> ))
Наиболее удобным выбором ключа является использование уникального идентификатора, такого как ID:
const todoItems = todos.map((todo) => ( <li key={todo.id}>{todo.text}</li> ))
Не рекомендуется использовать индексы массива в качестве ключей, если порядок элементов может изменяться, так как это может привести к ошибкам и лишним ререндерингам.
Применение React Profiler
Что такое React Profiler
React Profiler — это инструмент для профилирования производительности (react performance profiling) React-приложений. Он позволяет измерить скорость работы рендеринга и выявить узкие места.
<Profiler id="App" onRender={onRender}> <App /> </Profiler>
Использование Profiler увеличивает нагрузку на CPU и память, поэтому его стоит применять только при необходимости.
Как использовать React Profiler для выявления проблем с производительностью
Инструмент измеряет производительность и предоставляет подробный отчет о работе приложения: сколько времени тратится на рендеринг и где могут появиться узкие места. Эта информация крайне важна для оптимизации.
Profiler API — часть основной библиотеки React, предназначенная для сбора данных о времени рендеринга каждой составляющей. Profiler API позволяет внимательно изучить эффективность рендеринга и выявить возможные проблемы.
Пример:
import React, { Profiler } from 'react'; function MyComponent() { return ( <Profiler id="MyComponent" onRender={callback}> <div>My Component</div> </Profiler> ); }
В этом примере компонент MyComponent обернут элементом Profiler. Это позволяет собирать данные о производительности MyComponent и его дочерних компонентов. Обратный вызов onRender будет вызываться каждый раз, когда элемент в обернутом дереве обновляется на экране, предоставляя информацию о времени рендеринга. Это может помочь выявить те участки, которые требуют оптимизации.
Методы оптимизации кода в React
Мемоизация компонентов
Мемоизация — это техника оптимизации, при которой результат выполнения функции сохраняется в кэше и используется при повторных вызовах с теми же аргументами. Это особенно полезно при работе с дорогостоящими или часто вызываемыми функциями с одинаковыми входными значениями. Мемоизация помогает избежать избыточных вычислений и повышает общую эффективность.
Метод мемоизации React.memo() способен оборачивать чисто функциональные элементы, чтобы предотвратить повторную визуализацию если полученные реквизиты остаются неизменными.
import React from 'react'; const Post = ({ signedIn, post }) => { console.log('Rendering Post'); return ( <div> <h2>{post.title}</h2> <p>{post.content}</p> {signedIn && <button>Edit Post</button>} </div> ); }; export default React.memo(Post
Оптимизация через хуки: useMemo и useCallback
Хук useMemo() запоминает результат вызова функции или дорогостоящего вычисления. Результат кэшируется и пересчитывается только при изменении входных значений.
import React, { useMemo } from 'react'; function App() { const [count, setCount] = React.useState(0); const [otherState, setOtherState] = React.useState(''); const expensiveComputation = (num) => { let i = 0; while (i < 1000000000) i++; return num * num; }; const memoizedValue = useMemo(() => expensiveComputation(count), [count]); return ( <div> <p>Count: {count}</p> <p>Square: {memoizedValue}</p> <button onClick={() => setCount(count + 1)}>Increase Count</button> <input type="text" onChange={(e) => setOtherState(e.target.value)} /> </div> ); } export default App;
В коде expensive Computation функция имитирует ресурсоемкую операцию. UseMemo используется для кэширования результата вычисления. Запомненное значение, сохраненное в memoized Value, пересчитывается, если меняется состояние. Кнопка Increase Count увеличивает count состояние и вызывает перерасчет сохраненного значения.
И наоборот, изменение otherState через поле ввода не приводит к перерасчету, поскольку otherState не включено в useMemo массив зависимостей.
Хук useCallback() используется для запоминания функции. Действие предотвращает повторный рендеринг при передаче событий в качестве свойств дочерним элементам.
Пример использования хука useCallback():
import React, { useState, useCallback } from 'react'; const ParentComponent = () => { const [count, setCount] = useState(0); // Define a function that increments the count state const incrementCount = () => { setCount(count + 1); }; // Memoize the incrementCount function using useCallback const memoizedIncrement = useCallback(incrementCount, [count]); return ( <div> <p>Count: {count}</p> <ChildComponent onIncrement={memoizedIncrement} /> </div> ); }; const ChildComponent = React.memo(({ onIncrement }) => { console.log('Child component rendered'); return ( <div> <button onClick={onIncrement}>Increment Count</button> </div> ); }); export default ParentComponent;
В этом коде Parent Component отвечает за управление переменной состояния с именем count и представляет функцию с именем increment Count, которая обрабатывает увеличение счетчика. Increment Count, используя useCallback хук, гарантирует ее стабильность при рендеринге, если только какая-либо из ее зависимостей не претерпит изменений.
UseCallback следует использовать для улучшения работоспособности только для критически важных частей программы. Стоит всегда измерять влияние до и после применения useCallback, чтобы получить желаемый эффект.
Разделение кода
Разделение кода (code splitting) — метод для распределения большого пакета JavaScript на более мелкие и управляемые фрагменты. Разделение повышает эффективность за счет загрузки только необходимого кода для определенной части приложения.
Пример:
// AsyncComponent.js import React, { lazy, Suspense } from 'react'; const DynamicComponent = lazy(() => import('./DynamicComponent')); const AsyncComponent = () => ( <Suspense fallback={<div>Loading...</div>}> <DynamicComponent /> </Suspense> ); export default AsyncComponent; // DynamicComponent.js import React from 'react'; const DynamicComponent = () => ( <div> <p>This is a dynamically loaded component!</p> </div> ); export default DynamicComponent;
В этом примере компонент AsyncComponent выполняет разделение кода с помощью React.lazy, который позволяет загружать компоненты "лениво" (lazy-loading). Это помогает уменьшить размер основного пакета (bundle) и загружать только необходимый код.
import { useState } from 'react'; function MainComponent() { const [isModalDisplayed, setModalDisplayed] = useState(false); const [ModalComponent, setModalComponent] = useState(null); const loadModalComponent = async () => { const loadResult = await import('./components/Modal.js'); setModalComponent(() => loadResult.default); }; return ( <div> {isModalDisplayed && ModalComponent ? <ModalComponent /> : null} <button onClick={() => { setModalDisplayed(true); loadModalComponent(); }} >Load Modal Component</button> </div> ); }
В примере динамический импорт используется для загрузки ModalComponent только тогда, когда пользователь нажимает кнопку.
Suspense — это функция для улучшения взаимодействия с пользователем через управление асинхронными операциями (выборку данных). Функция приостанавливает отрисовку до выполнения определенного условия и отображает запасной вариант fallback.
Вот пример с применением Suspense:
import React from 'react'; import { Suspense } from 'react'; function App() { return ( <Suspense fallback={<div>Загрузка...</div>}> <AsyncComponent /> </Suspense> ); } function AsyncComponent() { const data = fetchData(); return ( <ul> {data.map((item) => ( <li key={item.id}>{item.title}</li> ))} </ul> ); } function fetchData() { return Promise.resolve({ id: 1, title: 'Заголовок 1' }); } export default App;
В примере выше Suspense отображает временный пользовательский интерфейс до тех пор, пока AsyncComponent не завершит асинхронную операцию. После успешной загрузки данных отобразится фактический пользовательский список компонентов.
Инструменты для анализа производительности
Как использовать DevTools для профилирования приложений
DevTools — это инструменты разработчика, встроенные в браузер, которые упрощают процесс отладки и улучшения кода веб-приложений.
Вкладка расширения «профилировщик» запускает и останавливает сеансы профилирования, помогает просматривать собранные данные в различных форматах и проверять отдельные коммиты.
Для начала профилирования нужно нажать кнопку записи:
Вкладка «производительность» помогает понять последовательность событий, которые приводят к определенному состоянию. Нужно нажать кнопку «Stop» по окончании profiling (профилирования).
Результат группируется по коммитам:
Более детальное представление об эффективности программы можно получить из столбцов диаграммы — цвет и высота каждого соответствуют времени рендеринга этого коммита.
Цель профилирования производительности — проанализировать собранные данные и использовать их для оптимизации приложения.
Практические советы и рекомендации
Избегание ненужных рендеров
Предотвращение ненужной визуализации в React подразумевает оптимизацию компонентов так, чтобы они перерисовывались только при изменении их пропсов. Это достигается следующими методами:
- Мемоизация с использованием хуков useMemo() и useCallback(), которые позволяют кэшировать результаты функций и предотвращать их избыточный вызов.
- Оптимизация вызовов API с применением React Query. React Query помогает кэшировать и синхронизировать данные с сервером, минимизируя количество запросов и рендеров.
- Создание мемоизированных селекторов с помощью Reselect. Reselect позволяет создавать селекторы, которые мемоизируют результаты и предотвращают избыточные вычисления.
Оптимизация использования сторонних библиотек
Одновременная обработка больших данных может привести к снижению работоспособности программы. Чтобы избежать этого, можно использовать виртуализацию. Этот метод поможет выделить только видимые данные и загрузить дополнительные по мере прокрутки пользователем.
Виртуализации можно добиться с помощью сторонних библиотек React-Window или React-Virtualized. Они отображают только видимые данные и загружают больше данных по мере необходимости, чтобы повысить производительность приложений.
Лучшие практики по работе с DOM
В JavaScript можно управлять содержимым веб-страницы с помощью объектной модели документа (DOM). Приведем в этой статье лучшие практики написания кода, который будет легко читаем, прост в поддержке и эффективен:
- Использование DOMContentLoaded события гарантирует, что код манипуляции с помощью объектной модели документа запустится только после полной загрузки документа.
- React.memo() уменьшит количество ненужных повторных отрисовок.
- Рендеринг на стороне сервера и code splitting поможет сократить время начальной загрузки.
- Кэширование выбранных элементов и сохранение результата в переменных.
- Проще поддерживать классы CSS по сравнению со встроенными стилями.
- Делегирование событий помогает уменьшить количество прослушивателей событий, снижая нагрузку на код.
- Обновление пакета DOM с помощью фрагмента сокращает количество перекомпоновок и делает код более эффективным.
- Использование метода stopPropagation для управления потоком событий в DOM.
- Тестирование code манипуляции с DOM гарантирует ожидаемое поведение.
Важность постоянного мониторинга и оптимизации производительности
Постоянный мониторинг и оптимизация производительности приложений — важные задачи для разработчиков, чтобы поддерживать высокую оценку пользователей. Медленная загрузка может привести к разочарованию пользователей и потере доходов. Особенно сейчас, когда клиенты ожидают быстрых ответов с возможностью работы на различных устройствах и сетевых условиях.
React сочетает скорость JavaScript с уникальными возможностями рендеринга, что делает приложения более отзывчивыми и продуктивными.