«Нам SSR или SPA?» - неправильный вопрос
Режим рендеринга выбирается не на проект, а на маршрут. Как это делается в Nuxt и что обычно ломается при переезде со SPA.
Вопрос «нам SSR или SPA?» задают так, будто это решение на весь проект и на все годы вперёд. А это решение на конкретный маршрут, и в Nuxt его так и можно принимать.
Сначала быстро про варианты, чтобы дальше говорить на одном языке.
SPA. Сервер отдаёт пустой div и бандл, браузер всё рисует сам. Первый экран появляется поздно, поисковику достаётся пустота, зато переходы внутри мгновенные и сервер не нужен.
SSR. Сервер на каждый запрос собирает готовый HTML. Контент виден быстро, поисковик доволен. Платишь сервером под нагрузкой и более аккуратным кодом - в window уже так просто не полезешь.
SSG. HTML собирается один раз при билде и лежит на CDN. Самый быстрый и дешёвый вариант, но подходит только контенту, который не меняется на каждый запрос.
Про гидратацию, без которой не понять остального
И при SSR, и при SSG браузер получает готовый HTML, но он мёртвый: кнопки не нажимаются, пока не приехал и не выполнился JS, который навесит обработчики. Это и есть гидратация.
Отсюда две вещи, которые важны на практике.
Первая: HTML на экране не равно работающая страница. Если бандл тяжёлый, человек видит контент, тыкает и ничего не происходит. INP это ловит, а на глаз - нет, потому что ты-то знаешь, когда страница «уже загрузилась».
Вторая: разметка на сервере и на клиенте должна совпадать, иначе получишь ошибку гидратации. Классика жанра - new Date() прямо в шаблоне: на сервере одно время, в браузере другое.
Главное, ради чего берут Nuxt
routeRules. Режим задаётся на маршрут, всё в одном месте:
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true }, // лендинг - статика
'/blog/**': { prerender: true }, // статьи - статика
'/catalog/**': { swr: 600 }, // кэш 10 минут, потом фоновое обновление
'/admin/**': { ssr: false }, // приватная админка - обычное SPA
},
});
Маркетинг лежит на CDN, каталог отдаётся из кэша, личный кабинет работает как SPA. Не надо выбирать одно на всех.
Правило, по которому я решаю:
- страницу должен видеть поисковик или ссылку кидают в тг - значит SSR или SSG, иначе не будет ни индексации, ни превью;
- страница за логином - SPA, серверный рендер там ничего не даёт и только усложняет;
- контент меняется реже раза в час - предгенерация или SWR-кэш.
Что ломается при переезде SPA → SSR
Три вещи из девяти проектов из десяти.
window и localStorage на верхнем уровне. На сервере их нет:
// упадёт
const theme = localStorage.getItem('theme');
// нормально
const theme = ref('light');
onMounted(() => { theme.value = localStorage.getItem('theme') ?? 'light'; });
Глобальные синглтоны с состоянием. На сервере модуль живёт между запросами, и состояние одного пользователя утечёт другому. Для этого в Nuxt есть useState - состояние на запрос. Баг из этой серии искать неприятно: воспроизводится только под нагрузкой.
Двойные запросы. Данные, загруженные на сервере, надо передать клиенту, а не запрашивать заново:
const { data } = await useAsyncData('posts', () => $fetch('/api/posts'));
Быстрая проверка
Отключи JavaScript и открой страницу. Если это лендинг или статья и ты видишь пустоту - у тебя не работают ни SEO, ни превью ссылок. Если это админка и ты видишь пустоту - всё в порядке, так и задумано.
Ссылки
- Режимы рендеринга в Nuxt
- Гибридный рендеринг и
routeRules useStateиuseAsyncData- Ошибки гидратации во Vue
- Rendering on the Web - разбор всех вариантов