Форум программистов, компьютерный форум, киберфорум
Reangularity
Войти
Регистрация
Восстановить пароль

Next.js для разработки React: преимущества серверного рендеринга

Запись от Reangularity размещена 20.03.2025 в 15:11
Показов 1434 Комментарии 0
Метки next.js, react, ssr

Нажмите на изображение для увеличения
Название: b6852454-f04b-470e-8304-3cfc71a40580.jpg
Просмотров: 98
Размер:	221.8 Кб
ID:	10474
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. Смысл работы примерно таков : пользователь заходит, выбирает из первой таблицы типы аккаунтов-&gt; нажимает...

Ошибка при создании проекта 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 которая считывает информацию с &lt;input type=&quot;text&quot;&gt; и записывает в...

Метки next.js, react, ssr
Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Обнаружение объектов в реальном времени на Python с YOLO и OpenCV
AI_Generated 29.04.2025
Компьютерное зрение — одна из самых динамично развивающихся областей искусственного интеллекта. В нашем мире, где визуальная информация стала доминирующим способом коммуникации, способность машин. . .
Эффективные парсеры и токенизаторы строк на C#
UnmanagedCoder 29.04.2025
Обработка текстовых данных — частая задача в программировании, с которой сталкивается почти каждый разработчик. Парсеры и токенизаторы составляют основу множества современных приложений: от. . .
C++ в XXI веке - Эволюция языка и взгляд Бьярне Страуструпа
bytestream 29.04.2025
C++ существует уже более 45 лет с момента его первоначальной концепции. Как и было задумано, он эволюционировал, отвечая на новые вызовы, но многие разработчики продолжают использовать C++ так, будто. . .
Слабые указатели в Go: управление памятью и предотвращение утечек ресурсов
golander 29.04.2025
Управление памятью — один из краеугольных камней разработки высоконагруженных приложений. Го (Go) занимает уникальную нишу в этом вопросе, предоставляя разработчикам автоматическое управление памятью. . .
Разработка кастомных расширений для компилятора C++
NullReferenced 29.04.2025
Создание кастомных расширений для компиляторов C++ — инструмент оптимизации кода, внедрения новых языковых функций и автоматизации задач. Многие разработчики недооценивают гибкость современных. . .
Гайд по обработке исключений в C#
stackOverflow 29.04.2025
Разработка надёжного программного обеспечения невозможна без грамотной обработки исключительных ситуаций. Любая программа, независимо от её размера и сложности, может столкнуться с непредвиденными. . .
Создаем RESTful API с Laravel
Jason-Webb 28.04.2025
REST (Representational State Transfer) — это архитектурный стиль, который определяет набор принципов для создания веб-сервисов. Этот подход к построению API стал стандартом де-факто в современной. . .
Дженерики в C# - продвинутые техники
stackOverflow 28.04.2025
История дженериков началась с простой идеи — создать механизм для разработки типобезопасного кода без потери производительности. До их появления программисты использовали неуклюжие преобразования. . .
Тестирование в Python: PyTest, Mock и лучшие практики TDD
py-thonny 28.04.2025
Тестирование кода играет весомую роль в жизненном цикле разработки программного обеспечения. Для разработчиков Python существует богатый выбор инструментов, позволяющих создавать надёжные и. . .
Работа с PDF в Java с iText
Javaican 28.04.2025
Среди всех форматов PDF (Portable Document Format) заслуженно занимает особое место. Этот формат, созданный компанией Adobe, превратился в универсальный стандарт для обмена документами, не зависящий. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru