Головна / Статті / Відкладення побічних ефектів у Next.js за допомогою API after()

Відкладення побічних ефектів у Next.js за допомогою API after()

Дізнайтеся, як API after() у Next.js виконує аналітику, логування та фонові завдання після надсилання відповіді, а також про його гарантії, перешкоди та компроміси у обробці помилок.

2818 слів

Більшість проблем з продуктивністю у додатках Next.js виникає через те, що кожну операцію розглядають як однаково термінову. Команди регулярно змушують користувачів чекати під час запису даних у аналітику, створення журналів подій, очищення кешу та надсилання сповіщень, перш ніж надати відповідь. Відвідувач змушений чекати 300 мілісекунд на вставку даних у базу, результат якої ніхто насправді не потребує бачити. Сама відповідь надходить з запізненням, тому що кілька не пов’язаних між собою додаткових операцій були примусово виконані одна за одною.

Типова схема пов’язує виконання критичних операцій із рутинними завданнями. Дії сервера залишаються нерухомими, чекаючи на відповідь від сервісу обліку подій. Обробники маршрутів зупиняються, щоб сервіс метрик міг отримати подію. Кожне з цих фонових завдань поступово збільшує відчутний час завантаження, і сукупний ефект — це повільний додаток, який відштовхує користувачів.

API after() у Next.js відокремлює побічні ефекти від життєвого циклу відповіді. Необов’язкові операції поміщаються всередину after(), і фреймворк планує їх виконання після того, як відповідь вже надана. Клієнт отримує свій вміст негайно, тоді як логування, аналітика та інші фонові завдання виконуються пізніше, повністю виходячи за межі критичного шляху.

У решті цього матеріалу розглядаються механізми after(), сценарії, у яких він є корисним, та операційні компроміси, які потрібно врахувати перед тим, як використовувати його у продакшені.

