Реалізовуємо SSR у Nuxt 3: ключові аспекти та приховані нюанси
Привіт! Мене звати Михайло Кухарський, я Front-end Engineer в ІТ-компанії Futurra Group. Торік я вже ділився на DOU власним досвідом оптимізації перформансу в розробці на Nuxt 3. Зокрема, розповів про кілька перевірених методів і порад, які дійсно допомогли підвищити якість наших продуктів.
Хоча тема покращення швидкості вже стала більш покритою, Nuxt 3 залишається відносно молодим і навіть сирим фреймворком. У ньому досі є низка викликів і недоліків, які важливо враховувати.
Серед них такі:
- нечіткість та недостатня деталізація документації;
- потужний потенціал функціоналу для фетчингу даних, але водночас його неочевидність та недостатня зрозумілість для розробників;
- ризик можливих витоків пам’яті;
- невелика спільнота й обмежена популярність, через що нерідко бракує швидких і готових рішень;
- часті випадки, коли вибір Nuxt 3 є недоцільним, і наслідки такого вибору.
З кожною проблемою я стикався особисто, тому прагну вам допомогти уникнути їх у майбутньому.
У попередній статті я вже розповідав, що таке SSR. Цього разу ми детальніше розглянемо, як він працює, які переваги пропонує, з якими недоліками можна зіткнутися та як правильно реалізувати його в Nuxt 3.
Як працює SSR: переваги і недоліки
Для початку, очевидно, у вас має бути сервер, що буде виконувати код вашого застосунку, як це робить кожен юзер у браузері. Таким же чином, як і браузер користувача, сервер має повернути після запиту до нього ваш застосунок, але вже із завчасно виконаним JS, CSS, а також мати готовий HTML з динамічними даними (якщо дані не динамічні, виникає резонне запитання: навіщо вам SSR?).
Після цього стан застосунку має успішно синхронізуватись, а дані не повинні відрізнятись від отриманих на сервері, адже це неодмінно спричинить проблеми з гідрацією і кінцевим досвідом користувача.
Схематично SSR можна зобразити так:
Які безпосередні переваги надає SSR
- Швидше завантаження сторінок. Завдяки коректній імплементації значно зростає швидкість завантаження. Це особливо помітно для користувачів з повільним інтернет-з’єднанням, адже частина ресурсомістких процесів переноситься на сервер.
- Краще індексування сторінок. Підвищується якість індексації, що особливо актуально для сторінок із динамічним контентом, як-от стрічок соціальних мереж чи сторінок товарів в онлайн-магазинах.
- Поліпшена доступність застосунку. Сервер бере велику частину завантаження сторінок на себе, дозволяючи користувачам зі слабшими девайсами або підключенням не жертвувати якістю свого досвіду в застосунку.
Недоліки SSR
- Більші витрати, адже за коректну роботу сервера з вищим навантаженням доведеться платити більше.
- Надто велике SSR-навантаження може мати негативний вплив на швидкість завантаження. Звучить парадоксально, але так і є. Некоректне та недоречне використання сервера, зловживання його ресурсами, постійні перевантаження сторінок безпосередньо під час сесії користувача можуть лише погіршити швидкісні показники застосунку.
- Багато бібліотек, які не підтримують SSR. Це може призвести до проблем з гідрацією, адже код, який покладається на використання бібліотек, не зможе реалізуватися на сервері.
- Більший розмір проєкту вкінці, адже він буде містити не лише статичні дані та елементи, а й динамічні.
Якщо підсумувати, SSR — це не універсальне рішення у веброзробці, а лише один із підходів, корисний за певних сценаріїв. Не слід вибирати інструменти, такі як Nuxt 3, лише заради реалізації SSR, якщо він вам насправді не потрібен, оскільки це може лише ускладнити розробку та весь процес загалом.
Далі я розгляну кілька прикладів, які демонструють, як може ускладнитися розробка SSR-застосунку, з якими проблемами можна зіткнутися та як їх вирішувати. Також хочу поглибити розуміння імплементації SSR-запитів у Nuxt.
Вирішення проблеми з витоками памʼяті в разі неправильної реалізації SSR
Спершу наведу класичний стек технологій, які використовуються разом з Nuxt:
Сфокусуюся саме на Pinia, адже з нею пов’язана одна з гострих проблем, яку я хочу допомогти вам вирішити, а саме — витоки памʼяті в застосунку.
Pinia — це бібліотека для використання сховища у Vue/Nuxt-додатку, яка прийшла на заміну Vuex. І відбулося це не просто так, адже більша інтуїтивність, легкість використання, гнучкість та широта можливостей — ключові переваги Pinia. Вона дозволяє легко створювати сховища для даних у застосунку, використовуючи як синтаксис Options API, так і Composition API.
Почнемо зі встановлення:
npx nuxi@latest module add pinia
Ось як можна створити базове сховище стану:
// OPTIONS API const useSomeStore = defineStore("some-state-store", { state: (): ISomeState => ({ stateKey: 1 }), actions: { async setKey(key: number) { this.key = key; }, }, getters: { isNumberEqualToKey: (state: ISomeState) => { return (key: number) => { return state.key === key; }; }, isKeyEven: (state: ISomeState) => { return state.key % 2 !== 0; }, }, }); export default useUserStore;
В Options API прикладі можна побачити, що функція від Pinia defineStore()
приймає наступні аргументи:
Id — рядок, що визначатиме ідентифікатор вашого сховища.
У прикладі це some-state-store.
Options — обʼєкт, що описує можливості вашого сховища, а саме:
- його базовий стан (state);
- внутрішні функції взаємодії зі станом (actions). Actions можуть бути як синхронні, так і асинхронні;
- Getters. Це спеціальні властивості, які обчислюються на основі інших даних. Вони кешуються на основі їхніх залежностей, а коли одна з них змінюється — тоді перераховуються. Так, правильно, це добре вам відомі computed properties з Vue.
Я спеціально наголосив, що функції Getters
завжди приймають стан як аргумент, посилаючись на нього під час виклику і до моменту його зміни. Однак вони також можуть приймати додатковий аргумент. У такому випадку функції будуть перераховуватися в разі зміни або стану сховища, або додаткових аргументів.
Розглянемо цей же приклад оголошення сховища з Composition API:
// COMPOSITION API export const useSomeStore = defineStore("some-state-store", () => { const key = ref<number>(0); const isNumberEqualToKey = computed(() => { return (key: number) => { return key.value === key; }; }); const isKeyEven = computed(() => { return key.value % 2 !== 0; }); const setKey = (key: number) => { key.value = key; }; return { key, setKey, isKeyEven, isNumberEqualToKey }; });
Безпосередньо в інших файлах проєкту отримати бажану функцію чи значення зі сховища можна таким чином:
// Правильно (Реактивність працює вірно) const someStore = useSomeStore(); const { key, isKeyEven, isNumberEqualToKey, setKey } = storeToRefs(someStore); onMounted(() => { console.log(key.value); }); // Неправильно (Реактивність може працювати невірно) const someStore = useSomeStore(); onMounted(() => { console.log(someStore.key.value); }); // Або const { key } = useSomeStore(); onMounted(() => { console.log(key.value); });
Я пояснив усі ці деталі не просто так, а тому, що наша команда (та, відповідно до форумів і відгуків, інші розробники) зіткнулася з проблемою витоків пам’яті в застосунку.
Як саме проявляється проблема витоків
Наприклад, користувач хоче увійти у ваш застосунок, і після логіна ви намагаєтеся під час серверного запиту присвоїти йому його дані, але він отримує дані іншого користувача.
Чому це критично? По-перше, очевидно, що така проблема неминуче призведе до невдоволення користувачів вашим сервісом, адже неправильна поведінка системи може бути постійною та дратівливою. По-друге, сплутування даних різних користувачів створює серйозний ризик: будь-які дії з важливими чи чутливими даними можуть мати фатальні наслідки для вашого продукту.
Наприклад, якщо користувач 1 хоче змінити пароль свого акаунта, але збережені в сховищі дані не відповідають його актуальним, ця дія може ненавмисно зачепити дані користувача 2. У підсумку користувач 1 залишиться незадоволеним через неможливість виконати бажану операцію, а в акаунті користувача 2 може відбутися небажана зміна, що викличе ще більші проблеми.
Причини та як зарадити
Причиною витоків памʼяті є втрата контексту застосунку в Pinia-сховищі під час ініціалізації вашого сховища на стороні сервера, тобто:
- Ви намагаєтеся викликати Pinia-сховище на сервері, але не передаєте йому контекст;
- Pinia-сховище бере останній відповідний контекст і використовує дані з нього.
Це застереження безпосередньо вказано в документації до Pinia, і це правило застосовується до будь-якого використання сховищ поза setup
або композицій.
Таке може відбуватись, наприклад, у middleware у вашому застосунку, адже в Nuxt middleware виконуються як на серверній, так і на клієнтській стороні:
// НЕправильно export default defineNuxtRouteMiddleware(() => { const store = useStore(); const { token } = storeToRefs(store); if (!token.value) { return navigateTo("login"); } }); // Правильно export default defineNuxtRouteMiddleware(() => { const nuxtApp = useNuxtApp(); const store = useStore(nuxtApp.$pinia); const { token } = storeToRefs(store); if (!token.value) { return navigateTo("login"); } });
У нашому випадку саме втрата контексту призводила до проблем з витоками памʼяті. Вони були повністю вирішені саме після введення правильної передачі контексту. Ось як виглядали графіки використання памʼяті сервера раніше:
А ось який результат ми отримали опісля. Чітко видно момент релізу з виправленою проблемою:
З огляду на ці графіки можна помітити:
- З плином часу обсяг використання памʼяті зростав прямопропорційно і міг сягати більш як 2 ГБ.
- Кожен стик графіків — новий деплой застосунку, з яким використання памʼяті скидалось, тобто що довше живе екземпляр додатка, то більше памʼяті застосунок використовує.
- На цей момент проблема відсутня і застосунок використовує стабільний обсяг памʼяті ≤ 128 МБ.
Інші зміни, які нам допомогли:
- Оновлення бібліотек, які могли самі мати проблеми в роботі з памʼяттю.
Так, наприклад, бібліотека @nuxtjs/i18n, яка часто може використовуватись для локалізації застосунку, тривалий час мала проблему з витоками памʼяті, яку успішно виправили в одній з версій. Детальніше з цією проблемою можна ознайомитись на відкритих по цій темі GitHub Issues — Issue #2612, Issue #2034.
- Видалення надлишкової ініціалізації сховищ на сервері.
Рішення може видатись банальним, але воно насправді дієве. Записування великого обсягу даних чи велика кількість таких операцій безпосередньо впливають на те, як ваш застосунок використовує ресурси.
- Додавання липких сесій з серверної сторони.
Якщо дуже базово, липкі сесії — це функція лоад-балансерів, яка гарантує, що запити користувача завжди надсилаються на той самий інстанс під час сесії. Це безпосередньо не вирішує проблеми витоків, але стабілізує роботу застосунку. Нам це допомогло знайти корінь початкової проблеми.
У нашому випадку саме втрата контексту стала основною причиною витоків пам’яті. Цю проблему вдалося вирішити після впровадження контексту під час ініціалізації сховищ, орієнтованих на серверну сторону. Однак я не стверджую, що запропоновані рішення гарантовано усунуть витоки пам’яті в будь-якому застосунку з подібними проблемами. Причини витоків можуть бути значно складнішими, і мої рекомендації охоплюють лише частину можливих сценаріїв.
Отже, розібравшись з однією проблемою, яка може виникати в спробах імплементувати SSR у вашому застосунку, можемо перейти до наступного важливого пункту в цій темі, а саме — правильний фетчинг даних у Nuxt і способи його реалізації.
Фетчинг даних у Nuxt 3 із SSR
Здавалося б, це одна з найважливіших потреб у фронтенд-розробці. Однак, як я вже згадував раніше, можливості фетчингу в Nuxt дуже гнучкі й насправді потужні. Проте, на жаль, вони не завжди достатньо прозорі та зрозумілі для розробників.
Загалом Nuxt надає нам три методи для фетчингу даних із сервера: $fetch
, useFetch()
та useAsyncData()
. Розглянемо кожен з них окремо.
-
$fetch
Це допоміжна обгортка над бібліотекою ofetch, яка допомагає робити запити на сервер зручніше і швидше. Сама бібліотека працює як на сервері, так і в браузері та на воркерах, що, власне, і дозволяє робити запити не лише на клієнтській стороні.
Працює $fetch
майже ідентично до звичайного JS fetch. Простий запит виглядатиме так:
const makeApiCall = async () => { try { const apiURL: string = "/api/route"; const apiOptions: NitroFetchOptions<any> = { method: "GET", baseURL, // Посилання на ваш сервер headers: { Authorization: `Bearer token`, }, }; const response = await $fetch(apiURL, apiOptions); return response; } catch (err) { console.log(err); } };
$fetch
приймає в себе два аргументи:
url
— рядок-посилання, на яке ви хочете зробити запит;options
— обʼєкт з налаштуваннями вашого запиту, його метод, заголовки тощо.
Нічого складного у $fetch
немає, це лише обгортка, яку ми використовуємо в Nuxt 3 замість звичайного fetch або популярного axios.
2. useFetch & useAsyncData
Обидві функції є композиціями, що слід завжди тримати в голові, адже їх ми вже не можемо використати всередині якоїсь іншої функції, а лише в <script setup>
.
Базово вони обидві є SSR-friendly і дозволяють нам виконувати запити на сервер. Але чому тоді варто використовувати саме їх, а не $fetch
, і в чому різниця між ними двома?
useFetch()
— це просто обгортка useAsyncData()
та $fetch
, з якою легше працювати, коли не потрібна додаткова конфігурація чи дії під час запиту, і вона сама формує унікальний ключ, базуючись на вашому запиті. Тому далі я буду розглядати саме useAsyncData()
, адже уся магія відбувається саме там.
Дані, які повертаються до нас із вищезгаданих композицій:
data
: реактивний обʼєкт з результатами запиту;refresh/execute
: функція для повторного надсилання такого ж запиту;clear
: функція для очищення обʼєктів data, error та status;error
: обʼєкт з помилкою, якщо запит не був виконаний успішно;status
: рядок, який ідентифікує, на якому етапі перебуває виконання запиту;"idle"
,"pending"
,"success"
,"error"
.
Обʼєкт для більш точних налаштувань роботи композицій виглядає ось так (значення за замовчуванням підкреслені):
{ lazy?: true | false, // Булеве значення, чи повинен запит бути виконаний "ліниво", і до цього ми ще повернемось пізніше server?: true | false, // Булеве значення, чи повинен запит виконуватись лише на клієнтській стороні pick?: ['title', 'description'], // Масив строк, який спрямований на облегшення даних, що будуть повертатись, себто в обʼєкті з результатами повернуться лише "title" та "description" transform?: (data) => { return data.map(item => ({ title: item.title, description: item.description })) }, // Функція, що дозволить більш точно керувати обʼєктом з результатами, альтернатива до pick default?: () => {}, // Ф-ія, що дозволяє вказати, які дані потрібно повернути, якщо запит пройшов не успішно, або його статус ще досі `pending` getCachedData?: () => {}, // Ф-ія, що дозволяє керувати кешованими даними, що збереглись в памʼяті застосунку, до цього ще також повернемось детальніше згодом watch?: someVariable.value | [someVariable.value, anotherVariable.value], // Реактивні обʼєкти, або масив таких обʼєктів, при зміні значень яких, запит буде оновлено immediate?: true | false, // Булеве значення, чи буде запит виконуватись миттєво, як тільки компонент чи сторінка будуть використані deep?: true | false, // Булеве значення, чи потрібно повернути обʼєкт з результатами в вигляді реактивних даних dedupe?: 'cancel' | 'defer', // Строка, що визначає, чи потрібно надіслати запит декілька разів одночасно }
Думаю, більшість опцій зрозумілі, якщо ви знайомі з принципами реактивності та роботою з Vue 3, але кілька з них я хочу розглянути детальніше.
1. Почнемо з dedupe
. Цей ключ відповідає за стратегію поведінки запиту в useAsyncData
, якщо запит був надісланий більше як один раз відразу.
Значення за замовчуванням — “cancel”
, і воно зазначає, що якщо запит надіслано кілька разів, то всі минулі мають бути скасовані.
Альтернативне значення — “defer”
, і воно, своєю чергою, визначає, що якщо запит уже був надісланий один раз, то ніяких додаткових запитів надсилати не потрібно.
Ця опція може бути корисною під час «лінивої» відправки даних, яка залежить від кількох динамічних даних, що можуть швидко змінюватись, і не тільки.
2. Перейдемо до getCachedData()
. Ця опція надає прекрасні можливості оптимізації надсилання ваших запитів і роботи застосунку, але працювати з нею, очевидно, потрібно дуже обережно, адже кешування даних із запитів на фронтенді — далеко не завжди хороша ідея.
Ця функція приймає два аргументи:
key
: рядок, що визначає унікальний ідентифікатор даних із запиту;nuxtApp
: інстанс вашого Nuxt-застосунку.
Значення за замовчуванням виглядає так:
{ getCachedData: (key, nuxtApp) => nuxtApp.isHydrating ? nuxtApp.payload.data[key] : nuxtApp.static.data[key], }
Тобто якщо застосунок зараз на етапі гідрації, повернемо закешовані дані, якщо вони є.
Функція має повертати кешовані дані, якщо вони доступні, а також повертати null
або undefined
, що змусить запит спрацювати знову.
Це дозволяє відчутно оптимізувати фронтенд, кількість запитів, що йдуть на сервер, себто пришвидшити роботу на стороні користувача і зменшити навантаження на застосунок та сервер. Але зловживати цією опцією точно не варто.
Далі розглянемо, як відбувається «ліниве» завантаження даних в useAsyncData()
, адже тут є кілька підводних каменів.
Ми можемо вказати, що запит має бути відправленим у режимі lazy
двома способами:
// Правильно useLazyAsyncData("unique-key", () => getSomeData()); // Також правильно useAsyncData("another-unique-key", () => getSomeData(), { lazy: true });
До чого призведе додавання цієї опції
Для розуміння подальшої інформації варто зазначити, що useAsyncData()
викликається у двох випадках:
- Під час гідрації.
- Під час завантаження нового компонента чи сторінки, себто від навігації.
Якщо розглядати роботу під час гідрації, опція lazy
не відіграє для нас особливої ролі, адже користувач одразу отримує дані, завантажені завчасно на сервері, і ніяких затримок чи інших наслідків немає.
Проте під час навігації, коли потрібно завантажити компонент і надіслати запит вперше, можна стикнутися з небажаними наслідками, такими як:
- відсутність даних, поки запит не встиг виконатись;
- блокування цієї самої навігації до моменту виконання запиту.
Також хочу зазначити, що useAsyncData()
справно працює як з await перед нею, так і без await
, що може заплутати вас під час розробки.
Розглянемо, чи вплине await
на виконання вашої композиції і які варіанти його поєднання з опцією lazy
:
// 1. Правильно useAsyncData("unique-key", () => getSomeData()); // 2. Також правильно useLazyAsyncData("another-unique-key", () => getSomeData()); // 3. Теж правильно await useAsyncData("third-unique-key", () => getSomeData()); // 4. І це теж правильно await useLazyAsyncData("last-unique-key", () => getSomeData());
1. Немає await
, немає lazy
:
- усі дані з запиту будуть при початковому відмальовуванні;
- роутинг буде заблоковано.
2. Немає await
, є lazy
:
- запит буде надіслано, щойно це стане можливо;
- роутинг не буде заблоковано.
3. Є await
, немає lazy
:
- усі дані з запиту будуть при початковому відмальовуванні;
- роутинг буде заблоковано.
4. Є await
, є lazy
:
- виконання функції буде заблоковано до виконання минулих асинхронних операцій;
- роутинг не буде заблоковано.
Отже, незважаючи на те, що це може заплутати нас під час розробки, ніяких суттєвих змін сам по собі await
не несе, а ось lazy
дає нам дуже важливу можливість — блокувати навігацію чи ні.
Як на мене, для кращого користувальницького досвіду блокувати роутинг не варто, але, аби уникнути проблем з просіданням даних у перші моменти від завантаження під час використання «лінивого» методу, потрібно коректно опрацьовувати статус виконання запиту. На щастя, Nuxt дає нам зручну можливість це зробити:
<script setup lang="ts"> const { getSomeData } = useSomeComposable(); const { data, status } = useLazyAsyncData("some-id", () => getSomeData()); const isPending = computed(() => { return status.value === "pending"; }); const dataList = computed(() => { return data.value?.data || []; }); </script> <template> <div> <template v-if="isPending"> <SomeLoaderComponent /> </template> <template v-else> <DataComponent v-for="item in dataList" :key="item.id" :item-data="item.data" /> </template> </div> </template>
У наведеному прикладі, якщо статус запиту перебуває в стані опрацювання (pending), ми показуємо користувачу лоадер, щоб забезпечити більш плавний і приємний досвід взаємодії.
Зверніть увагу на зручну та корисну поведінку функції useAsyncData()
. Як я вже згадував раніше, вона повертає функцію refresh()/execute()
, що дозволяє повторно виконати запит і оновити дані. Це значно спрощує роботу, оскільки позбавляє необхідності зберігати дані в додаткових змінних та вручну контролювати їхній стан.
// Складніше // Робимо запит завдяки useAsyncData const { data } = await useAsyncData("identifier", () => getNewData()); // Коли запит виконано, записуємо його в змінну const storedData = ref(data.value); // При змінні якихось даних, надсилаємо запит знову і оновлюємо дані в змінній watch( () => someVariable.value, async (value) => { if (value) { const newData = await getNewData(); storedData.value = newData; } }, );
// Простіше // Робимо запит завдяки useAsyncData const { data, refresh } = useAsyncData("identifier", () => getNewData()); // При змінні якихось даних, надсилаємо запит знову і оновлюємо дані watch( () => someVariable.value, async (value) => { if (value) { await refresh() } }, );
Таким чином, ми можемо спростити керування станом і даними компонента, очистити кодову базу і не навантажувати її зайвими змінними та операціями, маючи єдине джерело даних і керування ними — data
та refresh.
Якщо ж вам потрібно виконати оновлення даних у компоненті поза його межами, Nuxt також має що запропонувати.
Ми не просто так передаємо унікальний ключ-ідентифікатор в useAsyncData()
. Це дає нам низку переваг:
- кешування;
- уникнення дублювання запитів;
- можливість отримання даних по всьому застосунку;
- оновлення конкретних запитів з будь-якого місця застосунку.
Саме тому не варто нехтувати створенням ключів для ваших запитів.
Розглянемо приклад.
Ми хочемо мати доступ до даних із запиту на сторінці B, хоча запит був зроблений на сторінці А. Ми можемо досягти цього завдяки функції useNuxtData()
, передавши як аргумент наш унікальний ключ:
// Сторінка А <script setup> const { data: posts } = await useAsyncData("posts", () => $fetch("/api/posts")); </script> <template> <div> <h1>Posts</h1> <ul> <li v-for="post in posts" :key="post.id">{{ post.title }}</li> </ul> </div> </template>
// Сторінка B <script setup> const { data: posts } = useNuxtData("posts"); </script> <template> <div> <h2>Latest Post</h2> <p v-if="posts && posts.length">{{ posts[0].title }}</p> </div> </template>
Схожим чином можемо оновити дані в компоненті А, де відбувається початковий запит, зі стороннього компоненту B завдяки refreshNuxtData()
:
// Компонент А <script setup> const { data, status } = await useAsyncData("user-profile", () => $fetch("/api/user")); </script> <template> <div> <h1>User Profile</h1> <p v-if="status === 'pending'">Loading...</p> <div v-else-if="data"> <p>Name: {{ data.name }}</p> <p>Email: {{ data.email }}</p> </div> </div> </template>
// Компонент В <script setup> const refreshUserProfile = async () => { await refreshNuxtData("user-profile"); }; </script> <template> <button @click="refreshUserProfile">Refresh User Profile</button> </template>
Отже, refreshNuxtData()
— це функція, що дозволяє оновити певні дані вашого застосунку. Як аргумент вона отримує рядок, що має відповідати ключу-ідентифікатору певного запиту. Таких ключів вона може приймати необмежену кількість, а якщо їх не буде надано взагалі, це призведе до оновлення всіх даних вашого застосунку, тому будьте обережні, аби її використання не призвело до небажаних наслідків.
Можна впевнено стверджувати, що useAsyncData()
— це дуже гнучка і потужна композиція, з опануванням якої розробка на Nuxt 3 стане в рази зручнішою, а реалізація SSR запитів — простішою.
Підсумовуючи...
... хочу зазначити, що SSR не завжди найкраще рішення для вебпродуктів, але в певних випадках воно може бути надзвичайно ефективним. Завдяки вибору саме цієї стратегії та використанню Nuxt 3 один з наших продуктів досяг чудових перформанс-показників, високого рівня SEO-оптимізації та значного задоволення користувачів.
Втім, варто розуміти, що цей результат — наслідок не лише правильного вибору інструментів, але й величезної праці, проведених досліджень та експериментів протягом усього процесу розробки.
Більшість інформації, якою я поділився, отримана не з документації чи сторонніх статей, а завдяки детальному аналізу вихідного коду Nuxt 3. Моя мета — допомогти вам уникнути таких самих лабіринтів і експериментів. Якщо ви вирішите працювати з Nuxt 3 та SSR, будьте готові стикнутися зі згаданими недоліками та, можливо, шукати власні методи їх подолання.
Буду радий почути ваші думки, виклики або ідеї по темі. Діліться своїм досвідом у коментарях, адже обмін знаннями допомагає розвиватися всім нам!
4 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарів