Введение
Nuxt 3 + shadcn-nuxt — это не “красивые кнопки на Vue”. Это подход, где вы владеете исходниками UI‑компонентов, получаете SSR/SSG/ISR из коробки, строите типобезопасные фичи и не расплачиваетесь за магию на проде. Ниже — рабочая конфигурация, архитектурные решения, подводные камни и практики, которые пережили прод.
Сценарий: строим SPA/SSR‑приложение на Nuxt 3 с консистентным UI через shadcn (shadcn-nuxt), Tailwind, темизацией, нормальной доступностью, и прицелом на масштаб (страницы, формы, сложные оверлеи, таблицы, модалки).
Маленькая история с продакшена: мы однажды “поймали” рост TTFB вдвое после innocuous правки — добавили три useAsyncData без ключей в разных компонентах. Дедупликация не сработала, начались N+1. После агрегации фетчинга и кеша на Nitro вернули TTFB в норму. Мораль: SSR — это бэкенд с шаблоном, а не просто “рендер на сервере”.
Почему Nuxt 3 — базовый выбор
Что даёт реальный профит:
- Nitro‑сервер: единый рантайм для SSR/SSG/ISR с адаптерами под Node, Vercel, Netlify, Cloudflare. На проде — свободная миграция провайдеров и тонкий контроль кэша через routeRules/headers.
- Vite: быстрый DX, HMR и шустрый бандлинг.
- Vue 3 + Suspense: потоковый SSR, адекватные async‑границы.
- Автоимпорт и файловая архитектура: меньше бойлерплейта (но следите за коллизиями).
- Типобезопасность: TS “из коробки”, nuxt-typed-router для маршрутов, zod/valibot для схем.
На высоких нагрузках критично:
- Убирать N+1 в SSR: агрегируйте запросы, используйте общий ключ useAsyncData и кеш на уровне Nitro/Redis.
- Контролировать payload: experimental.payloadExtraction, transform в useAsyncData, сериализуйте только нужные поля.
- Включать ISR/HTML‑кеш через routeRules и CDN, вариантный кеш по cookie/locale когда нужно.
Практические нюансы:
- На edge‑рантаймах не все Node‑модули доступны (crypto, fs). На Vercel/Cloudflare используйте Web Crypto/Web Streams или выносите тяжелые штуки в фоновые воркеры/очереди.
- Логирование и трассировка: request‑id в event.context, pino с transport на stdout, OpenTelemetry — сильно экономит время на дебаг при деградациях.
Что такое shadcn-nuxt и чем он лучше больших UI‑библиотек
shadcn-nuxt — это генератор компонентов: вы копируете исходники в свой репозиторий и владеете ими.
- Полный контроль верстки и стилей. Нет вендор‑локина и “black box” поведения.
- Tailwind + CSS‑переменные как дизайн‑токены: простая темизация.
- A11y через Radix Primitives (radix-vue): фокус‑менеджмент, порталы, ARIA.
- Предсказуемые пропы, читаемый код — легко дорабатывать под продукт.
Подводные камни:
- Порталы/телепорты оверлеев (Dialog/Popover/Dropdown): z‑index и stacking context. Частая боль — модалка под шапкой. Лечится единой шкалой z‑index и отсутствием лишних контекстов (overflow/transform на родителях).
- Порядок @layer base/components/utilities в Tailwind. Неправильный порядок ломает темы.
- Иконки: lucide‑vue‑next импортируйте точечно. “Импорт всего” — +200 КБ неожиданно.
Быстрый старт и конфигурация
Создание проекта:
npx nuxi@latest init my-project
cd my-project
npm i
Установка shadcn-nuxt и модулей:
npm i shadcn-nuxt @nuxtjs/tailwindcss lucide-vue-next tailwindcss-animate
nuxt.config.ts (выжимка):
export default defineNuxtConfig({
modules: [
'@nuxtjs/tailwindcss',
'shadcn-nuxt',
// 'nuxt-typed-router',
// '@nuxtjs/color-mode',
// '@nuxt/devtools',
],
shadcn: {
prefix: 'Ui',
componentDir: './components/ui',
},
experimental: {
payloadExtraction: true,
},
nitro: {
// preset: 'vercel-edge', // если точно понимаете ограничения
},
routeRules: {
'/': { isr: 60 },
'/blog/**': { isr: 300 },
'/api/public/**': { cache: { maxAge: 60 } },
},
tailwindcss: { viewer: false },
})
components.json:
{
"$schema": "https://shadcn-vue.com/schema.json",
"style": "new-york",
"typescript": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "assets/css/tailwind.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"composables": "@/composables",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib"
},
"iconLibrary": "lucide"
}
Tailwind базовая тема:
/* assets/css/tailwind.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root { /* токены светлой темы */ }
.dark { /* токены темной темы */ }
}
tailwind.config.js (главное — content‑пути и темизация):
module.exports = {
darkMode: ['class'],
content: ['./app.vue','./components/**/*.{vue,js,ts}','./pages/**/*.{vue,js,ts}','./layouts/**/*.{vue,js,ts}','./plugins/**/*.{js,ts}','./nuxt.config.{js,ts}'],
theme: { extend: { /* цвета от CSS‑переменных, радиусы */ } },
plugins: [require('tailwindcss-animate')],
}
Добавление компонентов точечно:
npx shadcn-vue@latest add button card input label dialog dropdown-menu
Совет: закрепляйте версии шаблонов компонентов. Обновляйте осознанно — иногда меняются классы/слоты.
Архитектура и организация проекта
Структура:
components/
├── ui/ # сгенерированные shadcn
├── partials/ # мелкие переиспользуемые куски
├── sections/ # секции страниц
└── widgets/ # композитные виджеты
lib/
├── utils/
└── validations/ # zod/valibot схемы
server/
├── api/ # h3‑эндпоинты
└── services/ # внешние клиенты, БД
Автоимпорт c префиксами снижает коллизии:
components: {
global: true,
dirs: [
{ path: '~/components/ui', prefix: 'Ui' },
{ path: '~/components/partials', prefix: 'P' },
{ path: '~/components/widgets', prefix: 'W' },
],
}
Композиция поверх UI (оборачивайте, не форкайте базовые):
<template>
<UiCard>
<UiCardHeader>
<UiCardTitle>{{ title }}</UiCardTitle>
<UiCardDescription v-if="description">{{ description }}</UiCardDescription>
</UiCardHeader>
<UiCardContent><slot /></UiCardContent>
<UiCardFooter class="flex gap-2 justify-end">
<UiButton variant="outline" @click="$emit('cancel')">Отмена</UiButton>
<UiButton @click="$emit('confirm')">Подтвердить</UiButton>
</UiCardFooter>
</UiCard>
</template>
<script setup lang="ts">
defineProps<{ title: string; description?: string }>()
defineEmits<{ cancel: []; confirm: [] }>()
</script>
A11y: не пропускайте
- Фокус в диалогах: по открытию — первый интерактивный, по закрытию — возврат. Тестируйте таб‑циклы.
- Контраст валидируйте в CI (axe, pa11y). Playwright + @axe-core/playwright — must‑have.
- Не ломайте клавиатурную навигацию кастомными keydown.
- Проверяйте, что portal‑контейнеры доступны скринридерам, aria‑атрибуты на местах.
В проде регрессии по a11y часто приходят из правок overflow/z‑index. Скриншотные тесты плюс axe ловят это до релиза.
Производительность: что реально работает
Гидрация:
- Тяжелые виджеты (таблицы, редакторы) — динамический импорт + ClientOnly за пределами LCP.
- Попапы/модалки лучше держать смонтированными и “спящими” — быстрее, чем монтировать каждый раз.
- Избегайте reactivity transform — удобно, но даёт трудноотлавливаемые баги, и официально не рекомендуется.
Фетчинг данных:
// pages/users.vue
const { data: users } = await useAsyncData('users:list', () =>
$fetch('/api/users', { query: { limit: 50 } }),
{
server: true,
transform: (raw) => raw.items.map(({ id, name }) => ({ id, name })), // режем лишнее
}
)
Антипаттерны:
- Несколько useAsyncData без общих ключей — потеря дедупликации, N+1.
- Жирный payload — медленный TTFB. Режьте поля, включайте payloadExtraction.
- SSR внутри v-for дергает API для каждого элемента — агрегируйте на сервере.
Кеш и ISR:
- Маркетинг/блог: routeRules isr + Cache‑Control на CDN.
- Дорогое API кешируйте в Redis. В event.context инжектируйте клиент, используйте defineCachedEventHandler.
- Обязательно таймауты/ретраи при деградации бэка. “Оно обычно быстро” — плохая стратегия.
Анализ бандла:
- rollup-plugin-visualizer, vite-bundle-visualizer — проверяйте, кто привёз +200 КБ.
- Иконки импортируйте по именам, не wildcard.
Темизация
- Токены в
и .dark. Меняем значения — меняется дизайн, без охоты за классами. - @nuxtjs/color-mode с mode: 'class' избавит от FOUC.
- Договоритесь о шкале z‑index заранее (например, 10/50/100/1000 для оверлеев/тостов/модалок/навигации).
Серверная часть и интеграции
Nitro — это полноценный рантайм:
// server/api/users.get.ts
import { z } from 'zod'
const Query = z.object({ limit: z.coerce.number().min(1).max(100).default(20) })
export default defineEventHandler(async (event) => {
const { limit } = Query.parse(getQuery(event))
// зовем сервис/БД
return { items: await listUsers({ limit }) }
})
Интеграции в реальных проектах:
- Отдельный бэкенд: NestJS + Fastify для high‑load API, Nuxt — как BFF или фронт. Prisma — быстрый старт и миграции; TypeORM — если нужна глубокая ORM‑магия. На serverless‑БД (Neon/PlanetScale) контролируйте пул и холодный старт, смотрите в сторону Prisma Accelerate/Data Proxy.
- Redis для кеша/сессий, BullMQ для очередей. Снимает лавину запросов при пиках.
- AsyncLocalStorage (в Nest) и event.context (в Nitro) для request‑scoped логов и трейсинга.
- Безопасность: CSP и security‑заголовки в routeRules.headers, SameSite=Lax для BFF‑кук, CSRF там, где нужны state‑changing формы.
Типичные прод‑проблемы:
- Утечки памяти из SDK и незакрытых стримов при SSR. Профилируйте heap, ищите hanging timers.
- Блокировка event loop синхронным crypto/zlib — уносите в worker threads/очереди.
Типобезопасность и DX
- nuxt-typed-router для типизированных маршрутов.
- Формы: zod/valibot + vee-validate или @vueuse/form. Одна схема на клиенте и сервере.
- Генерация типов для API: openapi-typescript, graphql-codegen.
- useRuntimeConfig для секретов; валидируйте env Zod’ом на старте.
Тестирование и качество
- Unit: Vitest + Vue Test Utils.
- E2E: Playwright (скриншоты + axe).
- Линтинг: ESLint + eslint-plugin-vue + typescript-eslint. Правила на циклические зависимости.
- Перф: Lighthouse CI в PR, guard по bundle size.
Частые проблемы и решения
- Компоненты shadcn “сломались” после обновления Tailwind:
- Проверьте порядок @tailwind/@layer.
- Перегенерируйте компоненты CLI и смотрите diff.
- Модалки под хедером:
- Нет ли у родителей overflow/transform? Телепортируйте в body, задайте шкалу z‑index.
- Автоимпорт дребезжит:
- Префиксы + единообразные имена. Не дублируйте UiButton — делайте обёртку WButton с вашей логикой.
- Деплой на edge:
- Node‑специфика недоступна. Используйте Web‑аналоги или Node‑preset.
Заключение
Nuxt 3 и shadcn-nuxt одинаково уверенно чувствуют себя в MVP и зрелых продуктах:
- SSR/SSG/ISR под разные профили трафика.
- Контролируемый UI без вендор‑локина и с сильным a11y.
- Типобезопасный DX без лишнего бойлерплейта.
- Прямая интеграция с бэком (NestJS/Fastify, Prisma/TypeORM, Redis, BullMQ).
Если держать в фокусе производительность, a11y и архитектуру данных, стек отрабатывает сполна: быстрый интерфейс, предсказуемый TTFB и стабильность под нагрузкой — без магии и сюрпризов.