React Server Components: почему рендеры идут 5x чаще, чем должны
-
Все говорят, что Server Components - это спасение: меньше JS в браузере, быстрее загрузка, данные прямо в компоненте. Звучит как панацея. Но когда ты начинаешь их юзать в реальном приложении, оказывается, что рендеры идут чаще, чем раньше, а сам компонент работает странно. И это не баг - это архитектурная грабля, на которую наступают почти все.
Дело в том, что Server Components и SSR - это разные зверюшки, хотя на первый взгляд кажутся братьями-близнецами. Путаница в этом месте стоит в основе всех проблем с производительностью. Давайте разберёмся, что происходит под капотом, и почему твоё приложение дёргается как эпилептик при каждом обновлении данных.
Server Components vs SSR: в чём подвох
Очень часто путают эти два подхода, и это приводит к печальным последствиям. SSR рендерит HTML на сервере, но весь JavaScript всё равно уходит в браузер для гидратации. Компонент сначала отрисовывается на клиенте в виртуальном DOM, потом браузер сравнивает это с тем, что пришло с сервера, и если они совпадают - отлично, интерактивность включена. Если не совпадают - поздравляем, у тебя гидратационная ошибка и перерендер всего поддерева.
Server Components - это совсем другая история. Они вообще не попадают в браузер - ни код, ни логика. Сервер рендерит компонент один раз при запросе страницы, отправляет готовый HTML (или специальный формат, понятный React), и всё. Компонент не рендерится на клиенте, потому что его там нет. Это кардинально меняет правила игры.
Проблема начинается, когда ты смешиваешь эти подходы неправильно:
- SSR с неправильной гидратацией: клиентский рендер расходится с серверным, и React перерендеривает всё поддерево, чтобы синхронизировать.
- Server Components, куда вставлены клиентские хуки: компонент обязан отрендериться и на сервере, и на клиенте, удваивая работу.
- Импорты между слоями: если серверный компонент импортирует что-то из клиентского контекста, он теряет смысл серверности.
Как Next.js определяет, где рендериться компоненту
В React 19 и Next.js 13+ работает простое правило: если компонент импортирован в серверный контекст - он серверный. Если в клиентский - он клиентский. Но вот что забывают многие: если ты в серверном компоненте попытаешься использовать
useStateили другой клиентский хук, ничего не сломается сразу - но всё начнёт вести себя странно.Этот момент - как раз то, почему рендеры идут чаще. Когда ты случайно втащишь клиентскую логику в серверный компонент (например, передашь её как children или пропс), Next.js вынужден пометить весь компонент как клиентский. Результат: компонент рендерится дважды - сначала на сервере для гидратации, потом на клиенте, когда приходит интерактивность. Это не ошибка - это нарушение архитектуры.
Как это выглядит на практике:
- Каждый раз, когда состояние компонента меняется на клиенте, React перерендеривает его.
- Если компонент был помечен как серверный, но содержит клиентскую логику, он рендерится снова на клиенте.
- Если в серверном компоненте есть интерактивные элементы (кнопки, инпуты), вся логика валится на клиента, и сервер становится бесполезен.
Скрытые рендеры: когда ты думаешь, что оптимизируешь
Здесь начинается самое интересное. Разработчики видят, что компонент медленный, и начинают тащить туда
React.memo,useCallback,useMemo. Классическая ошибка.Проблема в том, что если компонент изначально архитектурно неправильный - неважно, насколько хорошо ты его оптимизируешь, рендеры всё равно будут идти паразитными путями. Это как красить ржавый забор - внешне выглядит лучше, но изнутри гниль разъедает конструкцию. Правильная организация компонента решает 90% всех проблем оптимизации. Остальные 10% - это мелкая суета.
Вот классический сценарий, на котором наступают многие:
-
Импорт серверного компонента в клиентский. Ты пишешь клиентский компонент вроде
'use client', а потом пытаешься импортировать туда серверный компонент в видеchildrenили пропса. Next.js вынужден сделать серверный компонент, по сути, клиентским, чтобы его можно было передать. Результат: сервер теряет свою ценность. -
Неправильная граница между слоями. Когда граница между серверными и клиентскими компонентами размыта, рендеры идут в обе стороны. Компонент рендерится на сервере для выдачи HTML, потом рендерится на клиенте для гидратации, потом рендерится ещё раз, когда приходит интерактивность. Три рендера вместо одного.
-
Забывчивость про директиву
'use client'. Если ты забыл добавить'use client'в клиентском компоненте, Next.js может попытаться рендерить его на сервере, а потом второй раз на клиенте. Это не приводит к крашу, но производительность падает.
Как это проявляется:
- Бесполезные перерендеры родительских компонентов, когда меняется состояние одного ребёнка.
- Компонент рендерится при каждом обновлении страницы, даже если данные не изменились.
- В DevTools видно, что компонент подсвечивается как обновляющийся, хотя ты ничего не трогал.
Как вообще должна работать архитектура
Чтобы избежать лишних рендеров, нужно чётко разделить ответственность. Серверные компоненты - для логики, данных и статичного HTML. Клиентские компоненты - только для интерактивности. И никогда они не должны смешиваться на одном уровне.
Типичная ошибка - когда разработчик пишет большой компонент, который пытается быть и серверным, и клиентским одновременно. Это невозможно. Нужно разбить его на две части: серверная часть запрашивает данные и отправляет готовый HTML, клиентская часть добавляет интерактивность и слушает события пользователя.
Правильная архитектура выглядит так:
- Серверный компонент: запрашивает данные напрямую из БД, форматирует их, передаёт клиентскому компоненту как пропсы.
- Клиентский компонент: получает уже готовые данные, добавляет интерактивность, слушает события.
- Граница между ними: ясная и непроницаемая.
Когда ты правильно разбиваешь компоненты, исчезают паразитные рендеры. Серверный компонент рендерится один раз при запросе. Клиентский компонент рендерится только когда меняется его состояние или пропсы. Никаких двойных проходов, никаких гидратационных ошибок.
Практическое правило при проектировании:
- Если компоненту нужны хуки (
useState,useEffect, обработчики событий) - он клиентский. - Если компоненту нужны данные из БД или файловой системы - он серверный.
- Если компоненту нужны оба - ты неправильно разбил архитектуру, вернись назад и переделай.
- Никогда не передавай серверный компонент как пропс в клиентский - это гарантированно вызовет лишние рендеры.
Как убить утечку памяти в процессе
Есть ещё один скрытый враг - утечки памяти, которые часто идут рука об руку с неправильной архитектурой. Когда компонент рендерится несколько раз, а код неправильно организован, в памяти могут накапливаться подписки на события, ссылки на данные, закрытия функций.
Например, если в серверном компоненте ты создаёшь соединение с БД или открываешь файл, а потом компонент случайно рендерится ещё раз (из-за архитектурной ошибки), соединение может остаться висящим. Это не сразу приводит к краху, но со временем приложение начинает жрать память как сумасшедший.
Проблема особенно острая, если в приложении много компонентов, которые неправильно архитектурно разделены. Каждый лишний рендер - это потенциальная утечка.
Как минимизировать утечки:
- Всегда закрывай соединения и подписки в
useEffectс правильной очисткой (cleanup функция). - Не создавай объекты и функции внутри
render, если они не нужны при каждом рендере. - Помни: серверные компоненты рендерятся один раз, поэтому там можно спокойнее работать с ресурсами.
- Клиентские компоненты рендерятся много раз - следи за утечками там тщательнее.
Реальный пример: что может пойти не так
Представь, у тебя есть страница продукта с деталями, комментариями и связанными товарами. Ты решил: вот я напишу один большой серверный компонент, который запросит всё данные и отправит клиенту готовый HTML.
Оно так и работает, пока пользователь не кликает кнопку “Добавить в корзину” или “Оставить комментарий”. Тут тебе нужна интерактивность. Ты добавляешь
useStateв компонент. Готово - теперь это клиентский компонент (явно или неявно). Он рендерится сначала на сервере для SSR, потом на клиенте для гидратации, потом снова, когда пользователь кликает кнопку. Три рендера вместо одного.Правильный подход: раздели компонент. Серверная часть запрашивает и отправляет данные. Клиентская часть обёртывает её и добавляет кнопки с обработчиками. Результат: серверный компонент рендерится один раз, клиентский - только когда нужна интерактивность. Производительность скачет.
Как проверить, что ты делаешь правильно
Есть простой способ убедиться, что архитектура правильная. Открой DevTools React, включи “Highlight updates when components render”, и посмотри, что подсвечивается.
Если видишь, что при каждом действии пользователя подсвечивается весь компонент и его родители - пиши архитектуру заново. Если видишь, что подсвечивается только конкретная часть, которая должна измениться - ты на правильном пути.
Ещё один трюк: отключи JavaScript в браузере и посмотри, загружается ли страница вообще. Если загружается и выглядит правильно - твои серверные компоненты работают. Если ничего не видно - значит, ты загрузил слишком много логики на клиент.
Что проверить в своём коде:
- Все ли компоненты с
'use client'действительно нуждаются в интерактивности? - Не передаёшь ли ты серверные компоненты как пропсы клиентским?
- Не рендерится ли один и тот же компонент несколько раз в разных контекстах?
- Используешь ли ты лишние зависимости в
useEffectилиuseMemo?
Итоговая сборка: план действий
Server Components - это не просто новая фича, это совсем другой способ думать об архитектуре. Лишние рендеры идут не потому, что React глупый, а потому что ты смешиваешь серверный и клиентский код в одном компоненте.
Запомни одно правило: если компоненту нужны хуки - он клиентский. Если нужны данные из БД - он серверный. Никаких гибридов, никаких смешивания. Граница должна быть острой, как бритва. Когда ты разберёшься с архитектурой, рендеры встанут на свои места, производительность прыгнет, а код станет чище и проще для поддержки.
Остаётся ещё кучу нюансов: как правильно кешировать данные на сервере, почему
revalidateиногда не срабатывает, как не наступить на грабли с гидратацией в React 19. Но это уже тема для отдельного разговора. Главное - понять, что лишние рендеры не падают с неба, они результат неправильно спроектированной архитектуры. Исправишь архитектуру - исчезнут и рендеры.
Здравствуйте! Похоже, вас заинтересовала эта беседа, но у вас ещё нет аккаунта.
Надоело каждый раз пролистывать одни и те же посты? Зарегистрировав аккаунт, вы всегда будете возвращаться на ту же страницу, где были раньше, и сможете выбирать, получать ли уведомления о новых ответах (по электронной почте или в виде push-уведомлений). Вы также сможете сохранять закладки и ставить лайки постам, чтобы выразить свою благодарность другим участникам сообщества.
С вашими комментариями этот пост мог бы стать ещё лучше 💗
Зарегистрироваться Войти© 2024 - 2026 ExLends, Inc. Все права защищены.