Как работает Client-Side Rendering (CSR) в Next.js

CSR (Client-Side Rendering) это подход, при котором рендеринг страницы происходит полностью в браузере. Сервер отдает минимальный HTML с JavaScript-бандлом, а React рендерит контент на стороне клиента.

В обычном React-приложении (без Next.js) весь рендеринг клиентский. В Next.js CSR тоже доступен, но используется точечно, для частей интерфейса, которым нужна интерактивность или данные, доступные только в браузере.

Когда CSR используется в Next.js

В Next.js не бывает страниц, которые полностью работают через CSR (как в обычном React SPA). Серверный компонент всегда рендерит начальный HTML. Но внутри страницы могут быть клиентские компоненты, которые загружают данные и рендерят контент на клиенте.

Типичные сценарии:

  • Загрузка данных, зависящих от действий пользователя (фильтры, поиск)
  • Компоненты, использующие browser API (localStorage, geolocation)
  • Интерактивные виджеты (чат, уведомления, таймеры)

Как это реализуется

Загрузка данных в клиентском компоненте

'use client'

import { useState, useEffect } from 'react'

interface Problem {
  id: string
  name: string
  difficulty: number
}

export default function ProblemSearch() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState<Problem[]>([])
  const [loading, setLoading] = useState(false)

  useEffect(() => {
    if (!query) {
      setResults([])
      return
    }

    setLoading(true)
    const controller = new AbortController()

    fetch(`/api/problems/search?q=${query}`, {
      signal: controller.signal
    })
      .then(res => res.json())
      .then(data => setResults(data))
      .finally(() => setLoading(false))

    return () => controller.abort()
  }, [query])

  return (
    <div>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Поиск задач на Hack Frontend"
      />
      {loading && <p>Загрузка...</p>}
      <ul>
        {results.map(p => (
          <li key={p.id}>{p.name}</li>
        ))}
      </ul>
    </div>
  )
}

Динамический импорт с отключением SSR

Иногда компонент вообще не может рендериться на сервере (использует window, document и т.д.). В таких случаях можно отключить SSR для конкретного компонента:

import dynamic from 'next/dynamic'

const CodeEditor = dynamic(
  () => import('@/components/ide/code-editor'),
  { ssr: false, loading: () => <p>Загрузка редактора...</p> }
)

export default function ProblemPage() {
  return (
    <div>
      <h1>JavaScript задача</h1>
      <CodeEditor />
    </div>
  )
}

ssr: false гарантирует, что компонент будет загружен и отрендерен только в браузере.

Паттерн: серверная обертка + клиентская интерактивность

Правильный подход в Next.js: загружать начальные данные на сервере, а интерактивность реализовывать на клиенте:

// app/problems/page.tsx (серверный компонент)
import { db } from '@/lib/db'
import { ProblemFilter } from './problem-filter'

export default async function ProblemsPage() {
  const problems = await db.problem.findMany({
    select: { id: true, name: true, difficulty: true }
  })

  return (
    <div>
      <h1>Задачи</h1>
      <ProblemFilter initialProblems={problems} />
    </div>
  )
}
// app/problems/problem-filter.tsx (клиентский компонент)
'use client'

import { useState } from 'react'

interface Problem {
  id: string
  name: string
  difficulty: number
}

export function ProblemFilter({
  initialProblems
}: {
  initialProblems: Problem[]
}) {
  const [difficulty, setDifficulty] = useState<number | null>(null)

  const filtered = difficulty
    ? initialProblems.filter(p => p.difficulty === difficulty)
    : initialProblems

  return (
    <div>
      <div>
        <button onClick={() => setDifficulty(null)}>Все</button>
        <button onClick={() => setDifficulty(1)}>Легкие</button>
        <button onClick={() => setDifficulty(2)}>Средние</button>
        <button onClick={() => setDifficulty(3)}>Сложные</button>
      </div>
      <ul>
        {filtered.map(p => (
          <li key={p.id}>{p.name}</li>
        ))}
      </ul>
    </div>
  )
}

Ключевой принцип:

В Next.js CSR применяется не для целых страниц, а для отдельных компонентов. Начальный HTML всегда приходит с сервера, что дает хороший SEO и быструю первую загрузку. Интерактивность добавляется поверх.

Недостатки чистого CSR

  • SEO. Контент, который рендерится только на клиенте, не виден поисковым ботам до выполнения JavaScript.
  • Первая загрузка. Пользователь видит пустую страницу или скелетон, пока JavaScript загружается и выполняется.
  • Производительность на слабых устройствах. Весь рендеринг ложится на процессор устройства пользователя.

Именно поэтому Next.js по умолчанию использует серверные компоненты и CSR применяется только там, где это необходимо.

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

  • nextjs.org/docs — клиентские компоненты
  • Lazy Loading — ленивая загрузка компонентов

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