nuxt-rendering-modes.md — vim

«Нам 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, ни превью ссылок. Если это админка и ты видишь пустоту - всё в порядке, так и задумано.

Ссылки