Главная / Статьи

Тестирование пути сбоя: Next.js, Remix и устаревшие данные авторизации

Пример анализа случая, показывающий, как ошибка с устаревшими разрешениями влияет на выбор между Next.js и Remix, а также как оценивать фреймворки с точки зрения обработки изменений, срока жизни кэша и удобства отладки.

Издатель: REACTAPP.TOP9 мин чтения2058 слов

Источники: Next.js vs Remix in 2026: The Faster Page Won Every Benchmark. One Stale Permission Changed Our Decision.

При сравнении фреймворков обычно учитывается идеальный сценарий: чистый браузер, загруженные данные и страница, которая открывается с первой попытки. Оценка производительности производственных систем проводится в других условиях — сразу после изменения данных. В этом кейс-стади рассматривается команда, которая создала один и тот же аутентифицированный интерфейс как с использованием Next.js, так и в архитектуре стиля Remix; она заметила, что Next.js побеждает во всех тестах на производительность, но затем изменила свое решение из-за одного отозванного разрешения, которое не хотело исчезать. К концу вы ознакомитесь с практическим способом оценки фреймворков отображения с учетом тех сбоев, которые ваш продукт действительно не может себе позволить.

Очевидный победитель в плане производительности

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

Затем из учетной записи клиента был удален администратор. И база данных, и журнал аудита отражали это удаление, а перезагрузка страницы показывала правильные разрешения. Однако когда затронутый пользователь вернулся на экран учетной записи с помощью встроенного навигатора, административные элементы снова появились. Кнопки, которые должны были исчезнуть, снова стали видимыми.

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

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

Экран, созданный дважды

Речь идет не о простом прототипе. Эта страница обслуживала множество пользователей и позволяла администраторам управлять людьми, их ролями, ключами API, определять, кто имеет доступ к информации о счетах, а также отслеживать все действия на счету. Большая часть трафика приходилась на чтение, но критически важными операциями были записи: отзыв роли, смена ключа, удаление пользователя, изменение прав доступа к счетам-фактурам.

