Казалось бы, оптимизация — важный процесс разработки, но бизнесу зачастую необходимо получить результат дешево, быстро и «вчера», поэтому ей пренебрегают. В итоге растет техдолг, а со временем в поддержку начинают писать недовольные пользователи.
Чтобы не попадать в ситуацию, когда приходится делать заплатки в разных частях приложения и придумывать велосипед, можно использовать несколько подходов. Я расскажу о тех, которыми пользуюсь сам. Итак, поехали!
Оптимизация больших списков
Думаю, многим знакома ситуация, когда вы решаете применить современный подход «бесконечной загрузки» к спискам. Или просто не рассчитали, что количество элементов на странице может быть намного больше, чем вы с аналитиком предполагали вначале.
Проблема довольно явная, если мы не забываем, что ресурсы на клиенте не бесконечные. Чем больше нод, тем больше оперативной памяти тратится на отрисовку нашего приложения.
Нам на помощь приходит виртуализация. Этот подход очень прост, есть популярные библиотеки react-virtualized и react-window, которые закрывают наиболее частые потребности в такой ситуации. Или вы можете написать свою реализацию, если хочется.
Суть подхода: вью отображает только те данные, которые сейчас видны пользователю на странице.


Верстка элементов построена на абсолютном позиционировании и опирается на одинаковый размер элементов в списках и таблицах, однако также есть решения для элементов, которые меняют свой размер (в текущих реалиях с этим практически не случается проблем).
Стоит не забывать про то, что изменение списка на разных устройствах может происходить с разной скоростью. Поэтому стоит предусмотреть «моковое» состояние элемента при очень быстром изменении вью.
Как пример: пользователь быстро листает список вниз, а данные не успевают загрузиться. В таком случае лучше показать «скелетон» или элемент с «моковыми» значениями.
Ссылки на примеры:
– https://react-window.vercel.app/#/examples/list/fixed-size
– https://bvaughn.github.io/react-virtualized/#/components/List
React.memo
Довольно недооцененный HOC (компонент высшего порядка). После перехода React с классового на функциональный подход вместо метода управления обновлением компонентов shouldComponentUpdate и чистых компонентов (PureComponent) пришел React.memo. Данный HOC позволяет нам решать, в какой момент следует обновить дочерний элемент, который он оборачивает.
Аналогично чистым компонентам, если входные свойства не изменяются, рендеринг компонента будет пропущен, что значительно улучшает его производительность и эффективность. Компонент запоминает результаты последнего рендера для заданных входных данных, что способствует повышению общей производительности приложения. Даже в этих компонентах процесс сравнения остается минимальным.

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

Спуск состояния в дочерний компонент
Случаются моменты, когда мы забываем, что изменение состояния компонента запускает цепочку обновлений дочерних компонентов. Мы пишем конструкции в таком стиле:

Спустя время мы замечаем, что setAction, который семантически относится к хедеру и не должен влиять на изменение списка, запускает перерисовку и списка, и фильтров. Выход в такой ситуации следующий:

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

Выше показан абстрактный пример запрета на обновление состояния, который может быть использован как в связке «клиент — сервер», так и отдельно на клиенте. Часто возникают ситуации, когда мы ждем ответа от нужного сервиса, и лишнее обновление будет только усугублять ситуацию. Для устранения «спама» событий обновления состояния есть другие подходы, которые мы рассмотрим далее.
Debounce, throttle и современные хуки React
Функции debounce и throttle — это декораторы, которые позволяют выполнять какое-либо действие по истечении времени. Ниже представлены декораторы хуки для React-приложения (примеры можно найти в репозитории https://github.com/Jock96/react-useful-tools).

Разница в том, что debounce выполняет последнее действие, если другие действия больше не поступают в течение определенного отрезка времени. Throttle, в свою очередь, выполняет действие раз в определенный промежуток времени, а остальные действия игнорирует в данном промежутке.

Яркий пример использования — фильтры, особенно строка поиска. Если у нас нет кнопки для передачи всей формы фильтров на бэк и мы решили давать возможность отправлять запрос на каждое изменение состояния, то нам срочно нужны данные декораторы.
С 18-й версии React предоставляет нам набор хуков для оптимизации: useTransition, useDeferredValue. Первые два хука решают похожую задачу — разделение обновления состояния на приоритеты (больше или меньше). В зависимости от приоритета выполняются сначала срочные изменения, а менее срочные остаются напоследок. Отличие заключается в том, что в useTransition мы явно оборачиваем какую-то логику в startTransition, а с useDeferredValue мы просто используем deferredValue там, где рендеры могут быть отложены.
Версия 19 React добавила нам хук useOptimistic, который позволяет, не дожидаясь выполнения операции, взаимодействовать с элементами на странице путем «оптимистичного» решения, — мы действуем так, словно запрос уже выполнился успешно. Минусы такого подхода очевидны: если запрос выполнился с ошибкой, то откат обновления данных пользователь увидит на форме. Обычно такой кейс можно улучшить с помощью UX — просто информировать пользователя о том, что возникла ошибка и изменения пришлось откатить, с указанием полей, которые вернулись на предыдущее состояние.
Стратегии кеширования и обновления состояния
В другой статье я затронул тему кеширования состояний, когда рассказывал про хук useCacheStrategy. С точки зрения оптимизации это отличный подход для того, чтобы снять лишнюю нагрузку и спам запросов на сервер. Помимо этого, мы можем сконцентрироваться только на кешировании и обновлять состояние на сервере только тогда, когда пользователь сделает определенное действие (пример: нажал кнопку отправить). В сочетании с хуками useTransition и useDeferredValue мы также снизим нагрузку на клиент, чем значительно улучшим ситуацию как на клиенте, так и на сервере.
Не стоит забывать, что мы также можем вручную решать, нужно ли нам обновлять состояние. Достаточно создать функции интерцептор и валидатор, с помощью которых мы будем решать, стоит ли обновлять стейт.

Как видно в примере выше, в функции stateValidator мы проверяем, является ли состояние числом меньше 10, и только тогда разрешаем вызов оригинального setState. Для удобства использования я сделал хук useOptimizedState, который позволяет управлять обновлением состояния (также можно найти в репозитории https://github.com/Jock96/react-useful-tools):

На сегодня у меня всё! В следующих статьях разберем другие подходы к оптимизации, которые обязательно помогут улучшить приложения и пользовательский опыт. Главное — разумно применять полученные знания и избегать преждевременной оптимизации.