Галоўная / Артыкулы / Задзейнаванне побачных эфектаў у Next.js за дапамой API after()

Задзейнаванне побачных эфектаў у Next.js за дапамой API after()

Дазвольце даклэ научыцца, як API after() у Next.js адрабатоўвае аналітыку, логаванне і задачы на фоне пасля адпаведзі, а таксама якія ў яго є гарантіі, прынцыповыя моменты і компрэсіі ў адрабатцы памылак.

2818 слоў

Большая частка проблем з боку выконванасця ў аплікацыях Next.js вынікае з таго, што кожны элемент работы спрыяецца як аднакова срочная. Команды рэгулярна змушваюць корыстувачаў чакаць на запіс даных у аналітыку, стварэнне лягасоў перагляду, чысткі кэша і адправку паведамленняў, прычым не вяртаючы адпаведны ўтвар. Адвізор мусіць чакаць 300 мілісекунд на вставку даных у базу, рэзультат якой ніхто на самай працы не павінен бачыць. Сам адпаведны ўтвар таксама прыходзіць пазней, таму што калькі некалязаных пасэкеджных дзеянняў былі прымушаны выконвацца адна за іншайю.

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

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

У рэште гэтага матерыялу расказваецца пра механізм after(), сцэнарыях, дзе ён практычны, і прыняццах, якія трэба взважыць пры ўжыцці яго у практычных умовах.

Ключавыя выводы

  • after() адкладае паслядзейныя эфекты да заканчэння адпаведзі, тады як неабяжнае выкананне задач не паўтараецца на шляху запыту, які бачыць корыстнік.
  • Тыповыя прыклады выкарыстання включаюць аналітычныя запісы, лёгкія для перагляду следы дзеяння, анвалідацыю кэша і паведамлення, якія надаюцца звычайным спосабам; жодна з гэтых функцый не павінна завершыцца раней, чым корыстнік побачыць рэзультат.
  • У працоўнай суперасобе Edge waitUntil() працюе інакш, тады калі 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() мелі час завершыцца. Якщо вы самі хостуеце ў серверлесс-контейнерах, вам трэба астаточна налаштаваць таймауты — якщо контейнер зупініцца раней, чым апштраф завершыцца, гэтае заданне проста згубяецца. Гэта справжня проблема для всьога, што не можа тольераваць перыядання, такога як запісвы паводзкаў аблікавання чы логінг, relacionаванный з адпаведнасцю.

    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() дазваляе выкарыстоўваць неабавязковыя задачы паza маршрутом запыту, на які чакае корыстнік, не вымагаючы стварэння спецыяльной інфраструктуры для очаквання. Такія задачы, як аналіз дадзейнаў, логаванне для аудыту, бякства кэша і надсылка паведамленняў, чудова падходзяць, адколі корыстнік отрымае сваю адпаведзь негайна, а прыстанок тым часам тыхо выконвае рештку задач у фоне.

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

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

    Спаднёе чытанне

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