Заканчэнне выкарыстоўвання функцый Parallel load у SvelteKit для адкрыцья водападоў
Дазвольце дазнаць, чаму абыякшыя вызовы awaits спрычынаюць медленную роботу сторанак, і як SvelteKit выкарыстоўвае функцыі load, Promise.all, абераганыя вызовы parent() і стрімаваныя promises, каб усунуць гэты эфект.
Крыніцы: Killing Request Waterfalls: Server-Side Data Fetching in SvelteKit
Старонка можа выглядаць повольна на шырокаспектральныя з’ёднання і быстрым бэкендзе, калі ўсі яе запыты выкананы адні за іншым, а не паралельна. Кожны крок у процесе запыту чакае, пакуль завершыцца попярэдні, таму час, пакуль будзе атрымана першая значымая інфармацыя, становіцца сумой усіх запытоў, а не трываласцю самага повольнага з іх. У гэтым кяліку пояснюецца, звядзе гэтыя «водоспады» выходзяць, і паказваецца, як структураваць функцыі load у SvelteKit так, каб важлівыя даны прыходзілі паралельна, розметка і загрузкі старонак не блакала адна другую, а повольныя, неканэйсарныя потокі даных прыходзілі пасля першай атрыманні інфармацыі.
Як выглядае «водоспад» запытоў
Ачыніце панель «Сеть» у інструментах разработчика прыгледача на стандартнай панелі керування, створанай на базе React чыра Vue, і завантажыце сторонку, якой патрэбны кілька фрагментаў дадзеных. Ступені часта ствараюць атмосферу «лесачака»: спачатку завершаецца першы запит, толькі пасля чаго пачынаецца другі, а трэці чакае на другі. Кожны такі крок адбываецца за межамі цэлага сетевага раунд-трыпу.
Код, які стоіць за гэтым «лесачакам», зазвычай выглядае няшкодным. Компонент запускаецца ў прыгледачы, і толькі пасля чаго выявляе, што ў яго патрэбна. Унутры хука effect чыра mount ён выканае ўсё такое, як const user = await getUser(), пасля чаго const posts = await getPosts(), а потым const comments = await getComments(). Тры лініі, тры вызовы await — нічога явна некоректнага.
Справа ў тым, што значыць await. Ён призупіняе выконанне функцыі пакуль обявленне не будзе вырашана, таму запит posts не можа нават быць адправленым, пакуль не будзе прыбыць весь адпаведны адпаведзень user, а comments чакае своей чергай на posts. Падазрóў, кожны вызов займае або-альбо 300 мс, якщо лічыць DNS, TLS, роботу сервера і перадачу данных. Тады на старонцы немае нічога корыстнага для адображэння прыблізна на весь час трох гэтых процесаў, а не толькі на час самага дзейснага.
Нічога не выклікае памылак, ніякі тэст не збываецца, і кожны компанент делае тое, што яму было сказана. Карыстальнікі проста спаглядаюць індыкатор загрузкі прыблізна у тры разы дэльней, чым трэба. Саме гэтая невиднасць робіць водоспадны падход такім распашчытым.
Чаму водоспадны падходы продовжаюць з’являцца
Большасць іх стварае трое звычайных прычын:
- Кожны компанент запрашоўвае свае сабе прызначаныя даны. У дапрынтовых аплікацыях родны компанент часта должен завершыць загрузку, перш чым падкомпанент зможа запрацаваць, а падкомпанент не можа пачаць свой запит, пакуль не отрымае параметра ад роднага компанента. Дрэва компанента стае ланцугам запытоў.
- Чаканне лінія за лініяй з прыzwычкі. Абарочныя запэўнення
awaitчытаюцца легка, і самэ гэта стварае адчуць, што яны серыязуюць запыты, якія ніколі не залежалі адзін ад другога. - Няма едзінага відображэння патрэбных даноў сторанкі. Калі код запрашэння распадзены межа трыма файламі, важка зразумець, што всі тры запыты моглі бы пачацца ў аднай і той жа момент.
Запіс коду з большайшая шыроць не дапамагае. Дапамагае перанесенне процэсу запрашоўкі дадзейнаў у іншае месца і час: яшчэ до таго, калі існуе які-небудзь компонент, у адным скоординаваным месцы. У SvelteKit такім месцам являецца функцыя load. Чыба прыглядзецца да тых сабе аптальных варыянтаў паралельнай обработкі з незалежнасцю ад фрэймворку, пагляньце на пораўнанне ў блогу Promise.all, Promise.race і sequential awaits.
Як функцыі load у SvelteKit зменяюць стандартны падход
Кожная маршрут SvelteKit можа экспортуваць функцыю load, якая выконваецца перад тым, как адрасаваецца компонент сторанкі. Компоненты больш не прабуюць запрашоўваць дадзейнавы пасля свайго запуску; маршрут спачатку збірае ўсё і перадае яго сторанцы як адну властівасць data.
Існуе два варыянты, і выбор мае значэнне:
- Універсальныя заданні у
+page.jsвыконваюцца на сервере пад час сервернага адраджавання і ў браузеры пад час навігацыі з боку кліента. Ооні падходзяць для вызоўа публічных API і вярнення значэнняў, якія не можна серыялізаваць. - Заданні сервера у
+page.server.jsвыконваюцца толькі на сервере. Його код ніколі не адправляецца ў браузер, таму ён можа безбедна выкарыстоўваць кліенты базы дадзенаў, прыватныя зменныя сераўісу і токены аутантыкацыі.
Лейауты таксама следуюць таму ж прынцыпу з +layout.js і +layout.server.js.
У большасці рэальных прыемоў все, што стосуецца базы дадзенаў, внутранняга сераўісу або секрэтных дадзенаў, павінна знаходзіцца ў +page.server.js. Його мінімальны варыянт выглядае так:
// src/routes/dashboard/+page.server.js
export async function load({ fetch }) {
const res = await fetch('/api/user');
const user = await res.json();
return {
user
};
}
Тры моманты, якія варта запам’ятаваць пры аналізе гэтага фрагмента, заслуговуюць на увагу.
Функцыя заданняў завершаецца раней, чым адраджуецца сторнка
Калі +page.svelte пачынае атрымляць тэкст, user ўжо являе сабой звычайны об’ект. Не існуе хуківа mount, няма індыкатора завантажэння, няма момента, калі значэнне ў стане undefined.
Існавайце fetch, які пасылае SvelteKit
Аргумент fetch не ёсць глобальным. SvelteKit аддае расширэную версію, якая прымае вялікія URL-адрэсы, такія як /api/user, перадае кукі і заголовкі прыходзячай заявкі пад час атрымлення тэкста на серверы, а калі мета — іншы маршрут у той жа прыемлік, яна безпасебна вызывае той хэндлер, а не стварае рэальную HTTP-заявку. Імпорт глобальнага fetch пазбярогае ўсё гэта.
Значэнне, якое вяртаецца, стае атрыбутам дадзеных сторанкі
Што бы ні не вернула функцыя, гэта будзе доступна для компонента сторнкі як data. У компаненте Svelte 5 гэта чытается за дапамою let { data } = $props(); (у старым коде Svelte 4 викорыстоўваецца export let data;), а пасля ў маркапе выводзіцца data.user.name.
Ментальная модель простая: спачатку запрашаецца даны, пасля — выводзіцца на экран, і отримваюцься готовыя значэння заместо запаветаў, якія трэба кераваць унутры компанента.
Аднак перанесенне запрашэнняў у функцыю load сама по сабе не пазбавляе нас ланцоўкі запрашэнняї. Тры паспەльныя вызовы await унутры load знову ствараюць тую ж самую ланцоўку на сервере. Праўыя рашэнні будуць пасля.
Пачатак кожнага незалежнага запрашэння адразу
Рашэннем є негайны запуск усіх незалежных запрашэнняў і спакоючы на ўсе яны разам:
// src/routes/dashboard/+page.server.js
export async function load({ fetch }) {
// Kick off all three requests immediately - none of them
// are awaited yet, so none of them block each other.
const userPromise = fetch('/api/user').then(r => r.json());
const postsPromise = fetch('/api/posts').then(r => r.json());
const commentsPromise = fetch('/api/comments').then(r => r.json());
// NOW wait for all of them to finish, in parallel.
const [user, posts, comments] = await Promise.all([
userPromise,
postsPromise,
commentsPromise
]);
return { user, posts, comments };
}
Чаму гэта быстрей
Ключовым фактарам являецца час між адправкай прыказу і чаканнем яго рэзультата. Выклік fetch(...) адправляе прыказ негайна; ён не чакае await. Дадзенне .then() проста апісвае, што робіць з адказам, калі ён нарэшце прыйде. Такім чынам у вышэўшам кодзе:
- першыя лініі адправляюць прыказ
/api/user; - другія лініі адправляюць
/api/posts, пакуль першы ўсё яшчэ у дарозе; - трэція лінія адправляе
/api/comments, пакуль першыя два ўсё яшчэ у дарозе; - толькі
Promise.allнасправды зупіняецца, і ён працюе зноў як толькі спадзейшы з трох праблемаў будзе завершана.
Абсалютны часы зменшаюцца з сумы трох запитоў да прыблізна трываласці самага повольнага з іх. Запиты, сервер і даны застаюцца незменнымі; толькі месца размешчэння оператыўны «await» зменілася. У ілюстратываным прыкладзе, калі кожны запит займае абоўша 300 мс плюс дадатковы час на обробку, гэта дазволяе сторанцы стаць прыдатной за абоўша 960 мс, а не за прыблізна 360 мс, пры чым разлік павяршаецца на повольных сетках або калі API занята.
Лічнае заўважлівасцяў да сторанцы з великай колькасцю дадзеных даўае такія результаты за мінімум зусиль. Не трэба дадаваць жадных бібліятэкаў і перэдзяляваць архітектуру; проста перакладзеныя калькі ліній коду.
На шта звернуць увагу пад час викорыстоўвання Promise.all
Promise.all абяроцецца як толькі хоць яны з ягох прамісаў абяроцецца. Якщо неудачны запит comments не должен знішчыць всю сторанку, выкорыстаўце Promise.allSettled або прызначыце кожнаму прамісу свой .catch(), які вяртае значэнне-замену. Таксама згадайце, што r.json() не перакантролюе r.ok; адпаведзь 404 або 500 з тэлам кешалёнкі ў формате JSON будзе апрацавана і вярнута як дахоўкі дадзенняў, таму перакантролюйце статус, калі правільнасць трэба залежаць ад яго.
Скрытая «водоспадная» структура між лейаутамі і сторанкамі
Разработчыкі, які выучылі трык з Promise.all, часта ўсё ж ствараюць другі тип такой «водоспадной» структуры, якая распрастараецца на файлы: як завантажэнне дадзенняў лейаута вялічыцца з завантажэнням дадзенняў сторанкі пад яным.
З самага пачатку SvelteKit запускае функцыі load з файлаў +layout.server.js і +page.server.js адночасна, так жа як і у прыкладзе вышэй. Апавядковы механізм — это функцыя parent(), якая дае функцыі load сторанкі доступ да дадзейнаў, якія вернулі лэйауты, якія знаходзяцца вышэй. Іноды самэ гэта і ўсё, што трэба, напрыклад, калі запит сторанкі выкалічвае ID пользователя, які можа быць заўяваны толькі лэйаутам. Аднак якщо ёё вызваць занадта рана, гэта спрычыніць серыялізацыю всіх наступных операцый:
// src/routes/dashboard/+page.server.js
// BAD: this creates a waterfall between the layout and the page,
// even if `posts` doesn't actually need anything from `parent()`.
export async function load({ parent, fetch }) {
const { user } = await parent(); // blocks here until layout's load finishes
const posts = await fetch(`/api/posts?userId=${user.id}`).then(r => r.json());
return { posts };
}
Якщо запит на павутыні дасправды трэбуе user.id, такая парадграфізацыя ўнемагчымлівае і ўсё ж цэлком нормальна; гэта справжня залежнасць. Проблема выступае тады, калі сторанцы таксама патрэбны даны, якія не маюць нічынаго з візуальным разметкаванням. Чаканне на parent() у першай лініі затрымляе всі наступныя запіты, укладзеныя ў той жа час, включаючы тые, якія є незалежнымі, пакуль не завершыцца процес разметкавання.
Рашэнне — пераранжаваць запіты: спачатку выконваць незалежную роботу, а чакаць на parent() — толькі тады, калі яго значэнне действительна патрэбна.
// src/routes/dashboard/+page.server.js
// GOOD: independent work starts immediately; parent() is only
// awaited once we actually need the merged result.
export async function load({ parent, fetch }) {
const commentsPromise = fetch('/api/comments').then(r => r.json());
const { user } = await parent(); // runs concurrently with the fetch above
const posts = await fetch(`/api/posts?userId=${user.id}`).then(r => r.json());
const comments = await commentsPromise;
return { user, posts, comments };
}
У гэтым случае запит на коментары адправляецца раней, чым сторанка чакае на готавароўку разметкі, таму яны перакрываюцься. Запит на павутыні все рава мусі чакаць на user.id, што є правільна, а обявленыя коментары зазвычай вже ўжо готавы на момент чакання ў канцы.
Аптэматычны прынцып: ставіце await parent() як і будзь-яе інша await-заявка. Размістайце яе адносова там, дзе ёй патрэбна значэнне, ніколі не на самым пачатку функцыі, і запускайце всі запиты, якія не залежаць ад дадзэйна роднай функцыі, яшчэ до ёё выканання.
Стрімаванне дадзэйна, якое не ўскладненага
Promise.all не завжды є правильным рашэнням. Якщо адзін запит ўповільнены, а його дадзэйн не патрэбен для таго, што відразу бачыць корыстнік, чаканне на яго робіць усю сторонку такой жа повольной, як і ўскладненай частка. На сторонцы товара імя, цэна і адпраўкі є важлівымі; раздел з адгукамі, які знаходзіцца на калькуляторах далей, — ні.
Для такога случая SvelteKit падтрымляе стрімаванне. Верніце праміс з сервера load без яго чакання, і SvelteKit адразу ж надасе выробленую сторонку, а потым доставіць значэнне праміса да браузера, калі ён будзе выкарыстоўвацца.
// src/routes/product/[id]/+page.server.js
export async function load({ fetch, params }) {
// Critical: awaited, blocks the initial render - but it's fast.
const product = await fetch(`/api/product/${params.id}`).then(r => r.json());
// Non-critical: NOT awaited. This is handed to the page as a
// pending Promise, and SvelteKit streams it in once it resolves.
const reviewsPromise = fetch(`/api/product/${params.id}/reviews`).then(r => r.json());
return {
product, // resolved value
reviews: reviewsPromise // still-pending promise
};
}
Продукт чакаецца, таму што пачатковая візуалізацыя яго трэбуе, а запит выконваецца быстра. Запит на пераказы запускаецца, але не чакаецца, таму сторанка отрымляе обявленую задачу пад data.reviews.
На сторанцы блок Svelte {#await} керуе обомі станамі. Унутры {#await data.reviews} візуалізуецца лёгкі заместак, напрыклад рэчып “Загружанне пераказаў...”, а ў галузі {:then reviews} пераглядаюцца рэзультаты. Неабяжны блок {:catch error} паказвае паведамленне, якщо запит не выйшаў.
Пользователь практычна адразу бачыць дакладні відомасці продукту, а рэгіон з пераказамі паказвае невялікі стан загружання; справжні пераказы заменяюць яго ў той момент, калі прыходзяць. Не існуе дадатковага коду для запытоў на боку кліента і няма хуків монтавання.
Аспекты стрімування
Згадайце пра гэтыя абмежэнні:
- Стрымінг работае за дапамою функцый загрузкі сервера. Обявленне ствараецца на сервере і стрымуецца з
+page.server.jsабо+layout.server.js. Універсальная функцыя загрузкі+page.jsтаксама може вярнуць обявленне, але яно не стрымуецца з сервера такім жа способам. - Ваш адаптар і хостынг павінны падтрымляць стрымованыя адпаведзі. Адаптары Node і Vercel падтрымляюць іх, як і большасць сучасных платформ, але пераканайцеся ў цым для менш популярных раштункоў. Проксі, якія буферуюць адпаведзі, таксама можу тыхо паспрыяць скасаванню гэтай перадзеі.
- Керуйце адхіленнямі. Стырымованае обявленне, якое адхіляецца без веткаў
{:catch}або працэсара.catch(), можа стаць некераваным адхіленнем на сервере. Дадзіце некритычным обявленням альтэратыўны варіант.
Полны цыкл жыцця запиту
За дапамогай паралельных критычных запрашэнняў, цяжкай упэўненасці ў parent() і стрімування некритычных дадзеных, запит да маршруту з великім калічынам дадзеных праходзіць праз следуючыя крокі:
- Браузер запрашае маршрут.
- SvelteKit выкананая функцыі распакоўкі маршруту і функцыі
loadсторанкі на серверы. - Усе незалежныя дадзеныя, такія як корыстувач, паспэўдзі і каментары, запрашаныя паралельна.
Пораўнайце гэта з версіяй, якая выконвалася на стороне кліента з самага пачатку. У замяну на тое, каб браузер адправляў тры послідовныя запыты пасля завантажэння стораніцы, сервер адправляе іх паралельна ўсё раней, чым што-небудзь будзе атрымана, і зазвычай з набліжна меншай затрымкай па адносу да джэрел даных, чым у браузера корыстніка.
Спіс пераконтраць пры выкладзе маршрутаў з великай кантэнтай даных
Расглядзіце гэтыя пытанні пры выкладзе маршрута, які патрэбуе калькі дадзеных:
- Чы гэтыя данні для пачатковага атрыбутавання запрашоўваюцца ў компоненте пад час яго стварэння? Пераместіце іх у функцыю
load, якая зазвычай знаходзіцца на сервере. - Чы ўнутроць функцыі
loadіснуе калькі незалежных вызоваў await паспялу? Спачатку запускайце всі запиты, а потым чакайце на ўсе яны за дапамогоюPromise.allабоPromise.allSettled. - Чы
await parent()— гэта першая лінія функцыіloadсторанкі? Пераместіце яе туды, дзе насправды выкарыстоўваюцца данні родзіча, і запускайце несвязаныя запиты раней. - Чы є якія-небудзь данні, якія не маюць значэння для пачатковага атрыбутавання? Верніце іх у вазе невыкарыстоўванага promise і атрыбутавайце іх за дапамогою
{#await}, пры чым неабходна выканаць блок catch.
fetch выходзіць з параметраў load? Толькі такая версія правільна перакладае абсылютныя URL-адрэсы і прыкладзяе кукі пасля адобразавання на серверы.Узагальненне
«Водоспады» не ўскладненняя кода — гэта нормальны наследак таго, калі запрашанні дадзейнаў розбросаны ў дрэве компонентаў. Функціі load у SvelteKit маюць меншое значэнне таму, што яны выкананы на серверы, а большое — таму, што яны збіраюць усе запрашанні, якія патрэбны сторанцы, en аднам візуабельным месца, дзе можна выбраць, калія з іх трэба выканаць разам, калія насправды залежна ад іншых, а калія можа прыйсці пазней. Неабходна толькі адна прычынка для змян: перастаць чакаць інстынктыўна і начаць чакаць свядома. Якщо гэта застосаваць да кожнай маршруту, гэта заўсёды зарабіць час для корыстнікаў нават пад спакойным з’яўленнем дадзейнаў і пад завантажаным серверам.