Next.js
Экспресс-шпаргалка 20/20
- App Router использует nested layouts, server components и file-based conventions нового поколения по сравнению с Pages Router.
- Server Components рендерятся на сервере и уменьшают JS в браузере; Client Components нужны для интерактивности и browser API.
- SSR - динамика/персонализация, SSG - статический контент, ISR - периодическое обновление pre-rendered страниц.
- Кэш в Next управляется на уровне fetch/route/page и контролируется revalidate/tag/path invalidation.
- Route Handlers - это серверные HTTP endpoints внутри app router, удобны для BFF сценариев.
- Hydration mismatch происходит, когда серверный HTML не совпадает с клиентским рендером.
- loading/error states реализуются через
loading.js,error.jsи Suspense boundaries. - Dynamic rendering нужен при request-time данных, static - когда данные стабильны и важна скорость/SEO.
generateMetadataформирует SEO-метаданные на сервере и должен учитывать data freshness.- Оптимизация media:
next/image,next/font, контроль размеров и preload критических ассетов. - Границы server/client нужно держать явными через
use clientи не импортировать server-only код в клиент. - Server Actions - серверные мутации рядом с UI, но требуют проверки прав и безопасной обработки ошибок.
- Авторизацию в Next обычно строят через middleware + server-side проверки + secure cookies.
- TTFB/LCP улучшают через кэш, streaming, уменьшение blocking fetch и оптимизацию critical path.
- BFF в Next подходит для умеренной сложности; при росте домена лучше выделенный backend.
- Edge/runtime дает меньшую latency ближе к пользователю, но имеет ограничения по API окружения.
- Глобальный state без boundaries усложняет hydration и увеличивает стоимость ререндеров.
- Инвалидация кэша после мутаций делается через revalidatePath/revalidateTag и явные правила freshness.
- i18n в Next проектируют через локализованные маршруты, словари и корректные SEO alternate links.
- Частые anti-patterns: смешивание server/client, отсутствие cache strategy и переизбыток client components.
1. Что изменилось в App Router по сравнению с Pages Router?
Теги: nextjs, react
Сложность: Middle/Senior
Короткий ответ
App Router переносит основную модель Next.js в app/: nested layouts, Server Components по умолчанию, file conventions вроде page, layout, loading, error, Route Handlers и новую модель data fetching/cache. Pages Router остается рабочим legacy-подходом на pages/, getServerSideProps, getStaticProps и API routes.
Что сказать на интервью (30-60 секунд)
Я бы не говорил "App Router просто новее". Главное изменение - архитектурная модель: layouts сохраняют состояние между навигациями, Server Components уменьшают клиентский JS, а загрузка/ошибки/streaming становятся частью route tree. При миграции риск не в синтаксисе, а в границах: где остается Pages Router, где нужен Client Component, где меняется кэш и где нельзя переносить старый data fetching один-в-один.
Мини-пример
// app/products/[id]/page.tsx
export default async function ProductPage({ params }) {
const product = await getProduct(params.id);
return <ProductDetails product={product} />;
}
Пояснение
В Pages Router страница была главной единицей маршрута. В App Router единица - сегмент route tree: layout, page, loading, error, not-found, route handler. Это позволяет держать общую оболочку, стримить части UI и точнее ставить error/loading boundaries.
Сильный ответ на интервью: App Router стоит внедрять не ради моды, а когда нужны RSC, nested layouts, server-first data fetching, streaming и более явная route composition. Если продукт стабилен на Pages Router, миграция должна идти сегментами, с проверкой cache semantics и метрик.
Углубление (2-3 минуты)
| Область | Pages Router | App Router |
|---|---|---|
| Route root | pages/ | app/ route segments |
| Data fetching | getServerSideProps, getStaticProps | async Server Components, fetch, cache controls |
| Layouts | через _app и композицию | nested layout.tsx |
| Loading/error | вручную | loading.tsx, error.tsx, Suspense boundaries |
| API | API routes | Route Handlers |
| Client JS | чаще page-level hydration | Server Components by default |
Кейс: dashboard имеет общую sidebar и много вложенных pages. В Pages Router sidebar часто пересоздается и state теряется. В App Router nested layout может сохранить state, а тяжелые данные уйдут в Server Components.
Практика
- Составьте router migration boundary map: какие routes остаются в
pages, какие уходят вapp, какие shared layouts нужны. - Для одного route отметьте: static/dynamic data, loading boundary, error boundary, client islands.
- Объясните, почему нельзя переносить
getServerSidePropsнапрямую вapp/.
Типичные ошибки
- Мигрируют весь проект разом без route-by-route плана.
- Переносят старые data fetching паттерны без понимания cache semantics.
- Делают весь route tree Client Components и теряют преимущество RSC.
- Забывают про новые error/loading boundaries.
Follow-up вопросы
- Какие части проекта можно оставить в Pages Router?
- Как nested layouts влияют на UX навигации?
- Почему App Router меняет подход к data fetching?
- Какие риски у route-by-route миграции?
Что повторить
- App Router file conventions:
page,layout,loading,error,route. - Server Components и data fetching in
app/. - Миграцию с Pages Router на App Router.
Связанные модули и карта
2. Разница между Server Components и Client Components?
Теги: nextjs, react
Сложность: Middle/Senior
Короткий ответ
Server Components выполняются на сервере и не отправляют свой JavaScript в браузер. Client Components нужны для state, effects, event handlers и browser APIs; граница задается директивой 'use client'.
Что сказать на интервью (30-60 секунд)
Я начинаю с правила: в App Router компонент серверный по умолчанию, а client boundary надо делать минимальной. Server Component хорошо подходит для чтения данных, доступа к server-only secret, тяжелой серверной логики и уменьшения bundle. Client Component нужен там, где есть useState, useEffect, обработчики кликов, window, localStorage. Ошибка - поставить 'use client' слишком высоко и утащить весь layout в client bundle.
Мини-пример
// Server Component
export default async function ProductPage() {
const product = await getProduct();
return <AddToCart productId={product.id} />;
}
'use client';
export function AddToCart({ productId }) {
return <button onClick={() => addToCart(productId)}>Add</button>;
}
Пояснение
Граница Server/Client - это не граница "важное/неважное", а граница исполнения. Server Component может читать базу или секреты, но не может иметь browser event handlers. Client Component может быть интерактивным, но все, что он импортирует, попадает в клиентский граф.
Практический критерий: держите страницу, data fetching и layout серверными, а интерактивность выносите маленькими client islands. Так вы снижаете JS cost и сохраняете UX.
Углубление (2-3 минуты)
| Нужна возможность | Server Component | Client Component |
|---|---|---|
| Data fetch на сервере | да | лучше избегать без причины |
| Secret/env server-only | да | нет |
useState/useEffect | нет | да |
| Event handler | нет | да |
| Browser API | нет | да |
| Меньше JS в браузере | да | нет, если расширить boundary |
Кейс: карточка товара делает весь page 'use client', чтобы работала кнопка "Купить". Сильное решение: оставить product data и layout серверными, а в client вынести только кнопку/виджет корзины.
Практика
- Составьте Server/Client component split table для страницы товара: layout, list, price, filters, cart button.
- Найдите место, где
'use client'можно опустить ниже. - Проверьте bundle impact: какой импорт случайно уехал в client graph.
Типичные ошибки
- Ставят
'use client'наlayout.tsxбез необходимости. - Импортируют server-only код в Client Component.
- Передают в Client Component несериализуемые props.
- Путают Server Components с SSR: это связанные, но разные механики.
Follow-up вопросы
- Почему Server Component не может иметь
onClick? - Что происходит с импортами внутри Client Component?
- Какие props можно передавать через server-client boundary?
- Как RSC влияет на LCP и JS bundle?
Что повторить
- Директиву
'use client'. - Serializable props и client boundary.
- Server-only imports, bundle analysis, client islands.
Связанные модули и карта
3. Как выбирать между SSR, SSG и ISR в Next.js?
Теги: nextjs, react, rendering
Сложность: Middle/Senior
Короткий ответ
SSR выбирают для request-time данных и персонализации, SSG - для стабильного контента на build time, ISR - для статической страницы с периодическим или on-demand обновлением. В App Router это выражается через cache/dynamic/revalidate настройки, а не только через старые функции Pages Router.
Что сказать на интервью (30-60 секунд)
Я выбираю стратегию от freshness и персонализации. Маркетинговая страница или docs - SSG. Каталог, который можно обновлять раз в минуту или по событию, - ISR/revalidation. Личный кабинет, цена по пользователю или cookies/headers - dynamic SSR. Главный риск - случайно сделать динамической страницу, которая могла быть статической, или закэшировать персональные данные.
Мини-пример
export const revalidate = 60;
export default async function Page() {
const res = await fetch('https://api.example.com/data', { next: { revalidate: 60 } });
return <pre>{JSON.stringify(await res.json(), null, 2)}</pre>;
}
Углубление (2-3 минуты)
| Стратегия | Когда выбирать | Риск |
|---|---|---|
| SSG/static | контент одинаков для всех и редко меняется | stale до rebuild/revalidate |
| ISR/revalidate | нужна скорость static и контролируемая свежесть | сложная инвалидация |
| SSR/dynamic | персонализация, cookies, request-time auth | выше TTFB и нагрузка |
| CSR | приватный виджет без SEO требований | хуже first load и SEO |
Кейс: product listing не персональный, обновляется CMS раз в 5 минут. Хороший выбор - ISR/revalidate/tag invalidation, а не SSR на каждый запрос. Для checkout наоборот нужна request-time логика и нельзя кэшировать пользовательские данные.
Практика
- Составьте rendering strategy matrix для: marketing page, product page, cart, admin dashboard.
- Укажите freshness SLA и источник персонализации.
- Опишите, как измерить TTFB/LCP до и после смены стратегии.
Типичные ошибки
- Делают SSR для всего, потому что "так свежее".
- Кэшируют response, зависящий от cookies/user.
- Не фиксируют freshness SLA.
- Путают ISR и client-side refetch.
Follow-up вопросы
- Когда static page может стать dynamic?
- Чем ISR отличается от SSR с cache header?
- Как cookies/headers влияют на rendering strategy?
- Что важнее для страницы: freshness или TTFB?
Что повторить
- Static vs dynamic rendering in App Router.
revalidate,cache,no-store,headers/cookies.- TTFB, LCP и freshness trade-off.
Связанные модули и карта
4. Как устроен кэш данных в Next.js и revalidation?
Теги: nextjs, react, cache
Сложность: Middle/Senior
Короткий ответ
Кэш в Next.js управляется на нескольких уровнях: request memoization, data cache, full route cache и router cache. Для данных важны fetch options, revalidate, tags, revalidateTag, revalidatePath и понимание, что именно инвалидируется.
Что сказать на интервью (30-60 секунд)
Я отвечаю через freshness contract: какие данные можно кэшировать, на сколько, и что происходит после мутации. В App Router fetch может быть cached/revalidated, а invalidation может идти по path или tag. Типичная ошибка - после Server Action обновить данные в базе, но не вызвать revalidation, и пользователь видит stale UI.
Мини-пример
await fetch('https://api.example.com/products', {
next: { revalidate: 300, tags: ['products'] },
});
// После мутации:
revalidateTag('products');
Пояснение
Кэш в Next.js нельзя объяснять одной фразой. Нужно различать "мы не дергаем один и тот же fetch дважды в рамках render pass" и "данные переживают запросы и deployment". Также важно помнить, что no-store и dynamic APIs сдвигают route к динамическому поведению.
Сильный ответ показывает timeline: первый запрос заполняет cache, следующие читают cache, после TTL или revalidateTag данные считаются stale/инвалидируются, следующий render получает свежую версию.
Углубление (2-3 минуты)
| Механизм | Что решает | Ловушка |
|---|---|---|
| Request memoization | дедупликация fetch в одном render pass | не заменяет persistent cache |
| Data cache | переиспользование данных между запросами | stale без стратегии |
| Full route cache | cache HTML/RSC payload route | ломается при dynamic data |
| Router cache | client-side navigation cache | пользователь может видеть старый payload |
revalidatePath | инвалидировать route/path | слишком широкий blast radius |
revalidateTag | инвалидировать группу данных | нужны правильные tags |
Практика
- Составьте cache/revalidation timeline для product list после изменения цены.
- Выберите
revalidatePathилиrevalidateTagи объясните blast radius. - Назовите, какие данные нельзя кэшировать: user-specific, auth-sensitive, request-only.
Типичные ошибки
- Кэшируют персональные данные без привязки к user/request.
- Инвалидируют весь path, когда нужен tag.
- Не понимают разницу между server data cache и client router cache.
- После мутации забывают revalidation.
Follow-up вопросы
- Чем
cache: 'no-store'отличается отrevalidate: 0? - Когда выбирать
revalidatePath, а когдаrevalidateTag? - Как кэш влияет на TTFB и stale data?
- Что будет после Server Action без revalidation?
Что повторить
- Data cache, full route cache, router cache.
fetchcache options,revalidate, tags.revalidatePath,revalidateTag, stale UI after mutation.
Связанные модули и карта
5. Что такое Route Handlers и когда их использовать?
Теги: nextjs, react
Сложность: Middle/Senior
Короткий ответ
Route Handlers - это HTTP endpoints в app/.../route.ts, которые обрабатывают GET, POST и другие методы через Web Request/Response. Их используют для webhooks, BFF endpoints, proxy/light API, auth callbacks и server-only integrations.
Что сказать на интервью (30-60 секунд)
Я не использую Route Handler как замену полноценному backend по умолчанию. Он хорош, когда endpoint близок к Next-приложению: form submit, webhook, BFF aggregation, signed URL, auth callback. Но в нем все равно нужны обычные backend-правила: validation, auth, rate limit, error shape, runtime choice, cache headers и observability.
Мини-пример
// app/api/users/route.ts
export async function GET(request) {
const session = await requireSession(request);
return Response.json({ userId: session.user.id });
}
Пояснение
Route Handler живет рядом с route tree, но это серверный endpoint. Он не должен импортироваться в Client Component как функция. Если задача - отрендерить данные для страницы, часто лучше Server Component. Если нужна HTTP boundary для внешнего клиента, webhook или browser fetch, тогда Route Handler уместен.
Сильный ответ включает runtime constraints. Edge runtime хорош для latency и простых операций, но имеет ограничения окружения. Node runtime нужен для Node APIs, тяжелых SDK или долгих операций.
Углубление (2-3 минуты)
| Use case | Route Handler подходит? | Почему |
|---|---|---|
| Webhook от провайдера | да | нужен HTTP endpoint и server secret |
| Данные для Server Component | часто нет | проще fetch/query прямо на сервере |
| BFF aggregation | да, если умеренная логика | близко к frontend contract |
| Долгая доменная операция | осторожно | лучше backend/queue |
| Public API для многих клиентов | осторожно | нужны версии, contracts, rate limits |
Практика
- Составьте Route Handler responsibility checklist: method, auth, validation, cache, runtime, error, observability.
- Для трех сценариев выберите: Server Component, Server Action, Route Handler или отдельный backend.
- Объясните, какие headers/cache policy должен вернуть
GETendpoint.
Типичные ошибки
- Тащат всю backend-доменную логику в
app/api. - Не валидируют request body и permissions.
- Путают Route Handler с Server Component data fetching.
- Забывают про cache headers и runtime limits.
Follow-up вопросы
- Чем Route Handler отличается от Server Action?
- Когда лучше отдельный backend вместо
app/api? - Как выбрать Node vs Edge runtime?
- Как Route Handler влияет на cache и security?
Что повторить
app/.../route.tsconventions.- Web
Request/Response,NextRequest,NextResponse. - BFF, webhooks, runtime choice, validation/auth.
Связанные модули и карта
6. Как избежать hydration mismatch в Next.js?
Теги: nextjs, react, rendering
Сложность: Middle/Senior
Короткий ответ
Hydration mismatch возникает, когда HTML, сгенерированный на сервере, отличается от первого клиентского рендера. Лечат не подавлением warning, а детерминированным initial render: одинаковая разметка, валидный HTML, перенос browser-only логики в useEffect или точечное отключение SSR только там, где это оправдано.
Что сказать на интервью (30-60 секунд)
Я бы объяснил, что hydration - это момент, когда React прикрепляет интерактивность к уже отрендеренному HTML. Если сервер показал одно, а первый render в браузере ожидает другое, получаем mismatch. Типичные причины: Date(), random values, window/localStorage в render, различия locale/timezone, неправильная вложенность HTML, CSS-in-JS config или расширения/CDN, которые меняют HTML. Исправление начинается с поиска недетерминированного initial render.
Мини-пример
'use client';
import { useEffect, useState } from 'react';
export default function Clock() {
const [now, setNow] = useState('');
useEffect(() => setNow(new Date().toISOString()), []);
return <span>{now}</span>;
}
Такой вариант не пытается отрендерить текущее время на сервере и в первом клиентском render по-разному; значение появляется после mount.
Пояснение
Важно не путать hydration mismatch с обычным state update. Если первое дерево совпало, а потом useEffect обновил UI, это нормально. Ошибка возникает, когда первое клиентское дерево уже отличается от серверного HTML.
Иногда допустимы suppressHydrationWarning или dynamic import с ssr: false, но это точечные исключения. Если ими закрывать системную проблему, вы теряете SEO/перформанс и маскируете реальные баги.
Углубление (2-3 минуты)
| Причина | Симптом | Исправление |
|---|---|---|
Date()/random в render | текст не совпадает | вычислить на сервере и передать, либо после mount |
window/localStorage в render | server/client ветки разные | читать в useEffect |
| Невалидный HTML | React перестраивает DOM | исправить nesting |
| Locale/timezone | разные даты/числа | фиксировать locale/timezone или hydrate placeholder |
| Browser-only widget | SSR невозможен | dynamic import без SSR для маленького island |
| CDN меняет HTML | mismatch только в prod | отключить HTML mutation |
Кейс: цена выводится через toLocaleString() без фиксированной locale, в проде сервер и клиент форматируют по-разному. Хороший ответ: фиксировать locale/currency на сервере и клиенте, покрыть visual/regression тестом и проверить production HTML.
Практика
- Составьте hydration mismatch diagnosis table: источник недетерминизма, где рендерится, как стабилизировать.
- Найдите в примерах 5 опасных выражений:
Date,Math.random,window,localStorage, locale formatting. - Объясните, когда
suppressHydrationWarningдопустим, а когда это маскировка ошибки.
Типичные ошибки
- Проверяют
typeof window !== 'undefined'прямо в JSX render и получают разные деревья. - Используют
suppressHydrationWarningкак универсальный fix. - Рендерят текущее время/случайные id без стабильного seed.
- Не проверяют валидность HTML nesting.
Follow-up вопросы
- Чем hydration mismatch отличается от обычного client update?
- Почему
Date()в render опасен? - Когда лучше
useEffect, а когдаssr: false? - Почему неправильная вложенность HTML может дать mismatch?
Что повторить
- React hydration и deterministic first render.
- Next.js hydration error causes/fixes.
- Browser-only APIs, locale/timezone, valid HTML nesting.
Связанные модули и карта
7. Как организовать loading/error states в App Router?
Теги: nextjs, react
Сложность: Middle/Senior
Короткий ответ
В App Router loading/error states задаются на уровне route segment: loading.tsx автоматически создает Suspense fallback, а error.tsx оборачивает сегмент в Error Boundary и должен быть Client Component. Это позволяет изолировать ожидание и сбои без падения всей страницы.
Что сказать на интервью (30-60 секунд)
Я проектирую loading/error не как "поставить спиннер", а как карту границ. loading.tsx нужен там, где сегмент может ждать данные и надо сохранить shell. error.tsx нужен там, где сбой можно локально восстановить или показать retry. Граница должна быть достаточно низкой, чтобы ошибка карточки не рушила весь dashboard, и достаточно высокой, чтобы не плодить хаотичные fallbacks.
Мини-пример
// app/dashboard/loading.tsx
export default function Loading() {
return <DashboardSkeleton />;
}
// app/dashboard/error.tsx
'use client';
export default function Error({ error, reset }) {
return <RetryPanel message={error.message} onRetry={reset} />;
}
Пояснение
loading.tsx работает через Suspense: пользователь может видеть мгновенный fallback, пока сегмент грузится или стримится. error.tsx ловит runtime errors в сегменте и вложенных children, но сам должен быть Client Component, потому что Error Boundary в React работает на клиенте.
Сильный ответ учитывает UX: skeleton должен соответствовать будущему layout, иначе будет CLS; error UI должен быть понятным, иметь retry/reset, логировать digest/error id и не показывать пользователю raw stack.
Углубление (2-3 минуты)
| Граница | Что покрывает | Риск ошибки |
|---|---|---|
| Root layout | глобальный shell | слишком широкий fallback |
| Route segment | один раздел/страницу | хороший default |
| Вложенный segment | конкретную вкладку/панель | точечная изоляция |
| Component Suspense | локальный async block | можно переусложнить |
global-error.tsx | root-level failure | должен быть крайним fallback |
Кейс: analytics dashboard имеет 4 панели, одна зависимость иногда падает. Плохой UX - весь dashboard показывает 500. Хороший UX - error boundary только у панели, остальные данные остаются видимыми, есть retry и error id для поддержки.
Практика
- Составьте loading/error boundary map для dashboard: layout, tabs, chart, table.
- Для каждого boundary укажите fallback, retry action и что логировать.
- Проверьте, что skeleton не меняет размеры и не создает CLS.
Типичные ошибки
- Делают один глобальный error boundary и теряют локальную изоляцию.
- Показывают raw error пользователю.
- Skeleton не совпадает с итоговым layout и дает layout shift.
- Забывают, что
error.tsxдолжен быть Client Component.
Follow-up вопросы
- Чем
loading.tsxсвязан с Suspense? - Почему
error.tsxдолжен быть Client Component? - Где поставить boundary: выше или ниже?
- Как сделать retry без полной перезагрузки страницы?
Что повторить
loading.tsx,error.tsx,global-error.tsx.- Suspense boundaries и streaming.
- Skeleton UI, retry/reset, error logging.
Связанные модули и карта
8. Когда нужен dynamic rendering, а когда static?
Теги: nextjs, react
Сложность: Middle/Senior
Короткий ответ
Static rendering подходит, когда результат одинаков для пользователей и может быть кэширован заранее. Dynamic rendering нужен, когда route зависит от request-time данных: cookies, headers, search params, auth/session, uncached fetch или явно заданного dynamic behavior.
Что сказать на интервью (30-60 секунд)
Я выбираю static по умолчанию для публичных страниц, где важны TTFB, CDN cache и SEO. Dynamic выбираю, если ответ зависит от конкретного запроса: user session, гео по header, AB test cookie, private dashboard. Главная ловушка - случайно вызвать dynamic API внутри route и превратить страницу в dynamic без понимания стоимости.
Мини-пример
export const dynamic = 'force-dynamic';
export default async function Page() {
const res = await fetch('https://api.example.com/data', { cache: 'no-store' });
const data = await res.json();
return <main>{data.length}</main>;
}
Пояснение
Static vs dynamic - это не "быстро vs медленно" в вакууме, а вопрос: можно ли переиспользовать результат между запросами? Если да, static/revalidated page обычно дешевле и быстрее. Если нет, dynamic честно считает на запрос.
Важно проверять triggers. В App Router использование request-specific данных может сделать route dynamic. Поэтому на интервью полезно говорить не только "я выберу SSR", а "какой конкретно сигнал делает страницу dynamic и как я проверю это в build/runtime".
Углубление (2-3 минуты)
| Trigger | Static или dynamic | Почему |
|---|---|---|
| CMS article с revalidate | static/ISR | общий результат |
| Dashboard по cookie/session | dynamic | user-specific response |
headers() для geo/personalization | dynamic | зависит от request |
cache: 'no-store' fetch | dynamic | нельзя переиспользовать |
| Product page с tag revalidation | static/ISR | свежесть через invalidation |
| Search results по query | dynamic или client | зависит от SEO/freshness |
Кейс: каталог товаров публичный, но разработчик читает cookie для баннера и вся страница стала dynamic. Хороший ответ: вынести баннер в client island или отдельный dynamic segment, а каталог оставить static/revalidated.
Практика
- Составьте dynamic/static trigger checklist для пяти routes.
- Для каждого route укажите: request-specific data, cache policy, freshness SLA, TTFB risk.
- Объясните, как проверить, что route не стал dynamic случайно.
Типичные ошибки
- Используют
force-dynamicкак быстрый fix без оценки TTFB/cost. - Читают cookies/headers в высоком layout и делают dynamic весь subtree.
- Кэшируют user-specific данные.
- Не документируют freshness SLA.
Follow-up вопросы
- Какие API делают route request-specific?
- Как сохранить static page, если нужен маленький personalized widget?
- Чем
force-dynamicопасен? - Как dynamic rendering влияет на TTFB и infrastructure cost?
Что повторить
- Static/dynamic rendering triggers in App Router.
- Cookies, headers, search params, uncached fetch.
- Segment-level boundaries and cache strategy.
Связанные модули и карта
9. Как работает generateMetadata и зачем он важен?
Теги: nextjs, react
Сложность: Middle/Senior
Короткий ответ
generateMetadata генерирует metadata для route на сервере: title, description, Open Graph, canonical и другие SEO/social fields. Он может быть static или dynamic и должен использовать ту же freshness/cache стратегию, что и контент страницы.
Что сказать на интервью (30-60 секунд)
Я объясняю generateMetadata как часть контракта страницы с поисковиками и social previews. Если title/OG image берутся из CMS, metadata должна обновляться вместе с контентом и не делать лишние медленные fetch. Важно не генерировать разные metadata и body по разным cache rules: иначе пользователь и crawler увидят несогласованную страницу.
Мини-пример
export async function generateMetadata({ params }) {
const product = await getProduct(params.id);
return {
title: product.name,
description: product.summary,
openGraph: { images: [product.ogImage] },
};
}
Пояснение
Metadata может быть статической через exported metadata object или динамической через generateMetadata. Динамическая metadata полезна для product/article pages, но она участвует в server render и может влиять на latency, если делает медленные fetch.
Сильный ответ включает SEO edge cases: canonical URL, robots/noindex для приватных страниц, Open Graph image freshness, fallback metadata при 404/not-found и отсутствие user-specific metadata на public cached routes.
Углубление (2-3 минуты)
| Сценарий | Metadata strategy | Риск |
|---|---|---|
| Static landing | exported metadata | забыть canonical/OG |
| Article/Product | generateMetadata + same data source | stale title/image |
| Private dashboard | noindex/robots | индексировать приватное |
| Deleted entity | not-found metadata | crawler видит битый preview |
| Multi-locale page | localized metadata + alternates | wrong locale/canonical |
Кейс: CMS обновила title статьи, body обновился через revalidation, а OG title остался старым из-за другого cache key. Хороший ответ: единый data source/cache tag для body и metadata, revalidateTag после CMS publish, проверка social preview.
Практика
- Составьте metadata freshness checklist: data source, cache tag, fallback, canonical, OG image, locale.
- Для article page опишите, что происходит при publish/update/delete.
- Проверьте, какие metadata нельзя делать user-specific на public cached route.
Типичные ошибки
- Генерируют metadata и body из разных источников/cache rules.
- Не задают fallback для missing/deleted entity.
- Делают приватные страницы индексируемыми.
- Забывают проверить Open Graph preview после revalidation.
Follow-up вопросы
- Когда достаточно static
metadata, а когда нуженgenerateMetadata? - Как metadata связана с cache/revalidation?
- Что должно быть в metadata для 404/deleted page?
- Почему metadata не должна зависеть от user session на public page?
Что повторить
- Static
metadatavsgenerateMetadata. - Open Graph, canonical, robots, alternates.
- Metadata caching/freshness and revalidation.
Связанные модули и карта
10. Как в Next.js оптимизировать изображения и шрифты?
Теги: nextjs, react
Сложность: Middle/Senior
Короткий ответ
В Next.js изображения оптимизируют через next/image: корректные размеры, форматы, lazy loading, placeholder, priority только для LCP image. Шрифты оптимизируют через next/font: self-hosting, subsets, display, preload и контроль CLS/FOUT/FOIT.
Что сказать на интервью (30-60 секунд)
Я начинаю с LCP: если hero image является LCP element, ему нужны реальные width/height или fill с sizes, правильный формат, CDN/remote config и иногда priority. Для остальных изображений lazy loading обычно полезнее. Для шрифтов цель - не тянуть лишние subsets/weights и не давать layout shift. Проверяю через Lighthouse/Web Vitals: LCP, CLS, transfer size, unused font weights.
Мини-пример
import Image from 'next/image';
export function Hero() {
return (
<Image
src="/hero.jpg"
alt="Product preview"
width={1200}
height={630}
priority
/>
);
}
Пояснение
next/image помогает с размером, форматом, lazy loading и layout stability, но он не спасает от неправильной модели: если не заданы размеры или sizes, браузер может выбрать не тот ресурс или получить layout shift. Remote images требуют явного разрешения доменов/patterns в конфиге.
next/font снижает зависимость от внешних font requests и помогает self-host fonts, но важно не импортировать все веса и языки. Лишний font weight - это реальный network cost.
Углубление (2-3 минуты)
| Объект | Что проверить | Метрика |
|---|---|---|
| Hero image | priority, размеры, формат, CDN | LCP |
| Responsive image | sizes, width candidates | transfer size |
| Remote image | remotePatterns, cache | errors/cache hit |
| Icons/thumbnails | не preload all | request count |
| Font | subsets, weights, display | CLS/FOUT |
| Critical font | preload только нужного | render delay |
Кейс: LCP 4 секунды из-за hero image 3000px, который грузится как обычный lazy image. Хороший ответ: определить LCP element, использовать next/image, задать размеры/sizes, включить priority только для hero, проверить формат и transfer size на mobile.
Практика
- Проведите image/font performance audit: LCP image, dimensions, sizes, priority, font weights, CLS.
- Назовите 3 случая, где
priorityвреден. - Объясните, как remote image config влияет на production.
Типичные ошибки
- Ставят
priorityна много изображений и забивают critical path. - Используют
fillбез корректногоsizes. - Грузят все font weights/subsets.
- Не задают размеры изображения и получают CLS.
Follow-up вопросы
- Как определить LCP image?
- Когда
priorityнужен, а когда вреден? - Что делает
next/fontс точки зрения network и CLS? - Почему
sizesважен для responsive images?
Что повторить
next/image:width,height,fill,sizes,priority, remote config.next/font: subsets, weights, display, self-hosting.- Web Vitals: LCP, CLS, transfer size.
Связанные модули и карта
11. Как разделять server/client логику без дублирования?
Теги: nextjs, react
Сложность: Middle/Senior
Короткий ответ
В App Router по умолчанию лучше держать данные, секреты, кэш и тяжелую логику на сервере, а на клиент выносить только интерактивные islands: state, event handlers, browser APIs. Дублирование уменьшают через server-only data layer, сериализуемые DTO и маленькие Client Components, а не через перенос всей страницы под 'use client'.
Что сказать на интервью (30-60 секунд)
Я начинаю с boundary audit: какие данные нужны для первого HTML, где есть секреты, где нужна интерактивность и какие props можно сериализовать. Server Component отвечает за data fetching, доступ к токенам и подготовку DTO. Client Component получает уже безопасные данные и владеет только UI-состоянием. Если общий код нужен с двух сторон, я выношу только чистые типы, схемы и formatters, но не server-only DAL.
Мини-пример
// app/products/[id]/page.jsx
import 'server-only';
import { getProductCard } from '@/lib/products-dal';
import { FavoriteButton } from './favorite-button';
export default async function Page({ params }) {
const product = await getProductCard(params.id);
return (
<>
<h1>{product.title}</h1>
<FavoriteButton productId={product.id} initial={product.favorite} />
</>
);
}
// app/products/[id]/favorite-button.jsx
'use client';
export function FavoriteButton({ productId, initial }) {
// event handlers and optimistic UI live here
}
Пояснение
'use client' помечает entry point клиентского дерева, а не просто "файл с React". Если поставить его слишком высоко, в client bundle утянутся лишние компоненты, часть логики потеряет server-only гарантии, а props начнут упираться в сериализацию.
Правильная граница обычно выглядит так: сервер получает данные и режет их до DTO, клиент получает минимальный payload и управляет интерактивностью. Дублирование устраняется не копированием fetch-логики в client и server, а выделением Data Access Layer, чистых shared helpers и явного контракта данных.
Углубление (2-3 минуты)
| Логика | Где держать | Почему |
|---|---|---|
| Database/API token access | Server Component или DAL | секреты не уходят в browser |
| Кнопки, формы, tabs, modals | Client Component | нужны events/state/browser APIs |
| DTO mapping | server-only DAL | фильтрация приватных полей |
| Formatter без секретов | shared helper | можно использовать с двух сторон |
| Optimistic UI | Client Component + Server Action | быстрый UX, сервер остается источником истины |
| Analytics/Web Vitals hook | маленький client island | не превращает layout в client app |
Кейс: карточка пользователя показывает name, role, salary, internalNotes. Ошибка - передать в Client Component весь объект user, потому что UI сейчас выводит только name. Хороший ответ - серверный DTO возвращает только разрешенные поля, а client получает минимальные serialized props.
Практика
- Сделайте server/client import boundary audit для одной страницы: файл, почему server/client, какие данные пересекают границу.
- Отметьте все модули, где должны быть
import 'server-only', и все client entry points с'use client'. - Проверьте, можно ли props Client Component безопасно сериализовать и нет ли приватных полей.
Типичные ошибки
- Ставят
'use client'на страницу или layout только ради одной кнопки. - Импортируют server-only DAL в Client Component.
- Передают в клиент "богатую" модель вместо безопасного DTO.
- Дублируют fetch-логику в Server Component и client hook без единого источника правил.
Follow-up вопросы
- Чем entry point
'use client'отличается от любого вложенного Client Component? - Почему props Client Component должны быть сериализуемыми?
- Как защититься от случайного импорта server-only кода в клиент?
- Как уменьшить client bundle без переписывания всей страницы?
Что повторить
- Server Components, Client Components,
'use client'. server-only, DTO, сериализация props.- Bundle boundary and client island design.
Связанные модули и карта
12. Что такое Server Actions и где их границы?
Теги: nextjs, react
Сложность: Middle/Senior
Короткий ответ
Server Actions - это async server functions для мутаций, которые можно вызывать из forms и client interactions. Их граница: валидировать вход, проверять authentication/authorization внутри каждой action, обновлять данные через server-only DAL, делать revalidatePath/revalidateTag и возвращать клиенту только безопасный результат.
Что сказать на интервью (30-60 секунд)
Я не называю Server Actions "магической заменой backend". Это удобный способ держать mutation рядом с UI, но action остается сетевой серверной точкой входа. Поэтому я отношусь к ней как к публичному POST: проверяю input, session, ownership, CSRF/origin модель проекта, idempotency и то, какие данные возвращаются. Для сложного public API, webhooks, long-running jobs или внешних клиентов чаще выбираю Route Handler или отдельный backend.
Мини-пример
// app/actions/update-display-name.js
'use server';
import 'server-only';
import { revalidatePath } from 'next/cache';
import { requireUser } from '@/lib/auth';
import { updateProfileName } from '@/lib/profile-dal';
export async function updateDisplayName(formData) {
const user = await requireUser();
const name = String(formData.get('name') ?? '').trim();
if (name.length < 2) return { ok: false, error: 'Name is too short' };
await updateProfileName(user.id, name);
revalidatePath('/settings');
return { ok: true };
}
Пояснение
Server Action удобна для form submission, optimistic UI и мутаций, после которых нужно вернуть обновленный UI в одном roundtrip. Но это не место для бизнес-логики без границ. Action должна быть тонкой: распарсить вход, проверить права, вызвать DAL/service, обновить cache и вернуть безопасный DTO.
Если mutation нужен мобильному приложению, внешнему интегратору, webhook provider или требует явного HTTP contract, лучше Route Handler или внешний backend. Если action вызывает тяжелую операцию, стоит подумать про queue/job и статус выполнения.
Углубление (2-3 минуты)
| Сценарий | Server Action подходит? | Что проверить |
|---|---|---|
| Update profile field | да | input, session, ownership, revalidation |
| Delete чужой ресурс | только с authz | проверка владельца обязательна |
| External webhook | нет | нужен Route Handler с signature verification |
| Public mobile API | чаще нет | нужен стабильный HTTP contract |
| Slow export/report | редко | лучше job queue/status route |
| Like/bookmark | да | idempotency и optimistic rollback |
Кейс: кнопка "Удалить проект" вызывает Server Action, которая проверяет только наличие session. Красный флаг - любой участник может удалить проект по id. Хороший ответ: action проверяет ownership/role, валидирует id, пишет audit log, возвращает нейтральную ошибку и инвалидирует только нужные paths/tags.
Практика
- Составьте Server Actions safety checklist: input validation, authn, authz, ownership, idempotency, cache invalidation, returned DTO.
- Для трех мутаций решите: Server Action, Route Handler или external backend.
- Опишите rollback для optimistic UI при ошибке action.
Типичные ошибки
- Проверяют права только в UI и считают action защищенной.
- Возвращают клиенту raw database entity с приватными полями.
- Используют Server Action для публичного API, которому нужен versioning и contract.
- Забывают revalidate и получают stale UI после успешной мутации.
Follow-up вопросы
- Почему Server Action нужно считать публичной точкой входа?
- Чем Server Action отличается от Route Handler?
- Где держать database logic: в action или DAL?
- Что делать после mutation, чтобы UI и cache не разъехались?
Что повторить
'use server', Server Functions, Server Actions.- Authn/authz inside mutations.
revalidatePath,revalidateTag, optimistic UI rollback.
Связанные модули и карта
13. Как проектировать авторизацию в Next.js приложении?
Теги: nextjs, react
Сложность: Middle/Senior
Короткий ответ
Авторизация в Next.js должна быть layered: session хранится безопасно, Proxy/Middleware делает только быстрые optimistic redirects, а реальные secure checks выполняются на сервере рядом с данными и мутациями. UI может скрывать кнопки, но не является защитой.
Что сказать на интервью (30-60 секунд)
Я разделяю authentication и authorization. Authentication отвечает, кто пользователь, authorization - что он может делать. В Next я не делаю тяжелые DB-проверки в Proxy на каждый prefetch; там допустим быстрый redirect по cookie/session. Secure check должен быть в DAL, Server Component, Server Action или Route Handler перед доступом к данным. Отдельно проектирую DTO, чтобы не отправить лишнее в Client Component.
Мини-пример
// lib/projects-dal.js
import 'server-only';
import { requireUser } from './auth';
import { db } from './db';
export async function getProjectForViewer(projectId) {
const user = await requireUser();
const project = await db.project.findFirst({
where: { id: projectId, members: { some: { userId: user.id } } },
select: { id: true, title: true, role: true },
});
if (!project) throw new Error('Forbidden');
return project;
}
Пояснение
Надежная схема не сводится к "поставить middleware". Proxy/Middleware полезен для coarse-grained маршрутов: пустить на /login, редиректнуть с /dashboard, быстро отфильтровать очевидно anonymous user. Но если операция читает приватные данные или меняет состояние, проверка должна быть там, где выполняется доступ к данным.
Нужно явно решить, что делать с forbidden ресурсами: показывать 403, 404-like not found, redirect или disabled UI. Это продуктово-безопасностное решение, и его нельзя заменять случайными if в компонентах.
Углубление (2-3 минуты)
| Уровень | Что делает | Что не должен делать |
|---|---|---|
| UI | hide/disable affordances | быть единственной защитой |
| Proxy/Middleware | быстрый redirect по cookie/session | DB ownership check на каждый prefetch |
| Server Component | secure read перед render | отдавать raw private model в client |
| Server Action | authz перед mutation | доверять hidden input |
| Route Handler | contract-level authz | раздавать внутренние ошибки |
| DAL | единая политика доступа | смешивать UI и permissions |
Кейс: пользователь меняет URL /projects/a на /projects/b. Если проверка была только в navigation/UI, он увидит чужой проект. Хороший ответ: secure DAL проверяет membership/role по каждому projectId, возвращает безопасный DTO или forbidden/not-found состояние.
Практика
- Сделайте auth boundary matrix: route, data read, mutation, UI affordance, где выполняется secure check.
- Для каждого protected resource опишите ownership rule и forbidden behavior.
- Проверьте, какие данные нельзя передавать в Client Component даже после успешной авторизации.
Типичные ошибки
- Считают hidden button достаточной авторизацией.
- Делают тяжелые DB-checks в Proxy и ухудшают TTFB/prefetch.
- Проверяют только authentication, но не ownership/role.
- Возвращают в client DTO с лишними приватными полями.
Follow-up вопросы
- Чем authentication отличается от authorization?
- Почему Proxy/Middleware не должен делать все secure checks?
- Где проверить ownership для Server Action?
- Когда лучше вернуть 403, а когда not found?
Что повторить
- Next.js Authentication guide: optimistic vs secure checks.
- Data Access Layer, DTO,
server-only. - Authn/authz/ownership and forbidden-state design.
Связанные модули и карта
14. Как оптимизировать TTFB и LCP в Next-проекте?
Теги: nextjs, react
Сложность: Middle/Senior
Короткий ответ
TTFB оптимизируют через cache/revalidation, уменьшение request-time работы, streaming и близость данных. LCP оптимизируют через быстрый first HTML, критический image/font path, меньший client JS и отсутствие blocking waterfalls. Сначала измеряют конкретный bottleneck, потом меняют rendering/cache/asset strategy.
Что сказать на интервью (30-60 секунд)
Я разделяю server delay и browser critical path. Если TTFB высокий, смотрю dynamic triggers, slow fetch, DB, region, cache miss, middleware/proxy overhead. Если LCP высокий при нормальном TTFB, смотрю LCP element: hero image, font, render-blocking CSS/JS, hydration cost, client bundle. В Next.js хороший ответ почти всегда связывает метрику с конкретной причиной, а не говорит "добавлю SSR" или "добавлю memo".
Мини-пример
// app/_components/web-vitals.js
'use client';
import { useReportWebVitals } from 'next/web-vitals';
export function WebVitals() {
useReportWebVitals((metric) => {
if (metric.name === 'TTFB' || metric.name === 'LCP') {
navigator.sendBeacon('/analytics/web-vitals', JSON.stringify(metric));
}
});
return null;
}
// app/products/[id]/page.js
export const revalidate = 300;
export default async function Page() {
const product = await getPublicProduct();
return <ProductHero product={product} />;
}
Пояснение
TTFB отвечает за то, как быстро сервер начал отдавать ответ. В Next на него влияют dynamic rendering, uncached fetch, region/runtime, Proxy/Middleware, slow backend и отсутствие revalidation strategy. LCP отвечает за момент, когда главный контент в viewport реально стал видимым; это может быть текст, image или video/poster.
Частая ошибка - лечить LCP только next/image, когда проблема на самом деле в slow server fetch. Или наоборот: кэшировать route, хотя LCP портит большой client bundle и поздняя загрузка hero image.
Углубление (2-3 минуты)
| Симптом | Вероятная причина | Что проверить |
|---|---|---|
| TTFB высокий на всех страницах | slow middleware/backend | trace server timing |
| TTFB высокий только dynamic route | cookies/headers/no-store | dynamic triggers |
| LCP высокий, TTFB нормальный | image/font/client JS | LCP element waterfall |
| LCP image поздно стартует | no priority/sizes/wrong preload | network priority |
| HTML быстро, UI кликается поздно | heavy client bundle | client boundary and hydration |
| Только один регион медленный | data/backend region | deployment and data locality |
Кейс: product page имеет TTFB 900 ms и LCP 3.8 s. Разбор: CMS fetch без cache тянет TTFB, hero image грузится lazy и без sizes. Хороший план: revalidate или tag revalidation для public product, parallelize fetch, задать hero dimensions/sizes/priority, измерить до/после по field data.
Практика
- Составьте TTFB/LCP bottleneck map: server work, cache, network, LCP element, client JS, fonts.
- Для каждой гипотезы укажите один measurement: Web Vitals, Lighthouse, DevTools waterfall, server timing, trace.
- Назовите изменение, которое улучшит TTFB, но может не улучшить LCP.
Типичные ошибки
- Улучшают "performance" без разделения TTFB, LCP, INP и CLS.
- Ставят
force-dynamicи потом удивляются TTFB. - Лечат LCP мемоизацией React-компонентов, когда bottleneck в network waterfall.
- Ставят
priorityна много images и ухудшают critical path.
Follow-up вопросы
- Чем TTFB отличается от LCP?
- Какие Next.js решения могут ухудшить TTFB?
- Как найти LCP element?
- Когда streaming помогает perceived performance, но не решает root cause?
Что повторить
- Web Vitals: TTFB, LCP, INP, CLS.
- Next.js cache/revalidation/dynamic rendering.
next/image,next/font, streaming, lazy loading, client bundle boundaries.
Связанные модули и карта
15. Как организовать API слой в Next: BFF или внешний backend?
Теги: nextjs, react
Сложность: Middle/Senior
Короткий ответ
BFF в Next подходит, когда API обслуживает конкретный web UI: агрегирует backend calls, прячет токены, адаптирует DTO и держит route handlers/server actions рядом с frontend. Внешний backend лучше, когда нужен независимый домен, несколько клиентов, сложные integrations, background jobs, строгие contracts или отдельный scaling/security ownership.
Что сказать на интервью (30-60 секунд)
Я выбираю не по моде, а по ownership и contract. Если endpoint нужен только текущему web app, можно сделать BFF: Route Handler валидирует request, вызывает backend, нормализует response, не раскрывает internal tokens. Если тем же API пользуются mobile, partners, CLI или несколько фронтов, Next BFF не должен становиться случайным core backend. Тогда нужен отдельный service с версионированием, auth, observability и SLA.
Мини-пример
// app/api/search/route.js
import { NextResponse } from 'next/server';
import { requireUser } from '@/lib/auth';
export async function GET(request) {
const user = await requireUser();
const query = request.nextUrl.searchParams.get('q') ?? '';
if (query.length > 80) {
return NextResponse.json({ error: 'Invalid query' }, { status: 400 });
}
const result = await fetchInternalSearch({ query, userId: user.id });
return NextResponse.json({ items: result.items.map(toSearchItemDTO) });
}
Пояснение
BFF полезен, когда frontend нуждается в удобном shape данных, а upstream backend неудобен или небезопасен для прямого вызова из browser. BFF может объединить несколько запросов, спрятать credentials, адаптировать ошибки и отдать ровно тот DTO, который нужен экрану.
Но BFF легко превращается в неуправляемый backend внутри frontend repo. Красный флаг: в route handlers появляется сложная доменная логика, миграции, очереди, cron, несколько внешних клиентов и разные SLA. В таком случае лучше отделить core backend, а Next оставить web-facing слоем.
Углубление (2-3 минуты)
| Критерий | Next BFF | Внешний backend |
|---|---|---|
| Только web UI | подходит | может быть лишним |
| Mobile/partners/CLI | слабее | лучше |
| Aggregation/DTO adapter | подходит | тоже можно, но дальше от UI |
| Long jobs/queues | слабее | лучше |
| Domain ownership | frontend-owned | backend/domain-owned |
| Strict API versioning | ограниченно | лучше |
| Hide tokens from browser | подходит | подходит |
Кейс: checkout page зовет pricing, inventory и promotions. BFF может собрать screen DTO и спрятать internal tokens. Но если checkout rules становятся общим доменом для web, mobile и support tools, правильнее вынести rules в backend service, а BFF оставить тонким adapter.
Практика
- Составьте API/BFF decision matrix для 5 endpoints: consumer, data ownership, auth, latency, versioning, scaling.
- Для одного BFF route опишите validation, rate-limit, timeout, error mapping и DTO filtering.
- Назовите момент, когда route handler пора выносить в отдельный backend.
Типичные ошибки
- Делают Next route handler публичным API без versioning и ownership.
- Проксируют backend без validation/rate limit/error mapping.
- Дублируют доменные правила между BFF и backend.
- Возвращают upstream response как есть и раскрывают лишние поля.
Follow-up вопросы
- Чем BFF отличается от core backend?
- Когда Route Handler лучше Server Action?
- Какие проверки нужны перед proxying to backend?
- Как не превратить Next app в случайный монолит?
Что повторить
- Route Handlers, NextRequest/NextResponse.
- BFF pattern, DTO mapping, backend proxy validation.
- API ownership, versioning, rate limit, error mapping.
Связанные модули и карта
16. Как деплоить Next.js на edge/runtime и что учитывать?
Теги: nextjs, react
Сложность: Middle/Senior
Короткий ответ
Runtime выбирают по capability matrix, а не по обещанию "edge быстрее". nodejs runtime подходит для Node APIs, тяжелых SDK, ISR и сложного server-side кода. edge полезен для легкой логики ближе к пользователю, но имеет ограниченный API surface, не поддерживает все Node-пакеты и требует проверки deployment adapter.
Что сказать на интервью (30-60 секунд)
Я бы начал с вопроса: что именно мы хотим улучшить - latency, cold start, data locality или cost? Edge хорош для lightweight proxy/auth redirect/header logic и географически близкой логики. Но если код использует Node APIs, native dependencies, database driver, ISR или тяжелые SDK, Node runtime безопаснее. Решение проверяю не в абстракции, а через runtime capability matrix и замер TTFB по регионам.
Мини-пример
// app/api/geo-banner/route.js
export const runtime = 'edge';
export function GET(request) {
const country = request.headers.get('x-vercel-ip-country') ?? 'unknown';
return Response.json({ country });
}
// app/reports/[id]/page.js
export const runtime = 'nodejs';
export default async function Page({ params }) {
const report = await renderHeavyReport(params.id);
return <ReportView report={report} />;
}
Пояснение
Edge runtime не является универсальным ускорителем. Он сокращает расстояние до пользователя для части логики, но ограничивает доступные APIs и совместимость пакетов. Node runtime по умолчанию шире по возможностям и лучше подходит для кода, который зависит от Node ecosystem.
Сильный ответ признает deployment reality: разные платформы по-разному поддерживают streaming, regions, cache, max duration и adapters. Поэтому runtime decision должен включать проверку зависимостей, данных, latency и fallback plan.
Углубление (2-3 минуты)
| Сценарий | Runtime | Причина |
|---|---|---|
| Geo redirect/banner | edge | легкая request-time логика |
| JWT cookie prefilter | edge или proxy | быстро, без DB-heavy work |
| PDF/report generation | nodejs | тяжелые зависимости и CPU |
| Prisma/native DB driver | nodejs | совместимость пакетов |
| Public ISR page | nodejs | Edge Runtime не покрывает ISR |
| Latency-sensitive API near user | depends | проверить data locality |
Кейс: команда переносит API route на edge, но внутри используется SDK с Node APIs и прямой database driver. В dev кажется нормальным, в prod route падает или теряет performance. Хороший ответ: сначала dependency/runtime audit, потом маленький edge endpoint только для lightweight задачи.
Практика
- Составьте runtime capability matrix: API route, dependencies, Node APIs, cache/ISR, max duration, data region.
- Для трех endpoints решите
nodejsилиedgeи укажите, что сломается при неправильном выборе. - Опишите измерение: TTFB по регионам, error rate, cold start, dependency compatibility.
Типичные ошибки
- Переносят все на edge без проверки Node APIs и native dependencies.
- Игнорируют data locality: edge близко к пользователю, но далеко от базы.
- Используют edge для тяжелых CPU/background задач.
- Обещают ISR/полный Node runtime там, где платформа это не поддерживает.
Follow-up вопросы
- Почему edge не всегда быстрее?
- Какие APIs и пакеты рискованны для Edge Runtime?
- Как data locality влияет на TTFB?
- Когда runtime decision лучше оставить
nodejs?
Что повторить
- Edge Runtime vs Node.js Runtime.
- Route Segment Config:
runtime,preferredRegion,maxDuration. - Deployment adapter limits and data locality.
Связанные модули и карта
17. Какие риски у глобального state в Next-приложении?
Теги: nextjs, react
Сложность: Middle/Senior
Короткий ответ
Глобальный state в Next опасен, когда в него складывают server data, session, cache и UI flags без границ. Это увеличивает client bundle, провоцирует hydration bugs, лишние rerenders и рассинхрон между server cache и client store. Глобальным должен быть только действительно shared UI state, а server data лучше держать в RSC/cache layer и передавать как DTO.
Что сказать на интервью (30-60 секунд)
Я разделяю state по владельцу: server data, URL state, form draft, session-derived UI, theme, modal state, optimistic state. Не все это должно жить в глобальном client store. Если положить туда данные из Server Components, придется решать freshness, invalidation, serialization и hydration. Поэтому я стараюсь держать providers как можно ниже, а глобальный store использовать только там, где state реально нужен многим client islands.
Мини-пример
// Лучше: server data приходит DTO, client owns only interaction.
export default async function ProductPage({ params }) {
const product = await getProductCard(params.id);
return <ProductDetails product={product} />;
}
'use client';
export function ProductDetails({ product }) {
const [tab, setTab] = useState('overview');
return <ProductTabs product={product} tab={tab} onTabChange={setTab} />;
}
Пояснение
React context и многие stores работают только на клиенте, поэтому provider расширяет client boundary. Если поставить global provider в root layout без необходимости, больше дерева станет client-driven, возрастет JavaScript cost и сложнее станет контролировать hydration.
В Next особенно важно не дублировать server cache в client store. Например, список товаров, полученный на сервере, не должен автоматически становиться вечной копией в Zustand/Redux без invalidation strategy. Иначе после Server Action или revalidation пользователь увидит stale данные.
Углубление (2-3 минуты)
| State | Где хранить | Риск при global store |
|---|---|---|
| Server data | RSC/cache/DAL | stale copy after mutation |
| URL filters | search params | share/back button ломаются |
| Form draft | local component/form lib | лишние rerenders всего app |
| Theme | small provider | provider слишком высоко |
| Modal/toast | scoped client provider | root client boundary разрастается |
| Auth/session claims | server checks + safe DTO | security decisions in UI |
Кейс: catalog page кладет все товары в Redux, а после Server Action обновляет только backend cache. Пользователь видит старую цену до hard reload. Хороший ответ: определить source of truth, после mutation инвалидировать server cache и обновить/сбросить client optimistic state.
Практика
- Сделайте global state placement audit: state name, owner, source of truth, lifetime, invalidation rule.
- Найдите state, который лучше перенести в URL, local state или Server Component.
- Объясните, какие providers можно опустить ниже по дереву.
Типичные ошибки
- Хранят server data в global client store без freshness contract.
- Ставят provider в root layout ради одной страницы.
- Используют global state вместо URL для filters/sort/pagination.
- Принимают authz decisions по client store вместо server check.
Follow-up вопросы
- Какие типы state не должны быть глобальными?
- Почему context provider лучше держать ниже в дереве?
- Как связать optimistic state и server revalidation?
- Когда URL state лучше client store?
Что повторить
- Server/Client Components and context providers.
- URL state vs local state vs global state.
- Source of truth, revalidation, optimistic updates.
Связанные модули и карта
18. Как строить кэш-инвалидацию после мутаций данных?
Теги: nextjs, react, cache
Сложность: Middle/Senior
Короткий ответ
Кэш-инвалидация после мутаций строится от карты зависимостей: какие pages, tags и client views зависят от измененных данных. В Server Action или Route Handler после успешной записи вызывают revalidatePath, revalidateTag или updateTag, а для optimistic UI отдельно описывают rollback и refresh strategy.
Что сказать на интервью (30-60 секунд)
Я не начинаю с "вызову revalidatePath везде". Сначала рисую cache dependency map: entity, list, detail page, metadata, related counters, user-specific views. Если обновилась статья, detail, list, sitemap/metadata и preview могут иметь разные tags. После mutation invalidation должна быть точечной: не чистить весь сайт, но и не оставлять stale блоки. Проверка - timeline publish/update/delete и ожидаемая видимость для следующего пользователя.
Мини-пример
// app/actions/publish-post.js
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
export async function publishPost(postId) {
await db.post.update({ where: { id: postId }, data: { status: 'published' } });
revalidateTag(`post:${postId}`);
revalidateTag('posts:list');
revalidatePath('/blog');
}
Пояснение
revalidatePath удобен, когда нужно обновить конкретный route. revalidateTag удобен, когда один data source используется в нескольких местах. updateTag в современных cache flows может сразу истечь tag после server-side mutation, чтобы следующий visitor увидел свежие данные.
Важно помнить про client state. Если UI сделал optimistic update, server cache invalidation сама по себе не гарантирует, что локальный store уже согласован. Нужен rollback при ошибке и refresh/reload данных после успешной мутации.
Углубление (2-3 минуты)
| Mutation | Что инвалидировать | Риск |
|---|---|---|
| Publish post | detail tag, list tag, /blog | stale listing |
| Update product price | product tag, category list, search card | разные цены |
| Delete entity | detail/not-found, list, metadata | битый preview |
| Change user profile | private profile cache, client session DTO | stale user menu |
| Toggle favorite | user-specific list, optimistic UI | неверный counter |
| Bulk import | broad tag group, not whole site | cache storm |
Кейс: CMS обновляет title статьи. Body обновился, но metadata и list card остались старыми. Хороший ответ: единая data/cache tag strategy для body, metadata и list preview; тест publish/update/delete timeline.
Практика
- Составьте cache invalidation timeline: before mutation, write, revalidate, next request, rollback.
- Для трех entities укажите tags, paths и связанные metadata/list views.
- Назовите случай, где
revalidatePathслишком грубый, аrevalidateTagточнее.
Типичные ошибки
- Чистят весь cache после маленькой мутации.
- Инвалидируют detail page, но забывают list/metadata/search preview.
- Смешивают public cache и user-specific данные.
- Не тестируют delete/unpublish, где stale cache особенно заметен.
Follow-up вопросы
- Когда выбрать
revalidatePath, а когдаrevalidateTag? - Что должно произойти после optimistic mutation error?
- Почему metadata тоже часть cache strategy?
- Как избежать cache stampede после bulk update?
Что повторить
- Next.js caching, tags, paths,
updateTag. - Server Actions and cache revalidation.
- Optimistic UI, rollback, stale data diagnosis.
Связанные модули и карта
19. Как строить мультиязычность и роутинг i18n в Next?
Теги: nextjs, react
Сложность: Middle/Senior
Короткий ответ
i18n в App Router обычно строят через locale segment вроде app/[lang]/..., server-side dictionaries, localized metadata, canonical/alternate links и понятную стратегию fallback. Важно не смешивать перевод UI, форматирование дат/валют, routing, SEO и content freshness в один случайный helper.
Что сказать на интервью (30-60 секунд)
Я бы описал i18n как routing + content + formatting + SEO. Route должен явно содержать locale или иметь понятную negotiation strategy. Словари лучше грузить на сервере по lang, чтобы не тащить все языки в client bundle. Для SEO нужны localized title/description, canonical, alternates и корректное поведение для missing translation. Проверяю не только happy path, но и смену языка на deep link.
Мини-пример
// app/[lang]/layout.js
export async function generateStaticParams() {
return [{ lang: 'ru' }, { lang: 'en' }];
}
export default async function Layout({ children, params }) {
const { lang } = await params;
return <html lang={lang}>{children}</html>;
}
Пояснение
Локализация - не просто JSON со строками. Если user lands на /en/products/42, нужно решить: есть ли английский контент, как формируются metadata, что показывать при missing translation, сохраняется ли path при переключении языка, как форматируются currency/date/timezone.
В App Router распространенная модель - вынести locale в dynamic segment [lang], генерировать static params для известных locales и грузить dictionary на сервере. Client Components получают уже нужные строки или минимальный translation scope.
Углубление (2-3 минуты)
| Область | Решение | Edge-case |
|---|---|---|
| Route | /[lang]/... | неизвестный locale |
| Dictionary | server-side load by lang | не грузить все языки |
| Metadata | localized title/description | stale OG после перевода |
| Alternates | canonical/hreflang | wrong canonical |
| Formatting | Intl locale/currency/timezone | hydration mismatch |
| Missing content | fallback/not-found | mixed-language page |
Кейс: сайт переводит UI, но product slug и metadata остаются русскими на /en. Для пользователя это выглядит непрофессионально, для SEO - дубли и wrong canonical. Хороший ответ: i18n checklist покрывает route, content, metadata, alternates, formatting и fallback.
Практика
- Составьте i18n routing checklist для
/[lang]/blog/[slug]: params, dictionary, metadata, alternates, fallback. - Опишите, что произойдет при missing translation и unknown locale.
- Проверьте, не попадают ли все dictionaries в client bundle.
Типичные ошибки
- Грузят все языки на клиент ради одной страницы.
- Переводят UI, но забывают metadata/canonical/alternates.
- Не фиксируют locale/timezone в форматировании и получают hydration mismatch.
- Делают fallback, который смешивает языки на одной странице.
Follow-up вопросы
- Почему locale в URL часто лучше неявной browser negotiation?
- Что должно быть в localized metadata?
- Как missing translation влияет на UX и SEO?
- Почему date/currency formatting связан с hydration?
Что повторить
- App Router i18n with
[lang]. - Dictionaries, metadata, alternates, canonical.
Intl, locale/timezone, hydration-safe formatting.
Связанные модули и карта
20. Какие anti-patterns чаще всего встречаются в Next-коде?
Теги: nextjs, react
Сложность: Middle/Senior
Короткий ответ
Частые Next.js anti-patterns: ставить 'use client' слишком высоко, использовать force-dynamic как универсальный fix, смешивать public и user-specific cache, тащить server-only код в клиент, делать Server Actions без authz, забывать metadata/revalidation и держать global state без source of truth.
Что сказать на интервью (30-60 секунд)
Я отвечаю не списком "плохих практик", а review-моделью. Сначала смотрю boundaries: что server, что client, где секреты. Потом cache: какие routes static/dynamic, какие tags/paths обновляются после mutations. Потом UX/performance: LCP image, fonts, streaming, loading/error boundaries. И отдельно security: authz в DAL/actions/route handlers, DTO filtering, no raw internal errors.
Мини-пример
// Anti-pattern: one small button turns the whole page into a client tree.
'use client';
export default function ProductPage({ product }) {
return (
<>
<ProductContent product={product} />
<button onClick={() => track(product.id)}>Track</button>
</>
);
}
Правка: оставить page Server Component, а кнопку вынести в маленький Client Component.
Пояснение
Anti-pattern в Next обычно проявляется не как один синтаксический баг, а как архитектурный drift: страница постепенно становится client app, cache rules становятся неявными, actions обходят authz, а metadata и lists устаревают после mutations.
Хороший кандидат на интервью умеет назвать симптом, root cause, исправление и проверку. Например: "TTFB вырос, потому что layout читает cookies и делает dynamic весь subtree; исправление - вынести personalization в client island или ниже по сегменту, а public content оставить cacheable".
Углубление (2-3 минуты)
| Anti-pattern | Симптом | Исправление |
|---|---|---|
'use client' на page/layout | большой JS bundle | client island |
force-dynamic везде | высокий TTFB | cache/revalidation strategy |
| Cookies в root layout | весь subtree dynamic | lower boundary |
| Server Action без authz | privilege bug | check ownership in action/DAL |
| Raw backend response | leak fields/errors | DTO + error mapping |
next/image без sizes | лишний transfer/LCP | responsive sizes |
| Global store for server data | stale UI | source of truth + invalidation |
Кейс: после релиза dashboard стал медленным, а build показывает dynamic почти все routes. Review находит cookies() в root layout ради баннера. Хороший план: вынести баннер ниже или в client island, вернуть public pages к static/revalidated rendering, добавить regression checklist.
Практика
- Проведите Next anti-pattern review table для 10 файлов: boundary, cache, auth, data leak, performance, metadata.
- Для каждого найденного риска запишите symptom, root cause, fix и validation.
- Выберите один anti-pattern и объясните его за 60 секунд как на интервью.
Типичные ошибки
- Называют anti-pattern, но не объясняют production symptom.
- Исправляют performance, не проверив cache/dynamic triggers.
- Считают Server Actions безопасными без server-side authz.
- Переносят все в client store вместо проектирования server/client boundaries.
Follow-up вопросы
- Как найти, что route случайно стал dynamic?
- Какой anti-pattern чаще всего портит LCP?
- Почему "просто поставить use client" дорого?
- Как проверить, что cache invalidation не сломала private data?
Что повторить
- Server/client boundaries, cache strategy, dynamic triggers.
- Server Actions security, DTO filtering, route handlers.
- Web Vitals, image/font optimization, loading/error boundaries.
Связанные модули и карта
Куда дальше
- Вернитесь в модуль: Next.js.
- Сверьтесь с картой темы: Next.js.
- Продолжайте по маршруту: Middle трек.
- Закрепите один ответ на практике в Песочнице.