Тестування шляху до невдачі: Next.js, Remix та застарілі дані автентифікації
Дослідження конкретного випадку, яке показує, як помилка старих дозволів впливає на вибір між Next.js та Remix, а також як оцінювати фреймворки з точки зору мутацій, терміну життя кешу та можливостей дебаггінгу.
Порівняння фреймворків зазвичай оцінюють ідеальний сценарій: чистий браузер, вже завантажені дані та сторінка, яка завантажується один раз. Системи у продакшені оцінюються в інший момент – після зміни даних. У цьому кейс-стаді вивчається команда, яка створила однаковий автентифікований інтерфейс у Next.js та за архітектурою у стилі Remix, спостерігала, як Next.js перемагає у всіх тестах продуктивності, а потім змінила своє рішення через одну скасовану дозволу, яка відмовлялась зникати. До кінця ви отримаєте практичний спосіб оцінки фреймворків для відображення контенту з урахуванням тих проблем, які ваш продукт дійсно не може собі дозволити.
Очевидний переможець у продуктивності
Різниця у продуктивності була значною. Версія Next.js надсилала до браузера менше JavaScript-коду, швидше створювала перший корисний вигляд інтерфейсу, залишала обробку ресурсомістких даних на сервері, а завдяки кешуванню забезпечувала миттєве переходження між сторінками.
Потім адміністратора було видалено з облікового запису клієнта. І база даних, і журнал аудиту відображали це видалення, а після перезавантаження сторінки з’явилися правильні дозволи. Проте коли постраждалий користувач повернувся на екран облікового запису за допомогою вбудованої навігації, адміністративні інструменти знову з’явилися. Кнопки, які мали зникнути, знову були видимі.
Бекенд-API відхиляв кожну дію з привілеями, тому жодні дані не змінилися та не сталося жодного інциденту з безпекою. Саме ця перевірка з боку сервера є причиною, чому авторизація завжди має забезпечуватися на рівні API, а не виводитися з того, що показує інтерфейс. Але цей захист не врятував архітектурне рішення.
У версії, створеній у стилі Remix, інженери за кілька хвилин знайшли всі місця, де могли залишатися застарілі дані. У версії Next.js знадобилося кілька людей, два суперечливі записи та одне випадкове повне завантаження, щоб з’ясувати, який шар створив відповідь. Це розслідування змінило попередній висновок.
Екран, який було створено двічі
Ця сторінка не була просто іграшкою. Вона обслуговувала багатьох користувачів та дозволяла адміністраторам керувати людьми, їхніми ролями, API-ключами, визначати, хто може отримувати інформацію про рахункування, а також отримувати оновлення про те, що відбулося на рахунку. Читання даних складало основну частину трафіку, проте критичними операціями були записи: скасування ролі, зміна ключа, видалення користувача, зміна прав доступу до рахунків-фактур.
Три вимоги спрямовували розвиток у різних напрямках:
- Швидке початкове відображення, навіть для рахунків із довгою історією діяльності.
У Next.js більша частина екрана відображалася за допомогою серверних компонентів, а невеликі клієнтські компоненти використовувалися лише для інтерактивних елементів. Дані облікового запису, які використовувалися кількома вкладеними макетами, проходили через кеш, щоб уникнути їхнього багаторазового завантаження, а успішні записи спричиняли їх перевірку.
Версія у стилі Remix була побудована на завантажувачах для кожного маршруту, призначених для читання даних та виконання дій для запису, причому стандартні заголовки кешу HTTP додавалися лише там, де була відома безпечна тривалість їх існування. Після завершення дії завантажувачі для відповідних маршрутів запускалися знову, дотримуючись послідовності, настільки простої, що її можна було намалювати на дошці. (На момент написання ця модель завантажувачів та дій продовжує використовуватися у режимі фреймворку React Router, до якого було інтегровано Remix; перевірте поточну документацію щодо найменувань.)
Обидві реалізації були надійними, і жодна з них не була створена для того, щоб програти. За ідеальних умов Next.js явно виграв, що виявило першу помилку у тесті: реальний трафік не обмежується ідеальними умовами.
Де серверські компоненти ускладнюють запит
Компоненти сервера виконали те, що обіцяли. Розділи з режимом лише для читання не вимагали клієнтського коду, отримання даних відбувалося безпосередньо біля компонентів, які їх використовували, повільні елементи інтерфейсу завантажувалися без блокування основного інтерфейсу, а стабільні дані облікового запису уникали необхідності повторних запитів до сервера.
Ціною цього стало те, що одну видиму сторінку тепер складали з кількох незалежних етапів обробки:
- Робота, обмежена одним відтворенням на сервері.
- Повторне використання даних завдяки кешуванню на серверній стороні.
- Стан маршрутизатора зберігався в браузері під час переміщень користувача.
- Кілька інстанцій сервера, кожна з яких мала власну копію закешованих значень.
- Анулювання кешу відбувалося після запису даних.
- Будь-які проксі чи CDN, які знаходилися перед додатком.
Те, у якому шарі зберігалася застаріла значення, не мало значення для користувача, але було критично важливим для того, хто перебував на чергуванні. Одна зі слідів показувала, що база даних повертала оновлену роль; інша — що інтерфейс дозволів відображав попередню. Жоден з варіантів не був неправильним: кожен фіксував різний запит, який проходив різним шляхом у системі.
Локальна розробка ніколи не відтворювала цю проблему. Один процес керував усіма кешами, інженери оновлювали їх після кожної зміни, а тестові облікові записи ніколи не дотримувалися послідовності дій справжнього клієнта, який тримав додаток відкритим до зміни дозволів. Усуваючи повторну роботу, проект також позбавив себе будь-якої простої можливості визначити, який шар надав певну значення.
Точна поведінка кожного рівня кешування Next.js, наприклад, термін зберігання попередньо завантажених фрагментів клієнтським маршрутизатором, змінювалася між основними версіями. Це є частиною уроку: якщо ваша коректність залежить від цих значень за замовчуванням, запишіть цю залежність та перевіряйте її після кожного оновлення.
Чому саме повне оновлення стало справжньою ключовою інформацією
Команда підтримки виявила, що примусове оновлення вирішує проблему. Це, здається, стосується кешу браузера, проте браузер пояснив лише частину ситуації. Оновлення надсилало абсолютно новий запит на сервер, і цей запит зазвичай потрапляв до версії без застарілої копії. Клієнтське навігування, натомість, використовувало той самий шлях з іншими даними та іншою історією анулювання. Оскільки сторінка виглядала правильною після кожного оновлення, баг постійно зникав щоразу, коли хтось на неї дивився.
Синтетичний моніторинг знову виявив ту саму проблемну зону. Кожна перевірка починалася з нової сесії та підтверджувала, що користувач із новою роллю бачить правильну сторінку. Ніхто не переходив від старих прав до нових у межах існуючої сесії. Команда відстежувала стани, тоді як баг існував між цими станами.
Завдяки цьому розумінню відтворення проблеми стало надійним:
- Відкрийте екран облікового запису, щоб його обробляла одна інстанція додатку.
- Змініть роль користувача з іншої сесії.
- Поверніться на екран через навігацію клієнта, не перезавантажуючи його.
- Перевірте, чи версія дозволу, яка відображається у інтерфейсі, збігається з тією, яку зберігає сервіс.
У компонентах не було жодних помилок. Дані з кешу поводилися саме так, як налаштовано; проте встановлений термін їх зберігання був просто недоречним для даних автентифікації. Називати це „помилкою застарілого кешу“ означає припускати наявність пошкодженої рядка коду. Насправді проблемою була невизначена архітектурна гарантія: протягом якого часу скасоване право має зникнути з усього, що може бачити користувач?
Перше рішення майже перенесло проблему на базу даних
Найшвидшим способом усунення проблеми було вимкнення кешування у всій частині акаунту. Застарілі дозволи зникли, і навантаження на базу даних миттєво зросло.
Вкладені макети та панелі запитували дублюючі дані про акаунт. Кешування робило ці дубльовані запити непомітними; його видалення перетворило проблему з дозволами на збільшення часу очікування у пулі з’єднань PostgreSQL, що поставило під загрозу інші сторінки акаунту.
У дискусіях щодо фреймворків зазвичай ігнорують саме цю точку. Кеш може становити ризик для коректності, а водночас бути джерелом навантаження. Його вимкнення повертає свіжість даних, водночас усуваючи припущення, яке підтримувало здоров’я бази даних.
Відновлення відбувалося поетапно, у менших масштабах та безпечніше:
- Дані дозволів були анульовані окремо для кожного облікового запису та версії дозволу, а не глобально.
- Операції авторизації виконувалися лише для конкретного запиту, тож вони ніколи не тривали довше одного запиту.
- Дубльовані операції читання усувалися перед тим, як розширювати діапазон некешованих шляхів.
- Частина трафіку спочатку проходила через нову логіку обробки, поки відстежувався час очікування у пулі.
- Кожна відповідь починалася з невеликого позначника, який вказував на спосіб її генерації, інстанцію, яка її надала, версію даних та чи була відповідь з кешу.
Цей маркер змінив підхід до подальших досліджень. Він дозволяв визначити джерело відповіді, не розкриваючи самі дані про дозволи, а інженерам більше не потрібно було здогадуватися щодо шляху виконання за наявності оновлення. Це недороге доповнення для будь-якої системи з кількома рівнями кешування.
Чому повільніша архітектура виявилася кращою для цього продукту
Система типу Remix не була стійкою до розподіленого стану. Кеші існують у браузерах та CDN, бази даних працюють з репліками, і кожен серйозний додаток зрештою стикається з застарілими даними, частковими помилками та необхідністю повторних спроб.
Його можливості були обмеженішими. Триелементна система завантаження, виконання дій та перевірки надавала команді компактну модель того, що відбувається після збереження даних. Після того, як певна дія змінювала роль об’єкта, інженери могли визначити шляхи, даних яких потрібно було оновити, та знати, де це зробити. Межі запитів збігалися з операторами слідування, які вже використовувалися. Він завантажував деякі дані двічі та потребував додаткового коду браузера для певних елементів інтерфейсу, але причини його некоректної роботи можна було швидко виявити.
Це підходить саме для цього продукту, але не робить його універсальним. На сайті, де переважає контент, дані якого рідко змінюються, та обробка даних браузером є важливою, архітектура Next.js залишається кращим варіантом; її можливості відображення та кешування можуть забезпечити значно кращий досвід користувача. Для інструменту керування, який вимагає автентифікації, наявність скасованого дозволу, який залишається видимим, коштує дорожче, ніж фактична різниця у продуктивності. Важливішими були можливість відстеження змін та передбачуваність актуальності даних.
Бенчмарк не був неправий. Він просто відповів на неповне запитання.
Переосмислення оцінки з урахуванням невдач
Друга оцінка не починалася з чистого браузера та готових даних. Вона починалася з ситуацій, які перший бенчмарк проігнорував:
- Дозволи змінюються під час відкритого екрана. Чи відображаються всі способи доступу до сторінки зміни протягом встановленого часу, і чи може інженер бачити, яка версія знаходиться на екрані?
- Екземпляр додатку зникає під час роботи. Чи може користувач безпечно спробувати ще раз, і чи залишає частково виконана робота заплутаний стан клієнта?
- Кешування вимкнене під час піку навантаження. Чи зможе база даних витримати найшвидше можливе термінове виправлення?
- Під час розгортання трафік обробляють дві версії додатку. Чи зможуть обидві інтерпретувати дані з кешу та відповіді на зміни?
- Підтримка відтворює сценарій роботи одного клієнта. Чи можуть вони з’ясувати, як була отримана сторінка, який кеш та екземпляр надали відповідь, та яка версія даних була використана, усе це без налагодження роботи живих серверів?
Next.js залишався швидкішим на кількох важливих маршрутах. Модель у стилі Remix дозволяла команді легше розуміти ситуацію, коли ці маршрути починали поводитися ненормально. На той момент команда порівнювала системи, а не скріншоти.
Вирішення на основі того, що може піти не так з сторінкою
Звуження вибору до „Next.js швидкий, Remix простий“ не витримає реальних умов експлуатації. Перш ніж вимірювати продуктивність, відповідайте на ці запитання:
- Які дані продукт може надавати застарілими та на який час?
- Чи впливають мутації на гроші, авторитет, запаси чи лише на візуальний вигляд?
- Скільки меж кешування та відображення може визначити команда на основі одного звіту клієнта?
- Чи зможе база даних функціонувати, якщо кеш буде видалено?
- Як поводяться дві розгорнуті версії одночасно?
Потім вимірюйте оптимальний сценарій роботи.
Команда дійшла розділеного рішення. Next.js залишився у тих випадках, коли серверні компоненти скорочували обсяг роботи на клієнті, а кожен елемент даних мав чітко визначений та безпечний термін існування. Дані дозволів, які часто змінювалися, навмисно залишалися простими: вони були обмежені межами запиту та ніколи не кешувалися. А для екрану операцій, де правильне відображення змін мало більше значення, ніж швидка статична доставка, було обрано підхід Remix. Якщо ви продовжуєте використовувати Next.js для таких екранів, механізм анулювання на основі тегів, про який йдеться у інструкції use cache та процесу перевірки на основі тегів, надає більш точний контроль, а перевірки авторизації всередині кожної серверної функції залишаються запасним рішенням, незалежно від того, що відображає інтерфейс.
Основні висновки
- Бенчмарк, який починається з нової сесії, не може виявити баги, що існують між станами; тестуйте переходи всередині довготривалих сесій.
- Розглядайте дані авторизації як окремий клас: з обмеженнями за запитом, версіоновані та явно анульовані.
- Кеш може нести реальне навантаження; сплануйте, як його видалити, перш ніж інцидент змусить це зробити.
- Зробіть джерело кожної відповіді видимим за допомогою легкого маркера, який вказує режим відображення, інстанцію, версію та статус кешу.
- Виберіть фреймворк, способи збою якого ваша команда може швидко пояснити. Один варіант може надати більше інструментів для прискорення; інший — полегшити діагностику неправильних сторінок. Для деяких продуктів саме друга характеристика є справжньою вимогою.