Ключові висновки

  • after() відкладає побічні ефекти до завершення відповіді, тим самим усуваючи необов’язкові операції з шляху запиту, який бачить користувач.
  • Типові застосування включають аналітичні події, історію аудиту, скасування кешу та надсилання сповіщень; жоден з цих процесів не має завершуватися до того, як користувач побачить результат.
  • На відміну від waitUntil(), який прив’язаний до Edge Runtime, after() діє послідовно незалежно від того, чи ви працюєте з Node.js чи Edge, а також незалежно від постачальника хостингу.
  • Оскільки збої всередині after() виникають після того, як клієнт вже отримав відповідь, потрібна явна обробка за допомогою try-catch для їх виявлення та запису.
  • Рівень гарантії виконання залежить від вашої платформи хостингу: середовища без серверів із жорсткими таймаутами можуть завершити виконання функції до того, як завершиться калебек after().
  • Що таке API after() та як він працює

    after() приймає функцію-колбек та виконує її після того, як потік відповіді закривається. Послідовність дій така: движок чергує функцію-колбек, HTTP-відповідь надсилається клієнту, і лише тоді виконується відкладена робота. Браузер ніколи не змушений чекати на завершення цієї функції-колбека.

    У межах однієї заявки Next.js зберігає порядок, у якому були зареєстровані виклики after(). Якщо ваш обробник викликає after() тричі поспіль, ці три функції-колбеки виконуються одна за одною, у тому самому порядку, після того, як відповідь буде отримана. Ця гарантія порядку є важливою, коли одне відкладене завдання залежить від іншого — наприклад, для запису дії у журнал перед скасуванням елемента кешу, який відображає цю дію.

    Це принципово інший підхід, ніж просто створення обіцянки та забування про неї. Обіцянки типу „створити та забути“ схильні мовчки ігнорувати некеровані відмови, що може призвести до неповних журналів подій чи несинхронності стану додатку. Функція after() дає можливість явно керувати плануванням цих операцій, що значно полегшує виявлення та належне оброблення помилок у продакшені.

    Різниця між блокуючою та неблокуючою обробкою стає очевидною, як тільки ви її виміряєте. Обробник, який синхронно записує логи перед відповіддю, зазвичай додає від 50 до 150 мілісекунд на кожен запит. Якщо перемістити цей же виклик для запису логів у функцію after(), обробник зможе повернутися менш ніж за 10 мілісекунд — це стільки часу потрібно лише на запис даних у базу. З точки зору користувача відповідь здається миттєвою, тоді як усі операції з обліку відбуваються непомітно на фоні.

    Практичні сценарії використання: аналітика логів та фонові завдання

    Аналітика є класичним прикладом. Коли клієнт завершує процес оплати, ваш додаток фіксує покупку та відображає екран підтвердження. Платформі аналітики не потрібно знати про цю покупку в момент її виконання — їй достатньо дізнатися про неї згодом. Виконання операції відстеження у функції after() може скоротити час процесу оплати на 100–200 мілісекунд без втрати будь-яких даних.

    Журналування для аудиту працює так само. Правила відповідності часто вимагають, щоб кожна зміна стану була зафіксована, але немає причин, чому користувач мусить чекати, поки система аудиту підтвердить цю зміну. Критичний шлях виконує саму зміну в базі даних; функція-колбек after() відправляє запис аудиту до окремої системи, якою часто є сховище журналів, оптимізоване для запису, або черга повідомлень.

    Анулювання кешу є ще одним хорошим варіантом, особливо коли це стосується кількох віддалених сервісів. Оновлення контенту може передбачати очищення кешів CDN, видалення певних ключів Redis та перевірку підключених клієнтів WebSocket. Жоден з цих кроків не впливає на те, що бачить користувач у відповіді. Оновлення записується до бази даних, повертається повідомлення про успіх, а функція after() забезпечує подальше анулювання кешу.

    Надсилання електронних листів чи сповіщень також добре підходить для виконання у функції after(), за умови, що додатку не потрібно синхронно відображати інформацію про невдачу доставки. Візьмемо, наприклад, скасування пароля: додаток записує токен для скасування, повертає повідомлення про успіх та відправляє електронний лист у фоновому режимі. Якщо постачальник електронної пошти відмовляє, користувач може просто спробувати знову через інтерфейс, замість того щоб бачити помилку під час первинної заявки.

    Цей підхід уникає тонкого, але дорогого способу збою. Якщо надсилання електронного листа відбувається безпосередньо та провайдер виходить з ладу, користувачеві відображається помилка 500, хоча токен для скидання було створено успішно. Вони намагаються ще раз, створюючи дублікат токена, і тепер ваша система змушена очищувати такі „безхазяйні“ токени або ризикувати проблемами з безпекою. Обробка надсилання електронного листа всередині after() повністю ізолює цей збій — відповідь все одно буде успішною, а окремий шар моніторингу може незалежно виявляти проблеми з доставкою.

    Поновлення фонових даних та підготовка кешу є схожими процесами. Припустімо, популярний товар розпродався: оновлення запасів має спричинити поновлення даних відповідних категорій та кешу головної сторінки. Саме оновлення запасів відбувається миттєво, тоді як функція after() аналізує граф залежностей та позначає застарілі записи для перегенерації. Відвідувачі, які переглядають сайт під час цього процесу поновлення, можуть бачити трохи застарілі дані, але додаток все одно працює швидко.

    Впровадження after() у Server Actions та Route Handlers

    Server actions можуть безпосередньо викликати after() як частину операції змін. Вони виконують свою основну функцію, планують необхідні додаткові дії та повертають керування клієнту. Next.js керує життєвим циклом у фоновому режимі, переконуючись, що функція-колбек виконається перед тим, як безсерверна функція зможе завершити свою роботу.

    Обробники маршрутів мають однакову структуру: вони виконують основну роботу, надсилають свою відповідь та переносять будь-які додаткові операції до функції after(). Це працює як у обробниках маршрутів App Router, так і в API-маршрутах Pages Router, налаштованих для використання середовища виконання App Router.

    Важливою деталлю є те, що after() має доступ до повного контексту, який існував під час його реєстрації. Усе, що було зафіксовано в замиканні — заголовки запиту, оброблені дані тіла, стан автентифікації — залишається доступним усередині функції-відповіді без додаткової налаштування.

    Мідлвейр також може використовувати after() для запису метаданих запиту, не уповільнюючи при цьому подальшу обробку запиту. Мідлвейр бере необхідні заголовки, планує виконання операції з запису та передає запит далі. Запис у журналі виконується асинхронно, поки запит продовжує свій шлях до пункту призначення.

    Наскільки надійним є це виконання, значною мірою залежить від того, де ви його розміщуєте. Vercel та подібні платформи подовжують термін існування функції саме для того, щоб у калебеки after() було час завершитися. Якщо ви самостійно розміщуєте код у безсерверних контейнерах, вам потрібно ретельно налаштовувати таймаути — якщо контейнер завершує роботу до того, як калебек буде завершений, ця робота просто втрачається. Це справжня проблема для всього, що не може допустити переривання, такого як події бухгалтерського обліку чи журналювання, пов’язане з дотриманням правил.

    after() проти waitUntil() проти традиційних підходів

    waitUntil(), який походить з Edge Runtime, вирішує схожу проблему, але на нижчому рівні: він утримує функцію активною до тих пір, доки не буде виконано відповідне обіцяння, запобігаючи передчасному завершенню роботи середовища. after() створює абстракцію на основі цього механізму та поводиться однаково як у Node.js, так і в Edge.

    Проекти, які вже використовують waitUntil() у контексті Edge Runtime, можуть поступово переходити на after(). Ці два інструменти не є ідентичними за формою: waitUntil() приймає обіцяння безпосередньо, тоді як after() обгортає функцію-відповідь. Обидва досягають однієї мети — запобігання передчасному завершенню, але after() зручніший у використанні, коли потрібно поєднати кілька фонових завдань, оскільки він звільняє від необхідності ручного керування кількома обіцяннями.

    Техніки типу «запусти та забудь», які ґрунтуються на обіцянках без очікування виконання чи викликах setTimeout, не надають жодних реальних гарантій. Середовище виконання може закритися ще до завершення виконання обіцянки, тихо видаливши всю роботу, яка була у процесі виконання. Бібліотеки для логування, створені на основі process.nextTick() чи setImmediate(), стикаються з тією самою проблемою після розгортання в безсерверних середовищах. Натомість after() чітко визначає намір та дає платформі реальну можливість його дотримати.

    Очерги повідомлень досі залишаються найнадійнішим варіантом для обробки у фоновому режимі, але вони супроводжуються значними операційними витратами. Для їх функціонування потрібна інфраструктура, спеціалізовані працівники, механізми повторної спроби та панелі моніторингу. Для простих додаткових дій, таких як запис у журнали чи скасування запису в кеші, така складна система є зайвою. after() знаходиться між цими двома крайнощами: він надійніший, ніж простий виклик без подальшого контролю, але водночас значно простіший, ніж створення системи на основі очерг.

    Цей компроміс найчіткіше проявляється у способі обробки помилок. Очередь повідомлень автоматично перепробовує виконати невдалі завдання та може направити постійні помилки до очереді для непрочитаних листів з метою подальшого аналізу. Натомість функція-колбек after() виконується лише один раз на запит. Якщо вона зазнає невдачі, ця робота втрачається, якщо тільки ви не створили власний механізм повторної спроби навколо неї. Це прийнятний ризик для операцій з низьким ступенем критичності, але для будь-яких місійно важливих завдань, таких як обробка платежу чи оновлення кількостей товарів на складі, належна очередь залишається правильним інструментом.

    Аспекти використання у продакшені: обробка помилок та гарантії виконання

    Керування помилками всередині after() повністю залежить від вас: обгорніть логіку у чіткі блоки try-catch. Винесена всередині калебеку виняток не дійде до клієнта, оскільки відповідь вже була надіслана до того, як запуститься калебек. Платформа зафіксує цю невдачу, але відновлення роботи — повторні спроби, попередження, логіка резервного варіанту — є обов’язком додатку.

    Наскільки надійно завершується калебек, сильно залежить від того, де розміщений додаток. У Vercel виконання функції продовжується, щоб надати калебекам типу after() можливість виконатися протягом встановленого таймауту. Робота з Next.js на AWS Lambda вимагає ретельної налаштування таймауту, щоб функція не була перезапущена раніше, ніж калебек завершиться. GCP Cloud Run та Azure Container Instances створюють подібні обмеження. У всіх цих випадках закономірність повторних помилок однакова: функція досягає таймауту раніше, ніж завершиться відкладена робота, і ця робота безповоротно втрачається.

    Здатність до спостереження стає надзвичайно важливою, як тільки ця схема потрапляє у продакшн. Звичайні журнали додатку охоплюють основний цикл життя запиту, але функції-виклики after() виконуються після того, як цей цикл технічно завершився, поза звичайним контекстом журналізації. Інструменти дистрибуйованого відстеження, такі як OpenTelemetry, потрібно налаштувати так, щоб вони явно фіксували цей виклик як окремий період часу. Якщо пропустити цей крок, будь-які помилки, що виникають усередині after(), стають практично невидимими для тих, хто відстежує систему.

    Тестування навантаження розкриває ще один аспект поведінки цього паттерну. Уявімо обробник маршруту, який виконує завдання тривалістю 500 мс у функції after(). При ізольованих тестах такий маршрут здається швидким. Однак при 100 одночасних запитах платформі доводиться виконувати 100 функцій-відповідей майже одночасно, і вона може не встигати. Основний маршрут запитів залишається швидким, проте відкладені завдання починають накопичуватися. Інфраструктуру потрібно проектувати не лише з урахуванням обсягу вхідних запитів, а й додаткового навантаження, яке створює фонова робота.

    Ця схема також вступає у конфлікт із API, які запроваджують обмеження на кількість запитів. Уявіть собі, що за хвилину до вашого додатку надходить 1 000 запитів, кожен з яких планує виклик сервісу аналітики через after(). Коли хвиля відповідей припиняється, постачальник аналітики раптово отримує близько 1 000 викликів поспіль, і може почати обмежувати їх кількість або відхиляти. Тут допомагає групування логіки повернення даних: замість того, щоб надсилати запит за кожною подією, необхідно накопичувати події в пам’яті та передавати їх разом у більших, рідкісніших пакетах.

    Однак блокування завдань створює власні проблеми з налаштуваннями. Буфер, який зберігає накопичені події, залишається в пам’яті до моменту виконання операції очищення, постійно споживаючи ресурси. Раптовий сплеск трафіку може вичерпати цю пам’ять ще до того, як відбудеться заплановане очищення. Іншим варіантом є використання функції-відповіді after(), яка додає події до постійної черги замість того, щоб зберігати їх у пам’яті, але тоді ви знову створюєте багато з тієї складності, якої after() мала допомогти вам уникнути.

    У кінцевому підсумку те, чи є after() правильним варіантом виклику, залежить від того, наскільки серйозними будуть наслідки втрати відкладеної роботи. Такі елементи, як події аналітики, записи журналу аудиту та скасування кешу, можуть дозволити собі періодичні збої в виконанні без порушення чогось критичного. Однак підтвердження оплат, зміни в запасах та події, пов’язані з безпекою, цього не дозволяють. Для таких завдань черга повідомлень або просте синхронне оброблення залишаються безпечнішим варіантом, навіть якщо це призводить до певних затримок у відповідях.

    Часто ставлені запитання

    Чи можуть функції-відповідачі after() отримувати доступ до даних, притаманних запиту, таких як заголовки чи кукі?

    Так. Колбек закривається у межах того самого контексту, який існував у момент виклику after(), тож будь-які змінні, значення заголовків чи дані куки, доступні на той момент, залишаються доступними всередині колбека. На практиці це означає, що ви можете отримувати доступ до таких елементів, як автентифікований користувач, оброблений тіло запиту чи вилучені метадані, без необхідності передавати їх через окремий канал.

    Що відбувається, якщо колбек after() генерує непропрацьовану помилку?

    Система виконання фіксує її, але помилка ніколи не доходить до клієнта, оскільки відповідь вже була надіслана до моменту виконання колбека. Застосунок повинен обгортати колбеки after() у блоки try-catch та передавати проблеми до інструменту моніторингу чи механізму повторних спроб. Якщо цього не зробити, проблеми просто зникають без сліду.

    Чи працює after() у середовищах Node.js та Edge Runtime?

    Так, API усуває різницю між двома середовищами виконання. У середовищі Edge Runtime він ґрунтується на тих самих семантиках, що й waitUntil(). У середовищі Node.js він використовує будь-який механізм, специфічний для платформи, який є доступним для подовження терміну життя функції. Інтерфейс залишається однаковим у обох випадках, але ступінь гарантованості виконання все одно залежить від налаштувань постачальника хостингу.

    Як after() порівнюється з використанням черги повідомлень для фонових завдань?

    Черга повідомлень забезпечує автоматичні спроби повторного виконання, обробку некоректних повідомлень та надійне виконання, але це супроводжується додатковими операційними витратами. after() — це значно простіший варіант, який ідеально підходить для додаткових ефектів, таких як логування чи анулювання кешу, де періодична втрата даних не є серйозною проблемою. Коли робота абсолютно не може бути втрачена, черга все одно є кращим вибором.

    Чи можуть кілька функцій-колбеків after() виконуватися одночасно, чи вони виконуються послідовно?

    У межах однієї запиту колбеки виконуються один за одним у тому порядку, в якому були зареєстровані. Якщо викликати after() три рази, система повністю завершить виконання першого колбека, перш ніж почати другий, а потім третій. Цей порядок є важливим, коли одна відкладена операція залежить від іншої, наприклад, для запису логу дії перед скасуванням відповідної запису в кеші.

    Висновок: Коли використовувати after() у вашому проекті Next.js

    after() дозволяє відокремити необов’язкові операції від шляху запиту, на який чекає користувач, не змушуючи створювати окрему інфраструктуру черг. Такі завдання, як відстеження аналітики, журналювання операцій, скасування кешу та надсилання сповіщень, ідеально підходять для цього, адже користувач отримує відповідь негайно, тоді як додаток тихо виконує решту завдань на фоні.

    Проблема полягає у тому, що виконання цих операцій не гарантоване. Платформи типу serverless швидко припиняють роботу функцій, і якщо середовище перериває виконання через callback, ці операції втрачаються. Тому таймаути потрібно налаштовувати з урахуванням потреб callback, а кожен after() callback має мати власну систему обробки помилок. Якщо час від часу втрата цих операцій є прийнятною, простота after() робить його вигідним варіантом. У інших випадках черга повідомлень залишається більш надійним вибором.

    Розуміння цих закономірностей має бути достатнім, щоб почати ефективно використовувати after() у додатку Next.js. Якщо застосовувати його розсудливо, це може суттєво вплинути на швидкість роботи додатку для користувачів, і саме ця різниця має найбільше значення на трафікованих сторінках, де навіть невеликі затримки накопичуються та можуть спонукати користувачів повністю покинути сесію.

    Пов’язана література

  • 20 розширених шаблонів Next.js для додатків App Router виробничого рівня — Дізнайтеся про двадцять шаблонів високого рівня для Next.js, які охоплюють підхід «сервер на першому місці», стрімінг, кешування, маршрутизацію та оптимізацію продуктивності для створення швидших та масштабованих додатків.
  • Відлагодження Server Actions у Next.js: помилки розгортання, CORS та обмеження навантаження — Практичний посібник з усунення проблем, який пояснює, чому Server Actions та API-шляхи у Next.js працюють без звуків або викидають неzрозумілі помилки, з конкретними рішеннями для кожного випадку.