Как работает 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 — ленивая загрузка компонентов