Next.js решает классическую проблему React-приложений: медленную первоначальную загрузку и плохую индексацию поисковиками. Вместо того чтобы заставлять браузер пользователя выполнять всю работу по рендерингу, Next.js формирует готовую HTML-страницу на сервере. В результате пользователь видит контент практически мгновенно.
Сравнивая Next.js с другими решениями для SSR, нельзя не отметить его уникальный подход. Gatsby, например, генерирует статические страницы на этапе сборки - отличный выбор для блогов и документации. Nuxt.js, вдохновлённый Next.js, предлагает похожую функциональность для Vue.js. А вот Angular Universal... честно говоря, его настройка может превратиться в настоящий квест.
Next.js берёт лучшее из разных миров. Он поддерживает как серверный рендеринг, так и статическую генерацию страниц. При этом разработчику не приходится ломать голову над конфигурацией - фреймворк следует принципу "convention over configuration". Файл в папке pages автоматически становится маршрутом, а код разделяется на серверный и клиентский без дополнительных усилий. Я помню свой первый проект на Next.js - интернет-магазин с тысячами товаров. Клиентский рендеринг приводил к мучительно долгой загрузке первой страницы. После перехода на Next.js время до первого взаимодействия сократилось втрое, а поисковые роботы наконец-то смогли корректно индексировать каталог.
Однако Next.js - не волшебная таблетка. Серверный рендеринг требует дополнительных серверных ресурсов. Кэширование и оптимизация становятся критически важными для высоконагруженных приложений. К тому же, некоторые библиотеки React могут конфликтовать с SSR, что потребует дополнительных усилий по адаптации кода. Next.js активно развивается - каждый релиз приносит новые возможности и улучшения производительности. Появление IncrementalStaticRegeneration изменило правила игры для динамического контента, а встроенная поддержка TypeScript сделала разработку более надёжной.
Основы Next.js
Next.js строится на мощном фундаменте архитектурных принципов, которые определяют его уникальный подход к веб-разработке. В сердце фреймворка лежит гибридная система рендеринга, сочетающая преимущества серверной и клиентской обработки. Когда пользователь запрашивает страницу, Next.js запускает многоступенчатый цикл. Сначала происходит серверный рендеринг - компоненты React выполняются на сервере, формируя базовый HTML. Этот HTML мгновенно отправляется клиенту, обеспечивая быструю первую отрисовку. Параллельно с этим браузер получает минимально необходимый JavaScript-код для "оживления" страницы - процесса, известного как гидратация.
Файловая система Next.js служит основой для маршрутизации. Каждый файл в директории pages/ автоматически становится доступным маршрутом. Файл pages/about.js превращается в путь /about, а pages/posts/[id].js обрабатывает динамические маршруты вида /posts/1, /posts/2 и так далее. Это избавляет от необходимости писать сложные конфигурации роутинга. Структура типичного Next.js проекта выглядит примерно так:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
| my-app/
├── pages/
│ ├── index.js
│ ├── about.js
│ └── posts/
│ └── [id].js
├── public/
│ └── images/
├── components/
│ └── Header.js
└── styles/
└── globals.css |
|
В Next.js каждая страница проходит через свой жизненный цикл. Возьмём, к примеру, страницу товара в интернет-магазине. При запросе сначала выполняется функция getServerSideProps:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| export async function getServerSideProps(context) {
const { id } = context.params;
const response = await fetch(`/api/products/${id}`);
const product = await response.json();
return {
props: { product }
};
}
export default function ProductPage({ product }) {
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<button onClick={() => addToCart(product)}>Купить</button>
</div>
);
} |
|
Этот код демонстрирует ключевое преимущество Next.js - изоляцию серверной логики. Функция getServerSideProps выполняется только на сервере, её код никогда не попадает в клиентский бандл. Это позволяет безопасно работать с секретными ключами API и выполнять тяжёлые вычисления.
Next.js предлагает несколько способов получения данных: getStaticProps для статического контента, getServerSideProps для динамических данных и getInitialProps для универсального получения данных. Выбор метода зависит от природы контента и требований к производительности. В отличие от классического React, где весь рендеринг происходит на клиенте, Next.js позволяет точно контролировать, где и когда выполняется код. Компоненты могут содержать серверную логику, клиентские обработчики событий и даже условный рендеринг в зависимости от среды выполнения:
JavaScript | 1
2
3
4
| const DynamicComponent = dynamic(() => import('../components/Heavy'), {
ssr: false,
loading: () => <p>Загрузка...</p>
}); |
|
Этот пример показывает, как Next.js позволяет отключить серверный рендеринг для тяжёлых компонентов, которые должны выполняться только на клиенте. Такая гибкость - одна из сильных сторон фреймворка.
Одна из самых впечатляющих особенностей Next.js - встроенная поддержка API-маршрутов. Создав файл в директории pages/api, мы получаем полноценный серверный эндпоинт. Это позволяет строить полностью автономные приложения без необходимости отдельного бэкенда:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // pages/api/products.js
export default async function handler(req, res) {
if (req.method === 'POST') {
try {
const { name, price } = req.body;
const product = await db.products.create({ name, price });
res.status(201).json(product);
} catch (error) {
res.status(500).json({ error: 'Не удалось создать продукт' });
}
} else {
res.status(405).json({ error: 'Метод не поддерживается' });
}
} |
|
API-маршруты поддерживают все HTTP-методы и автоматически парсят тело запроса. При этом они изолированы от клиентского кода, что позволяет безопасно работать с базами данных и внешними API.
Next.js предоставляет мощные средства оптимизации производительности. Автоматический code splitting разбивает приложение на небольшие чанки, которые загружаются по мере необходимости. Префетчинг страниц происходит автоматически при появлении ссылок в области видимости:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| import Link from 'next/link';
function NavBar() {
return (
<nav>
<Link href="/products">
<a>Продукты</a>
</Link>
<Link href="/blog" prefetch={false}>
<a>Блог</a>
</Link>
</nav>
);
} |
|
Встроенная поддержка CSS-модулей и Sass облегчает стилизацию компонентов. Каждый CSS-модуль автоматически скопирован, что предотвращает конфликты стилей:
JavaScript | 1
2
3
4
5
6
7
8
9
| import styles from './Button.module.css';
export default function Button({ children }) {
return (
<button className={styles.primary}>
{children}
</button>
);
} |
|
Next.js предлагает продвинутую систему управления заголовками страниц и метатегами через компонент Head. Это критически важно для SEO и социальных сетей:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| import Head from 'next/head';
function ProductPage({ product }) {
return (
<>
<Head>
<title>{product.name} - Наш магазин</title>
<meta property="og:title" content={product.name} />
<meta name="description" content={product.description} />
</Head>
{/* Контент страницы */}
</>
);
} |
|
Компонент Image из next/image автоматически оптимизирует изображения, конвертируя их в современные форматы и применяя ленивую загрузку:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| import Image from 'next/image';
function ProductCard({ product }) {
return (
<div>
<Image
src={product.imageUrl}
alt={product.name}
width={300}
height={200}
placeholder="blur"
blurDataURL={product.thumbnailUrl}
/>
</div>
);
} |
|
Фреймворк также предоставляет промежуточное ПО (middleware) для обработки запросов перед рендерингом страниц. Это позволяет реализовать аутентификацию, редиректы и другую логику на уровне маршрутизации:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
| // middleware.js
export function middleware(req) {
const { pathname } = req.nextUrl;
if (pathname.startsWith('/admin')) {
const token = req.cookies.get('auth-token');
if (!token) {
return NextResponse.redirect(new URL('/login', req.url));
}
}
return NextResponse.next();
} |
|
Next.js автоматически оптимизирует шрифты, загружая их с приоритетом и предотвращая смещение макета при загрузке. Это часть более широкой стратегии по улучшению Core Web Vitals:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| import { Inter } from '@next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
});
export default function Layout({ children }) {
return (
<div className={inter.className}>
{children}
</div>
);
} |
|
Задержка рендеринга React (useState, useEffect) Здравствуйте!
Очень нужна консультация по вопросу асинхронности useState
У меня в проекте есть фильтр, все готово, он работает, но с задержкой... Objects are not valid as a React child (found: TypeError: response[0].includes is not a function). REACT Всем привет. Создаю страничку на React. Смысл работы примерно таков : пользователь заходит, выбирает из первой таблицы типы аккаунтов-> нажимает... Ошибка при создании проекта React с помощью пакета create-react-app Привет. Пытаюсь изучать JavaScript. Дошёл до библиотеки React. Пытаюсь создать первое приложение. Ввожу в терминале команду npx create-react-app... Посоветуйте практический курс на React redux/ react Всем привет. Столкнулся с тем, что мне не хватает практики. Подскажите какой практический курс по реакту.
типо...
Практическое применение
Начнём с настройки рабочего окружения. Создать новый проект можно одной командой:
Bash | 1
| npx create-next-app@latest my-shop --typescript |
|
Я специально выбрал TypeScript - он значительно упрощает разработку больших приложений. После установки зависимостей мы получаем готовый к работе проект. Запускаем его командой npm run dev, и... вуаля! Приложение работает на localhost:3000.
Давайте создадим простой, но показательный пример - страницу блога с комментариями. Такой кейс отлично демонстрирует работу с данными и пользовательским вводом:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
| // pages/posts/[slug].tsx
interface Post {
title: string;
content: string;
comments: Comment[];
}
interface Comment {
id: number;
text: string;
author: string;
}
export async function getServerSideProps({ params }) {
const post = await fetchPost(params.slug);
return { props: { post } };
}
export default function PostPage({ post }: { post: Post }) {
const [comments, setComments] = useState(post.comments);
const [newComment, setNewComment] = useState('');
async function handleSubmit(e: FormEvent) {
e.preventDefault();
const comment = await submitComment(post.id, newComment);
setComments([...comments, comment]);
setNewComment('');
}
return (
<article className="max-w-2xl mx-auto">
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
<div className="mt-8">
<h2>Комментарии ({comments.length})</h2>
{comments.map(comment => (
<div key={comment.id} className="border-b py-3">
<p>{comment.text}</p>
<small>Автор: {comment.author}</small>
</div>
))}
</div>
<form onSubmit={handleSubmit} className="mt-4">
<textarea
value={newComment}
onChange={e => setNewComment(e.target.value)}
className="w-full p-2 border"
placeholder="Ваш комментарий..."
/>
<button type="submit" className="mt-2 px-4 py-2 bg-blue-500 text-white">
Отправить
</button>
</form>
</article>
);
} |
|
А теперь рассмотрим реализацию аутентификации. Next.js прекрасно работает с JWT-токенами. Создадим middleware для защиты приватных маршрутов:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { verify } from 'jsonwebtoken';
export async function middleware(req: NextRequest) {
const token = req.cookies.get('token');
if (!token && req.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', req.url));
}
try {
const decoded = verify(token, process.env.JWT_SECRET!);
const response = NextResponse.next();
response.headers.set('x-user-id', decoded.sub as string);
return response;
} catch {
return NextResponse.redirect(new URL('/login', req.url));
}
} |
|
Для работы с API-маршрутами Next.js предоставляет удобный интерфейс. Создадим эндпоинт для аутентификации:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| // pages/api/auth/login.ts
import { sign } from 'jsonwebtoken';
import { compare } from 'bcrypt';
import { prisma } from '@/lib/prisma';
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).end();
}
const { email, password } = req.body;
try {
const user = await prisma.user.findUnique({ where: { email } });
if (!user) {
return res.status(401).json({ message: 'Неверные учётные данные' });
}
const isValid = await compare(password, user.password);
if (!isValid) {
return res.status(401).json({ message: 'Неверные учётные данные' });
}
const token = sign(
{ sub: user.id, email: user.email },
process.env.JWT_SECRET!,
{ expiresIn: '7d' }
);
res.setHeader(
'Set-Cookie',
[INLINE]token=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=604800[/INLINE]
);
res.json({ user: { id: user.id, email: user.email } });
} catch (error) {
res.status(500).json({ message: 'Внутренняя ошибка сервера' });
}
} |
|
На клиентской стороне реализуем форму входа с обработкой ошибок и редиректом:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
| // pages/login.tsx
import { useState } from 'react';
import { useRouter } from 'next/router';
export default function LoginPage() {
const [error, setError] = useState('');
const router = useRouter();
async function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
const form = e.currentTarget;
const formData = new FormData(form);
try {
const res = await fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify({
email: formData.get('email'),
password: formData.get('password'),
}),
headers: { 'Content-Type': 'application/json' },
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.message);
}
router.push('/dashboard');
} catch (err) {
setError(err.message);
}
}
return (
<div className="min-h-screen flex items-center justify-center">
<form onSubmit={handleSubmit} className="w-full max-w-md">
{error && (
<div className="bg-red-100 border-red-400 text-red-700 px-4 py-3 rounded">
{error}
</div>
)}
<input
name="email"
type="email"
required
className="mt-4 w-full px-3 py-2 border rounded"
placeholder="Email"
/>
<input
name="password"
type="password"
required
className="mt-2 w-full px-3 py-2 border rounded"
placeholder="Пароль"
/>
<button
type="submit"
className="mt-4 w-full bg-blue-500 text-white py-2 rounded"
>
Войти
</button>
</form>
</div>
);
} |
|
Для работы с данными в Next.js часто используют внешние API. Создадим сервис для взаимодействия с бэкендом, который инкапсулирует логику запросов:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| // lib/api.ts
class ApiService {
private static instance: ApiService;
private token: string | null = null;
private constructor() {}
static getInstance(): ApiService {
if (!ApiService.instance) {
ApiService.instance = new ApiService();
}
return ApiService.instance;
}
setToken(token: string) {
this.token = token;
}
private async fetch<T>(endpoint: string, options?: RequestInit): Promise<T> {
const headers = {
'Content-Type': 'application/json',
...(this.token && { Authorization: [INLINE]Bearer ${this.token}[/INLINE] }),
...options?.headers,
};
const response = await fetch(`/api/${endpoint}`, {
...options,
headers,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
async getPosts(): Promise<Post[]> {
return this.fetch<Post[]>('posts');
}
async createPost(data: CreatePostData): Promise<Post> {
return this.fetch<Post>('posts', {
method: 'POST',
body: JSON.stringify(data),
});
}
} |
|
Этот паттерн синглтон обеспечивает единую точку доступа к API и управление состоянием аутентификации. Теперь создадим хук для работы с этим сервисом:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // hooks/useApi.ts
import { useEffect } from 'react';
import { useRouter } from 'next/router';
export function useApi() {
const router = useRouter();
const api = ApiService.getInstance();
useEffect(() => {
const handleError = (error: Error) => {
if (error.message.includes('401')) {
router.push('/login');
}
};
window.addEventListener('apierror', handleError);
return () => window.removeEventListener('apierror', handleError);
}, [router]);
return api;
} |
|
А вот пример использования серверных компонентов Next.js для создания интерактивной формы с предварительным просмотром:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| // components/PostEditor.tsx
'use client';
import { useState } from 'react';
import dynamic from 'next/dynamic';
import { MDXRemote } from 'next-mdx-remote';
const MarkdownEditor = dynamic(() => import('./MarkdownEditor'), {
ssr: false,
loading: () => <p>Загрузка редактора...</p>,
});
export default function PostEditor() {
const [content, setContent] = useState('');
const [preview, setPreview] = useState(false);
return (
<div className="grid grid-cols-2 gap-4">
<div className="border rounded p-4">
<MarkdownEditor
value={content}
onChange={setContent}
className="w-full h-96"
/>
</div>
<div className="border rounded p-4">
<div className="prose">
<MDXRemote
source={content}
components={{
h1: props => <h1 className="text-2xl font-bold" {...props} />,
p: props => <p className="my-4" {...props} />,
}}
/>
</div>
</div>
</div>
);
} |
|
Особое внимание стоит уделить обработке ошибок. Next.js предоставляет несколько механизмов для этого. Создадим компонент для красивого отображения ошибок:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| // components/ErrorBoundary.tsx
import { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export class ErrorBoundary extends Component<Props, State> {
state = { hasError: false };
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Ошибка компонента:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div className="p-4 bg-red-50 border border-red-200 rounded">
<h2 className="text-red-800">Что-то пошло не так</h2>
<details className="mt-2 text-sm">
<summary>Технические детали</summary>
<pre className="mt-2 whitespace-pre-wrap">
{this.state.error?.toString()}
</pre>
</details>
</div>
);
}
return this.props.children;
}
} |
|
Для оптимизации производительности важно правильно использовать кэширование. Next.js предлагает встроенную поддержку кэширования на уровне страниц:
TypeScript | 1
2
3
4
5
6
7
8
9
| // pages/posts/[id].tsx
export async function getStaticProps({ params }) {
const post = await fetchPost(params.id);
return {
props: { post },
revalidate: 60, // Обновлять страницу каждую минуту
};
} |
|
При работе с формами часто требуется валидация данных. Создадим кастомный хук для управления формами:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| // hooks/useForm.ts
import { useState, useCallback } from 'react';
import { z } from 'zod';
export function useForm<T extends z.ZodType>(schema: T) {
const [data, setData] = useState<z.infer<T>>();
const [errors, setErrors] = useState<Record<string, string>>({});
const validate = useCallback((values: unknown) => {
try {
schema.parse(values);
setErrors({});
return true;
} catch (error) {
if (error instanceof z.ZodError) {
const newErrors = {};
error.errors.forEach(err => {
newErrors[err.path.join('.')] = err.message;
});
setErrors(newErrors);
}
return false;
}
}, [schema]);
return { data, setData, errors, validate };
} |
|
Продвинутые техники
Next.js предлагает два основных подхода к рендерингу: статическую генерацию (SSG) и серверный рендеринг (SSR). Выбор между ними зависит от характера данных и требований к производительности. SSG отлично подходит для контента, который меняется редко - документации, маркетинговых страниц, блогов. SSR незаменим для динамического контента, требующего актуальности в реальном времени.
Рассмотрим продвинутую технику статической генерации с инкрементальной регенерацией (ISR):
TypeScript | 1
2
3
4
5
6
7
8
9
| export async function getStaticProps() {
const products = await fetchProducts();
return {
props: { products },
revalidate: 60, // Обновление раз в минуту
fallback: 'blocking' // Важно для ISR
};
} |
|
ISR позволяет обновлять статические страницы в фоновом режиме, не затрагивая пользовательский опыт. При этом первый запрос после истечения времени ревалидации запустит фоновую регенерацию, а пользователь получит предыдущую версию страницы.
Оптимизация изображений - отдельная головная боль веб-разработки. Next.js предоставляет компонент Image с автоматической оптимизацией:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| function Gallery({ images }) {
return (
<div className="grid grid-cols-3 gap-4">
{images.map(image => (
<Image
key={image.id}
src={image.url}
alt={image.alt}
width={300}
height={200}
quality={75}
placeholder="blur"
blurDataURL={image.thumbnail}
priority={image.isPriority}
sizes="(max-width: 768px) 100vw, 33vw"
/>
))}
</div>
);
} |
|
Этот код автоматически конвертирует изображения в современные форматы, применяет ленивую загрузку и создаёт разные размеры для различных устройств. Приоритетная загрузка (priority) помогает оптимизировать LCP (Largest Contentful Paint).
Для работы с мультимедиа Next.js предлагает встроенную поддержку потоковой передачи данных:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| export default function VideoPlayer({ videoId }) {
const videoRef = useRef<HTMLVideoElement>(null);
const [isBuffering, setIsBuffering] = useState(false);
useEffect(() => {
if (!videoRef.current) return;
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
videoRef.current?.play();
} else {
videoRef.current?.pause();
}
});
});
observer.observe(videoRef.current);
return () => observer.disconnect();
}, []);
return (
<div className="relative">
<video
ref={videoRef}
src={`/api/videos/${videoId}`}
controls
preload="metadata"
onWaiting={() => setIsBuffering(true)}
onPlaying={() => setIsBuffering(false)}
/>
{isBuffering && (
<div className="absolute inset-0 flex items-center justify-center bg-black/50">
<span className="loading-spinner" />
</div>
)}
</div>
);
} |
|
Управление кэшированием требует особого внимания. Next.js позволяет тонко настраивать стратегии кэширования на уровне страниц и API-маршрутов:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| export const config = {
runtime: 'edge',
regions: ['sfo1', 'iad1'],
};
export default async function handler(req) {
const cache = await caches.open('my-cache');
const cached = await cache.match(req);
if (cached) {
return cached;
}
const response = await fetch(req.url, {
headers: req.headers,
cache: 'force-cache',
});
// Кэшируем только успешные ответы
if (response.ok) {
await cache.put(req, response.clone());
}
return response;
} |
|
Продвинутая работа с данными часто требует сложной логики получения и обновления. SWR (stale-while-revalidate) - отличное решение для этого:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| function useProduct(id: string) {
const { data, error, mutate } = useSWR(
[INLINE]/api/products/${id}[/INLINE],
async url => {
const res = await fetch(url);
if (!res.ok) throw new Error('Network response was not ok');
return res.json();
},
{
revalidateOnFocus: false,
dedupingInterval: 5000,
shouldRetryOnError: false
}
);
return {
product: data,
isLoading: !error && !data,
isError: error,
refresh: () => mutate()
};
} |
|
Такой подход обеспечивает отзывчивый интерфейс и актуальность данных без избыточных запросов к серверу.
Next.js предлагает мощные инструменты для оптимизации производительности. Один из них - автоматическая оптимизация шрифтов. Вместо стандартной загрузки через CSS, используется специальный компонент:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| import { Inter, Roboto_Mono } from '@next/font/google';
const inter = Inter({
subsets: ['latin', 'cyrillic'],
display: 'swap',
preload: true,
fallback: ['system-ui', 'arial']
});
const mono = Roboto_Mono({
weight: ['400', '700'],
variable: '--font-mono'
});
export default function Layout({ children }) {
return (
<div className={`${inter.className} ${mono.variable}`}>
{children}
</div>
);
} |
|
Этот подход предотвращает смещение макета при загрузке шрифтов и улучшает метрики Core Web Vitals. Кстати, о метриках - Next.js автоматически собирает аналитику производительности:
TypeScript | 1
2
3
4
5
6
7
8
9
| export function reportWebVitals(metric) {
if (metric.label === 'web-vital') {
analytics.send({
name: metric.name,
value: metric.value,
id: metric.id
});
}
} |
|
Управление состоянием в Next.js приложениях требует особого внимания. Серверный рендеринг накладывает свои ограничения. Я часто использую комбинацию Redux Toolkit и Next.js middleware для синхронизации состояния:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| const store = configureStore({
reducer: {
auth: authReducer,
cart: cartReducer
},
middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(apiMiddleware)
});
function hydrateStore(initialState) {
if (typeof window === 'undefined') {
return store;
}
return configureStore({
...store,
preloadedState: initialState
});
} |
|
Для оптимизации больших списков данных полезна виртуализация. Вот пример с использованием react-virtual:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| function VirtualizedList({ items }) {
const parentRef = useRef();
const rowVirtualizer = useVirtual({
size: items.length,
parentRef,
estimateSize: useCallback(() => 50, []),
overscan: 5
});
return (
<div
ref={parentRef}
style={{ height: '400px', overflow: 'auto' }}
>
<div
style={{
height: [INLINE]${rowVirtualizer.totalSize}px[/INLINE],
position: 'relative'
}}
>
{rowVirtualizer.virtualItems.map(virtualRow => (
<div
key={virtualRow.index}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: [INLINE]${virtualRow.size}px[/INLINE],
transform: [INLINE]translateY(${virtualRow.start}px)[/INLINE]
}}
>
{items[virtualRow.index]}
</div>
))}
</div>
</div>
);
} |
|
Next.js позволяет оптимизировать загрузку модулей через динамические импорты с учётом серверного рендеринга:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| const DynamicChart = dynamic(() =>
import('react-chartjs-2').then(mod => mod.Line), {
ssr: false,
loading: () => <ChartSkeleton />,
suspense: true
});
function Dashboard() {
return (
<Suspense fallback={<ChartSkeleton />}>
<DynamicChart data={chartData} options={chartOptions} />
</Suspense>
);
} |
|
Для сложных форм я создал собственный хук, объединяющий валидацию, управление состоянием и обработку отправки:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| function useComplexForm<T extends Record<string, unknown>>({
initialValues,
validationSchema,
onSubmit
}: FormConfig<T>) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const validateField = useCallback((name: keyof T, value: unknown) => {
try {
validationSchema.pick({ [name]: true }).parse({ [name]: value });
setErrors(prev => ({ ...prev, [name]: undefined }));
} catch (error) {
setErrors(prev => ({
...prev,
[name]: error.errors[0].message
}));
}
}, [validationSchema]);
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
try {
const validData = validationSchema.parse(values);
await onSubmit(validData);
} catch (error) {
if (error instanceof z.ZodError) {
setErrors(error.formErrors.fieldErrors);
}
} finally {
setIsSubmitting(false);
}
};
return {
values,
errors,
isSubmitting,
setFieldValue: (name: keyof T, value: unknown) => {
setValues(prev => ({ ...prev, [name]: value }));
validateField(name, value);
},
handleSubmit
};
} |
|
Для работы с большими объёмами данных Next.js позволяет использовать потоковую передачу данных:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
| export default function handler(req, res) {
const stream = createReadStream('large-file.csv');
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Transfer-Encoding', 'chunked');
stream.pipe(res);
stream.on('error', error => {
console.error('Stream error:', error);
res.status(500).end();
});
} |
|
Критический взгляд
Хотя Next.js решает множество проблем современной веб-разработки, важно трезво оценивать его сильные и слабые стороны. На практике производительность Next.js-приложений может существенно отличаться от заявленной в документации. Я столкнулся с этим при разработке крупного маркетплейса: серверный рендеринг действительно ускорил первую отрисовку, но создал новые проблемы. При большом количестве одновременных запросов нагрузка на сервер выросла настолько, что пришлось серьёзно оптимизировать код и настраивать кэширование:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
| export async function getServerSideProps({ req, res }) {
const cacheKey = `page:${req.url}`;
const cachedData = await redis.get(cacheKey);
if (cachedData) {
return { props: JSON.parse(cachedData) };
}
const data = await fetchExpensiveData();
await redis.setex(cacheKey, 300, JSON.stringify(data));
return { props: data };
} |
|
Типичная ошибка - чрезмерное использование getServerSideProps. Каждый такой запрос блокирует рендеринг страницы. Вместо этого часто можно применить клиентскую подгрузку данных или ISR:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| export async function getStaticProps() {
const staticData = await fetchStaticContent();
return {
props: { staticData },
revalidate: 60
};
}
function Page({ staticData }) {
const { data: dynamicData } = useSWR('/api/dynamic-content', fetcher);
if (!dynamicData) return <LoadingSpinner />;
return (
<div>
<StaticContent data={staticData} />
<DynamicContent data={dynamicData} />
</div>
);
} |
|
Core Web Vitals в Next.js-приложениях требуют особого внимания. Крупные JavaScript-бандлы могут существенно замедлить TTI (Time to Interactive). Решение - грамотное разделение кода и отложенная загрузка компонентов:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
loading: () => <Skeleton />,
ssr: false
});
function Dashboard() {
const [showHeavy, setShowHeavy] = useState(false);
return (
<div>
<button onClick={() => setShowHeavy(true)}>
Загрузить тяжёлый компонент
</button>
{showHeavy && <HeavyComponent />}
</div>
);
} |
|
Отдельная проблема - гидратация. При несоответствии серверного и клиентского рендеринга React может выдавать предупреждения и даже перерендеривать контент. Это особенно заметно при работе с датами или случайными значениями:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| function Comment({ timestamp }) {
// Плохо: разные результаты на сервере и клиенте
const timeAgo = new Date(timestamp).toLocaleString();
// Хорошо: стабильный вывод
const [timeAgo, setTimeAgo] = useState(() =>
new Date(timestamp).toISOString()
);
useEffect(() => {
setTimeAgo(new Date(timestamp).toLocaleString());
}, [timestamp]);
return <span>{timeAgo}</span>;
} |
|
На производительность сильно влияет размер первоначального HTML. Next.js может генерировать огромные строки, особенно если не контролировать размер данных в getServerSideProps:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
| // Антипаттерн: передача избыточных данных
export async function getServerSideProps() {
const entireDatabase = await fetchEverything();
return { props: { data: entireDatabase } };
}
// Правильно: только необходимые данные
export async function getServerSideProps() {
const { id, title, summary } = await fetchArticle();
return { props: { article: { id, title, summary } } };
} |
|
Работа с формами может стать неожиданным источником проблем. Next.js не предоставляет встроенного решения для валидации, а серверный рендеринг усложняет интеграцию популярных библиотек:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
| function Form() {
// Проблема: состояние формы теряется при серверном рендеринге
const form = useForm();
// Решение: использование useEffect для инициализации
const [form] = useState(() => createForm());
useEffect(() => {
form.initialize();
}, []);
return <form>{/* ... */}</form>;
} |
|
Некоторые браузерные API просто недоступны при серверном рендеринге. Это требует дополнительной проверки окружения и условного рендеринга:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| function GeolocatedMap() {
const [coords, setCoords] = useState(null);
useEffect(() => {
if (!navigator?.geolocation) {
console.warn('Геолокация не поддерживается');
return;
}
navigator.geolocation.getCurrentPosition(
pos => setCoords(pos.coords),
err => console.error('Ошибка геолокации:', err)
);
}, []);
if (!coords) return <LoadingMap />;
return <Map center={coords} />;
} |
|
SEO в Next.js тоже не идеален. Хотя серверный рендеринг помогает с индексацией, динамические маршруты могут создавать проблемы для поисковых роботов. Нужно внимательно настраивать sitemap и метатеги:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| function generateSitemapEntries() {
const pages = getAllPages();
return pages.map(page => ({
loc: [INLINE]https://mysite.com${page.path}[/INLINE],
lastmod: page.updateDate,
priority: calculatePriority(page),
changefreq: page.type === 'blog' ? 'weekly' : 'monthly'
}));
}
function calculatePriority(page) {
switch (page.type) {
case 'home': return 1.0;
case 'category': return 0.8;
case 'product': return 0.6;
default: return 0.4;
}
} |
|
Тестирование Next.js-приложений требует особого подхода. Серверный рендеринг усложняет написание тестов, а стандартные инструменты не всегда работают корректно:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| describe('ProductPage', () => {
it('корректно отображает данные с сервера', async () => {
const mockData = { name: 'Тестовый продукт', price: 100 };
// Мокаем getServerSideProps
const props = await getServerSideProps({
params: { id: '123' },
req: {},
res: {}
});
const { container } = render(<ProductPage {...props.props} />);
expect(container).toHaveTextContent(mockData.name);
expect(container).toHaveTextContent(mockData.price.toString());
});
}); |
|
Подводя черту: когда выбирать Next.js
Next.js - мощный инструмент, но не универсальное решение. За годы работы с фреймворком я пришёл к пониманию, что его выбор должен определяться конкретными требованиями проекта. Next.js особенно хорош для проектов, где критична скорость первой загрузки и SEO. Интернет-магазины, новостные порталы, маркетинговые сайты - здесь преимущества серверного рендеринга раскрываются в полной мере. На одном из моих проектов - крупном агрегаторе недвижимости - переход на Next.js привёл к увеличению органического трафика на 40%.
Но есть сценарии, где Next.js может оказаться избыточным. Для внутренних корпоративных приложений с авторизованным доступом SEO не так важен, а накладные расходы на серверный рендеринг могут не окупиться. В таких случаях классический React с клиентским рендерингом часто оказывается более практичным решением.
Существуют достойные альтернативы. Remix, например, предлагает похожую функциональность с акцентом на вложенной маршрутизации и управлении данными. Gatsby отлично подходит для статических сайтов с минимальной динамикой. А для небольших проектов Astro с его подходом "zero JavaScript by default" может оказаться более подходящим выбором.
Next.js постоянно развивается. Недавнее появление серверных компонентов и улучшенная система кэширования делают его ещё привлекательнее. Но важно помнить - технологии должны решать бизнес-задачи, а не создавать дополнительную сложность.
Мой опыт показывает, что Next.js особенно эффективен в следующих ситуациях:
- Проекты с большим объёмом контента, где важна индексация поисковиками.
- Приложения, требующие быстрой первой загрузки на медленных устройствах.
- Проекты, где необходима тесная интеграция фронтенда и бэкенда.
- Сайты с динамическим контентом, требующие кэширования на уровне страниц.
При этом стоит взвесить альтернативы, если:
- Проект представляет собой закрытую административную панель.
- Большая часть контента генерируется на клиенте.
- Требуется максимальная простота развёртывания.
- Ресурсы сервера ограничены.
В конечном счёте, выбор технологии должен основываться на конкретных потребностях проекта, а не на популярности или личных предпочтениях. Next.js - отличный инструмент, но не серебряная пуля для всех проблем веб-разработки.
Не переходит по страницам TS React react-router Здравствуйте, не могу понять в чём моя проблема почему у меня не переходит со страницы Главная на страницу Настройки аккаунта. Вроде всё правильно... Несовместимость React-Router и React-Bootstrap Добрый день,
Пишу маленький проект и в качестве дизайна решил использовать React-Bootstrap.
При создании Навигации сайта использовал Nav. Я... Разница между React и React native Я хочу начать освоение React для фрондента, но при этом хотел бы иметь возможность писать мобильные приложения. И поэтому у меня вопрос: эти... Полифилл для appendChild не подскажите? для IE11 (REACT) Internet Explorer 11 не поддерживает ф-ю appendChild. Нужен полифилл react/ react hook с Rxjs Здравствуйте.
Столкнулся с проблемой изучения библиотеки RxJs.
У меня есть ТЗ, создать секундомер. Но проблема в том, что не могу разобраться как... Идеи для React проекта Добрый вечер, уважающие разработчики! Я начинающий React разработчик, мне нужны идеи для портфолио.
Нужно создать 3-6 работ разного уровня... Config для React App (Конфигурация) Добрый день, дамы и господа!
Недавно начал изучать React. Создал свое первое приложение через npx create-react-app.
Мое приложение кидает... QR сканер для react на несколько камер Здравствуйте. Нужно на react добавить возможность сканирования QR кода для входа в систему.
Нашла компоненты типа react-qr-reader,... Подкиньте идеи для приложений на React Я около месяца изучаю React и уже легко делаю todo list средней сложности
Подкиньте пожалуйста идеи для приложений для новичков
React router и... Посоветуйте уроки react для начинающих Здравствуйте.
Посоветуйте, пожалуйста уроки по react для начинающих.
К примеру создание первого приложения в visual studio code.
Что-то совсем... Кастомные стили для React-codemirror2 Здравствуйте. Я пытаюсь реализовать редактор кода (JSON) в реакт-приложении. Использую библиотеку React-codemirror2. Но никак не могу добиться... Как написать функцию для кнопки в Js React Для кнопки с HTML документа надо написать функцию для JS React которая считывает информацию с <input type="text"> и записывает в...
|