Три требования действовали в противоположных направлениях:

  • Быстрая первоначальная отрисовка, даже для счетов с длинной историей действий.
  • Минимальное количество JavaScript-кода в браузере, поскольку пользователи работали на медленных корпоративных компьютерах.
  • Изменения разрешений, которые отображаются стабильно независимо от того, как пользователь попадает на страницу.
  • В Next.js основную часть экрана отрисовывали серверные компоненты, а небольшие клиентские компоненты использовались только для интерактивных элементов. Данные учетной записи, применяемые в нескольких вложенных лейаутах, хранились в кэше, чтобы избежать их многократного загрузки, а успешные записи запускали процедуру повторной верификации.

    Версия в стиле Remix была построена на загрузчиках для каждого маршрута, предназначенных для чтения данных и выполнения действий при записи; стандартные заголовки кэша HTTP добавлялись только там, где был известен безопасный срок хранения данных. После завершения действия загрузчики соответствующих маршрутов запускались заново, следуя последовательности, достаточно простой для записи на доске. (На момент написания этого текста данная модель загрузчиков и действий сохраняется в режиме фреймворка React Router, в который был интегрирован Remix; для уточнения названий обратитесь к актуальной документации.)

    Обе реализации были надежными, и ни одна из них не была создана с целью потерпеть неудачу. В идеальных условиях Next.js явно выигрывал, что выявило первый недостаток теста: реальный трафик не ограничивается идеальными условиями.

    Где серверные компоненты усложняют процесс запроса

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

    Ценой этого стало то, что одна видимая страница теперь формировалась из нескольких независимых этапов обработки:

    • Работа, ограниченная одним запуском серверной обработки.
    • Повторное использование данных благодаря кэшированию на стороне сервера.
    • Состояние маршрутизатора сохранялось в браузере при навигации между страницами.
    • Несколько инстансов сервера, каждая из которых хранила собственную копию закэшированных значений.
    • Аннулирование кэша происходило после записи новых данных.
    • Любые прокси-серверы или CDN, расположенные перед приложением.

    Для пользователя было неважно, в каком слое хранилась устаревшая информация, но для сотрудника, находящегося на дежурстве, это было критично. Один лог показывал, что база данных возвращает обновленные данные о роли; другой — что интерфейс управления разрешениями отображает прежние данные. Ни один из вариантов не был неверным: каждый фиксировал отдельный запрос, проходивший по разному пути в системе.

    Во время локальной разработки проблема так и не воспроизводилась. Один процесс контролировал весь кэш, инженеры обновляли его после каждой изменения, а тестовые учётные записи никогда не имитировали действия реального пользователя, который оставлял приложение открытым до изменения прав. Благодаря устранению повторной работы такая архитектура лишила возможности просто определить, какой слой предоставил ту или иную информацию.

    Точное поведение каждого уровня кэширования Next.js, например, сколько времени клиентский маршрутизатор хранит заранее загруженные сегменты, менялось между крупными версиями. Это и есть урок: если ваша работоспособность зависит от этих значений по умолчанию, запишите эту зависимость и проверяйте её при каждом обновлении.

    Почему именно полная перезагрузка стала ключом к решению

    Специалисты по поддержке обнаружили, что принудительная перезагрузка устраняла проблему. Это, казалось бы, указывает на кэш браузера, однако браузер объяснил лишь часть ситуации. При перезагрузке отправлялся совершенно новый запрос на сервер, и этот запрос обычно попадал в версию без устаревшей копии данных. Вместо этого клиентская навигация использовала тот же путь, но с другими данными и иной историей аннулирования кэша. Поскольку страница выглядела корректно после каждой перезагрузки, ошибка исчезала каждый раз, когда кто-то на неё смотрел.

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

    Благодаря этому пониманию воспроизведение ошибки стало надежным:

    1. Откройте экран учетной записи, чтобы его обрабатывала одна инстанция приложения.
    2. Из другой сессии измените роль пользователя.
    3. Вернитесь на экран с помощью навигации клиента, не перезагружая страницу.
    4. Проверьте, соответствует ли версия прав, отображаемая в интерфейсе, той, которую сохраняет сервис.

    В компонентах не было никаких ошибок. Запомненные данные в кэше вели себя ровно так, как было настроено; однако установленный срок их хранения оказался неподходящим для данных авторизации. Называть это «ошибкой устаревшего кэша» означает предполагать наличие ошибки в коде. На самом деле проблемой была отсутствующая архитектурная гарантия: в течение какого времени отозванное право должно исчезнуть из всего, что может видеть пользователь?

    Первое решение практически перенесло проблему на базу данных

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

    Вложенные макеты и панели запрашивали дублирующиеся данные учетных записей. Благодаря кэшированию эти дублирующие запросы казались бесплатными; их отключение превратило инцидент с разрешениями в увеличение времени ожидания подключений к PostgreSQL, что поставило под угрозу другие страницы с данными учетных записей.

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

    Восстановление происходило поэтапно, плавно и безопасно:

    • Данные разрешений аннулировались отдельно для каждого аккаунта и версии разрешения, а не в глобальном масштабе.
    • Запросы на авторизацию выполнялись в рамках конкретного запроса, поэтому они никогда не сохранялись дольше времени выполнения одного запроса.
    • Дублирующиеся запросы удалялись до того, как объем обрабатываемых данных без использования кэша увеличивался.
    • Часть трафика сначала проходила через новую логику обработки, при этом отслеживалось время ожидания в пуле ресурсов.
    • Каждый ответ теперь содержал небольшой маркер, указывающий способ его генерации, инстанцу, которая его обработала, версию данных и факт использования кэша для ответа.

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

    Почему медленнее архитектура оказалась лучше для этого продукта

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

    Его возможности были более ограниченными. Комбинация загрузчика, операции и повторной верификации позволила команде сформировать компактную когнитивную модель того, что происходит после записи данных. После того как операция изменяла роль, инженеры могли определить маршруты, данные которых требовали обновления, и знать, где это сделать. Границы запросов совпадали с теми, что уже использовались операторами трейсинга. Он загружал некоторые данные дважды и требовал дополнительного кода браузера для определенных виджетов, но причины его несрабатываний удавалось быстро выявить.

    Это подходит именно для данного продукта, но это не делает его универсальным. На сайте, где преобладает контент с редко меняющимися данными и где важна оптимизация работы браузера, архитектура Next.js остается лучшим выбором; её возможности по отрисовке и кэшированию позволяют обеспечить значительно лучший пользовательский опыт. Для инструмента управления с аутентификацией потеря разрешения, которое остаётся видимым, стоит дороже, чем незначительная разница в производительности. Важнее были возможность отслеживания изменений и предсказуемость актуальности данных.

    Тестирование не было неверным. Оно просто ответило на неполный вопрос.

    Перестройка оценки с учётом сбоев

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

    • Изменяется разрешение, пока экран остается открытым. Показываются ли все способы доступа к странице изменения в установленные сроки, и может ли инженер увидеть, какая версия отображается на экране?
    • Пример эксплуатации приложения исчезает на полпути. Может ли пользователь безопасно попробовать снова, и не приводит ли частичная обработка к запутанному состоянию клиента?
    • Кэширование отключается в момент пиковой нагрузки. Сможет ли база данных выдержать самое срочное временное решение?
    • Во время развертывания трафик обслуживают две версии приложения. Могут ли обе они интерпретировать данные из кэша и ответы на изменения?
    • Служба поддержки воспроизводит сценарий работы одного клиента. Могут ли они определить, как отображалась страница, какой кэш и пример эксплуатации ответили, а также какая версия данных была использована, всё это без отладки рабочих серверов?

    Next.js оставался быстрее на нескольких важных маршрутах. Модель в стиле Remix позволяла команде легче разбираться с ситуацией, когда эти маршруты переставали работать нормально. В такой момент команда сравнивала системы, а не скриншоты.

    Принятие решения на основе возможных ошибок страницы

    Сужение выбора до «Next.js быстр, Remix прост» не выдержит проверки в реальных условиях. Перед измерением производительности ответьте на следующие вопросы:

    • Какие данные продукт может отдавать устаревшими и на какой срок?
    • Влияют ли мутации на деньги, права доступа, запасы или только на визуальное представление?
    • Сколько границ кэширования и отрисовки может определить команда на основе одного отчета клиента?
    • Сможет ли база данных функционировать при удалении кэша?
    • Как ведут себя две развернутые версии параллельно?

    Затем измерьте производительность на «идеальном» пути.

    Команда пришла к противоречивому решению. Next.js остался в тех случаях, когда серверные компоненты сокращали объем работы на стороне клиента, а каждый элемент данных имел четко определенный и безопасный срок жизни. Часто меняющиеся данные разрешений намеренно оставлялись простыми: они использовались только в рамках конкретного запроса и никогда не кэшировались. Для экранов операций, где правильность обработки изменений имела большее значение, чем быстрая статическая доставка контента, был выбран подход Remix. Если вы продолжите использовать Next.js для таких экранов, механизм аннулирования на основе тегов, описанный в директиве use cache и процессе повторной верификации с использованием тегов, позволит вам получить более точный контроль, а проверки авторизации внутри каждой серверной операции останутся основным механизмом защиты независимо от того, что отображается на интерфейсе.

    Основные выводы

    • Бенчмарк, запускаемый в новой сессии, не может обнаружить ошибки, существующие между состояниями; тестируйте переходы внутри долгоживущих сессий.
    • Рассматривайте данные авторизации как отдельный класс: они должны быть связаны с конкретным запросом, иметь версию и быть явно аннулируемыми.
    • Кэш может создавать реальную нагрузку; спланируйте способы его удаления до того, как инцидент вынудит вас это сделать.
    • Сделайте источник каждого ответа видимым с помощью простых маркеров, указывающих режим отображения, экземпляр, версию и статус кэша.
    • Выбирайте фреймворк, способы сбоев которого ваша команда может быстро объяснить. Один вариант может предоставить больше инструментов для ускорения работы, а другой — облегчить диагностику некорректных страниц. Для некоторых продуктов именно второй критерий является ключевым.