Ключевые особенности Next.js

На собеседовании этот вопрос проверяет, насколько глубоко ты знаешь фреймворк, а не просто слышал о нем. Недостаточно перечислить "SSR, роутинг, оптимизация". Нужно понимать, как каждая фича работает и зачем она нужна.

Разберем основные особенности Next.js по порядку.

Гибридный рендеринг

Главная сила Next.js в том, что ты сам выбираешь стратегию рендеринга для каждой страницы. В одном приложении могут сосуществовать страницы с разными подходами:

  • SSG статика, генерируется при сборке. Для страниц, которые меняются редко.
  • SSR рендерится на сервере при каждом запросе. Для динамического контента.
  • ISR статика с фоновым обновлением. Компромисс между SSG и SSR.
  • CSR рендеринг на клиенте, как в обычном React.
// Страница "О компании" на Hack Frontend — статика (SSG по умолчанию)
export default async function AboutPage() {
  const info = await fetch('https://api.hackfrontend.com/about')
  const data = await info.json()

  return <About data={data} />
}
// Лента задач — обновляется каждые 5 минут (ISR)
export default async function ProblemsPage() {
  const res = await fetch('https://api.hackfrontend.com/problems', {
    next: { revalidate: 300 }
  })
  const problems = await res.json()

  return <ProblemList problems={problems} />
}

На собеседовании:

Часто спрашивают: "Когда бы ты выбрал SSR вместо SSG?". Хороший ответ: SSR для страниц с персонализированным контентом (профиль, дашборд), SSG для контента, одинакового для всех (блог, документация).

App Router и файловая маршрутизация

Начиная с версии 13, Next.js использует App Router. Структура папок в директории app напрямую определяет маршруты приложения:

Каждая папка это сегмент URL. Специальные файлы имеют конкретные роли:

ФайлНазначение
page.tsxUI страницы
layout.tsxОбщая обертка для страницы и вложенных роутов
loading.tsxUI загрузки (работает с Suspense)
error.tsxUI ошибки (работает с Error Boundary)
not-found.tsxUI для 404
// app/docs/[slug]/page.tsx
// Динамический роут: /docs/javascript, /docs/react и т.д.
export default async function DocPage({
  params
}: {
  params: { slug: string }
}) {
  const doc = await getDocument(params.slug)

  return <article>{doc.content}</article>
}

Server Components

В App Router компоненты по умолчанию серверные. Это значит:

  • Они выполняются только на сервере
  • Их код не попадает в клиентский бандл
  • Они могут напрямую обращаться к базе данных, файловой системе, env-переменным
// Серверный компонент — код не отправляется в браузер
import { db } from '@/lib/db'

export default async function UserStats() {
  const totalUsers = await db.user.count()
  const totalProblems = await db.problem.count()

  return (
    <div>
      <p>Пользователей на Hack Frontend: {totalUsers}</p>
      <p>Задач: {totalProblems}</p>
    </div>
  )
}

Когда нужна интерактивность, помечаешь компонент как клиентский:

'use client'

import { useState } from 'react'

export default function ThemeToggle() {
  const [dark, setDark] = useState(false)

  return (
    <button onClick={() => setDark(!dark)}>
      {dark ? 'Светлая тема' : 'Темная тема'}
    </button>
  )
}

Важно:

Серверные компоненты не могут использовать useState, useEffect, onClick и другие клиентские API. Если нужна интерактивность, выделяй ее в отдельный клиентский компонент.

Server Actions

Server Actions позволяют вызывать серверные функции напрямую из компонентов, без создания API-эндпоинтов:

// actions/subscribe.ts
'use server'

import { db } from '@/lib/db'

export async function subscribe(email: string) {
  await db.subscriber.create({
    data: { email }
  })
}
// Клиентский компонент вызывает серверную функцию
'use client'

import { subscribe } from '@/actions/subscribe'

export default function SubscribeForm() {
  return (
    <form action={async (formData) => {
      const email = formData.get('email') as string
      await subscribe(email)
    }}>
      <input name="email" type="email" placeholder="Email" />
      <button type="submit">Подписаться на Hack Frontend</button>
    </form>
  )
}

Вложенные макеты (Nested Layouts)

Макеты в Next.js сохраняют состояние между переходами. Если у тебя есть сайдбар или навигация, они не перерендериваются при переходе между страницами:

// app/docs/layout.tsx
// Этот макет оборачивает все страницы /docs/*
export default function DocsLayout({
  children
}: {
  children: React.ReactNode
}) {
  return (
    <div className="flex">
      <Sidebar />
      <main className="flex-1">{children}</main>
    </div>
  )
}

При переходе с /docs/javascript на /docs/react макет с сайдбаром останется на месте, перерисуется только содержимое children.

Встроенная оптимизация

Компонент Image

Автоматически оптимизирует изображения: ресайзит, конвертирует в WebP/AVIF, добавляет lazy loading:

import Image from 'next/image'

export default function Avatar() {
  return (
    <Image
      src="/avatar.png"
      width={64}
      height={64}
      alt="Аватар пользователя"
    />
  )
}

Предзагружает страницу в фоне, когда ссылка попадает в область видимости. Переход становится практически мгновенным:

import Link from 'next/link'

export default function Nav() {
  return (
    <nav>
      <Link href="/problems">Задачи</Link>
      <Link href="/docs">Документация</Link>
    </nav>
  )
}

Оптимизация шрифтов

next/font загружает шрифты без сдвига макета (layout shift) и без запросов к Google Fonts на клиенте:

import { Inter } from 'next/font/google'

const inter = Inter({ subsets: ['latin', 'cyrillic'] })

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html className={inter.className}>
      <body>{children}</body>
    </html>
  )
}

Middleware

Middleware выполняется до обработки запроса и позволяет перенаправлять, переписывать URL или модифицировать заголовки:

// middleware.ts (в корне проекта)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const locale = request.cookies.get('locale')?.value || 'ru'

  if (!request.nextUrl.pathname.startsWith(`/${locale}`)) {
    return NextResponse.redirect(
      new URL(`/${locale}${request.nextUrl.pathname}`, request.url)
    )
  }
}

export const config = {
  matcher: ['/((?!api|_next|favicon).*)']
}

Встроенная поддержка метаданных

Next.js предоставляет удобный API для управления SEO-метаданными:

import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'Задачи по JavaScript — Hack Frontend',
  description: 'Решай задачи из реальных собеседований по фронтенд-разработке',
  openGraph: {
    title: 'Задачи по JavaScript',
    description: 'Решай задачи из реальных собеседований',
    type: 'website'
  }
}

export default function ProblemsPage() {
  return <ProblemsList />
}

Полная картина

Как отвечать на собеседовании:

Не перечисляй все фичи списком. Выбери 3-4 ключевых и объясни, как они связаны между собой. Например: "Next.js использует App Router с файловой маршрутизацией, где компоненты по умолчанию серверные, что уменьшает клиентский бандл и дает доступ к данным без API-слоя через Server Actions".

Полезные ресурсы

Связанные темы