Пошук уроків, статей та іншого контенту
Порівняєте серверний і клієнтський бандли та навчитеся не переносити зайвий код у браузер.
Бандл — це набір JavaScript-коду, який Next.js готує для виконання застосунку.
У застосунку з App Router код зазвичай розподіляється між двома середовищами:
серверний бандл виконується на сервері;
клієнтський бандл надсилається браузеру й виконується на стороні користувача.
Цей поділ важливий не лише для продуктивності. Серверний і клієнтський код мають різні можливості:
сервер може звертатися до бази даних і приватних API;
браузер може реагувати на кліки, введення та інші події користувача;
код, який потрібен лише серверу, не повинен потрапляти до браузера.
В App Router компоненти є серверними за замовчуванням.
// app/products/page.tsx
import { getProducts } from "@/lib/products";
export default async function ProductsPage() {
const products = await getProducts();
return (
<main>
<h1>Товари</h1>
<ul>
{products.map((product) => (
<li key={product.id}>
{product.name}: {product.price} грн
</li>
))}
</ul>
</main>
);
}Такий компонент:
виконується на сервері;
може бути асинхронним;
може отримувати дані безпосередньо із серверного джерела;
не потрапляє до клієнтського JavaScript-бандла як інтерактивний компонент.
Клієнтський компонент позначається директивою "use client" на початку файлу:
// components/counter.tsx
"use client";
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Натиснуто: {count}
</button>
);
}Клієнтський компонент потрібен, коли використовується:
useState, useEffect або інші React Hooks;
обробники подій, наприклад onClick;
API браузера, наприклад window або localStorage;
бібліотека, яка працює лише в браузері.
Директива "use client" створює межу клієнтського бандла.
Усі імпорти клієнтського компонента також стають частиною клієнтського графа залежностей.
// components/product-filter.tsx
"use client";
import { formatPrice } from "@/lib/format-price";
import { expensiveServerUtility } from "@/lib/expensive-server-utility";
export function ProductFilter() {
// ...
}У цьому прикладі і formatPrice, і expensiveServerUtility потенційно потрапляють у клієнтський граф, оскільки їх імпортує клієнтський компонент.
Тому "use client" не означає «зробити клієнтським лише цей файл». Воно визначає напрямок імпортів:
ProductFilter
├── format-price
└── expensive-server-utilityЯкщо підключити великий пакет або серверну логіку під клієнтською межею, зайвий код може збільшити JavaScript, який завантажує браузер.
Серверний компонент може імпортувати:
функції доступу до бази даних;
серверні SDK;
файлову систему;
змінні середовища;
модулі для обробки даних, які не потрібні браузеру.
Наприклад:
// lib/products.ts
import "server-only";
type Product = {
id: string;
name: string;
price: number;
};
export async function getProducts(): Promise<Product[]> {
const response = await fetch("https://api.example.com/products", {
headers: {
Authorization: `Bearer ${process.env.PRODUCTS_API_TOKEN}`,
},
next: {
revalidate: 60,
},
});
if (!response.ok) {
throw new Error("Не вдалося отримати товари");
}
return response.json();
}Пакет server-only допомагає захистити модуль від випадкового імпорту в клієнтський код. Якщо такий модуль спробують імпортувати з клієнтського компонента, Next.js повідомить про помилку під час збірки.
Секретний токен також не можна передавати в клієнтський компонент. Змінні середовища без префікса NEXT_PUBLIC_ призначені для сервера.
Серверний компонент може рендерити клієнтський компонент і передавати йому дані через props.
// app/products/page.tsx
import { getProducts } from "@/lib/products";
import { ProductList } from "@/components/product-list";
export default async function ProductsPage() {
const products = await getProducts();
return (
<main>
<h1>Товари</h1>
<ProductList products={products} />
</main>
);
}// components/product-list.tsx
"use client";
type Product = {
id: string;
name: string;
price: number;
};
type ProductListProps = {
products: Product[];
};
export function ProductList({ products }: ProductListProps) {
return (
<ul>
{products.map((product) => (
<li key={product.id}>
<button type="button" onClick={() => alert(product.name)}>
{product.name} — {product.price} грн
</button>
</li>
))}
</ul>
);
}У цьому прикладі:
page.tsx отримує товари на сервері.
Запит до API та токен не потрапляють у браузер.
ProductList додає інтерактивність.
До клієнта передаються лише дані, потрібні для роботи списку.
Props між серверними та клієнтськими компонентами мають бути сумісними з передаванням даних від сервера до клієнта. Найнадійніше передавати прості значення:
рядки;
числа;
логічні значення;
масиви;
звичайні об’єкти;
null.
Не слід передавати функції або серверні об’єкти на кшталт підключення до бази даних.
Розглянемо невдалу структуру:
// components/dashboard.tsx
"use client";
import { getDashboardData } from "@/lib/dashboard";
import { formatCurrency } from "@/lib/format-currency";
export function Dashboard() {
// Клієнтський компонент не повинен сам отримувати серверні дані
// через серверну функцію з приватними залежностями.
const data = getDashboardData();
return <div>{formatCurrency(data.total)}</div>;
}Проблеми такого підходу:
компонент стає клієнтським без потреби;
серверна логіка опиняється під клієнтською межею;
отримання даних змішується з інтерактивним інтерфейсом;
клієнтський бандл може містити зайві імпорти.
Краще розділити отримання даних і взаємодію з користувачем:
// lib/dashboard.ts
import "server-only";
export async function getDashboardData() {
return {
total: 12500,
items: 42,
};
}// app/dashboard/page.tsx
import { getDashboardData } from "@/lib/dashboard";
import { DashboardControls } from "@/components/dashboard-controls";
export default async function DashboardPage() {
const data = await getDashboardData();
return (
<main>
<h1>Панель керування</h1>
<p>Загальна сума: {data.total} грн</p>
<p>Кількість записів: {data.items}</p>
<DashboardControls />
</main>
);
}// components/dashboard-controls.tsx
"use client";
import { useState } from "react";
export function DashboardControls() {
const [isDetailsVisible, setDetailsVisible] = useState(false);
return (
<section>
<button
type="button"
onClick={() => setDetailsVisible((visible) => !visible)}
>
{isDetailsVisible ? "Сховати деталі" : "Показати деталі"}
</button>
{isDetailsVisible && <p>Додаткові деталі панелі</p>}
</section>
);
}Тепер:
getDashboardData виконується тільки на сервері;
DashboardControls містить лише код для взаємодії;
клієнтський бандл не отримує серверну функцію та її залежності.
Клієнтську межу варто розміщувати якомога ближче до інтерактивної частини.
Невдалий варіант:
// app/page.tsx
"use client";
import { Header } from "@/components/header";
import { ProductList } from "@/components/product-list";
import { Footer } from "@/components/footer";
export default function HomePage() {
return (
<>
<Header />
<ProductList />
<Footer />
</>
);
}Увесь файл сторінки та його імпорти тепер опиняються в клієнтському графі.
Краще залишити сторінку серверною:
// app/page.tsx
import { Header } from "@/components/header";
import { ProductList } from "@/components/product-list";
import { Footer } from "@/components/footer";
export default function HomePage() {
return (
<>
<Header />
<ProductList />
<Footer />
</>
);
}А "use client" залишити лише в компоненті, якому потрібна інтерактивність:
// components/product-list.tsx
"use client";
export function ProductList() {
return (
<button type="button" onClick={() => console.log("Фільтр змінено")}>
Змінити фільтр
</button>
);
}Так серверна сторінка може містити клієнтські компоненти, не стаючи клієнтською цілком.
Навіть клієнтський компонент може підключати велику залежність лише тоді, коли вона потрібна. Для цього використовується dynamic.
// components/report-preview.tsx
"use client";
import dynamic from "next/dynamic";
const ReportChart = dynamic(() => import("./report-chart"), {
loading: () => <p>Завантаження графіка...</p>,
});
export function ReportPreview() {
return (
<section>
<h2>Звіт</h2>
<ReportChart />
</section>
);
}Графік завантажується окремим клієнтським фрагментом JavaScript, а не обов’язково разом з основним кодом компонента.
Це корисно для важких частин інтерфейсу, які:
потрібні лише на окремих екранах;
відкриваються після дії користувача;
не потрібні для першого відображення сторінки.
Динамічний імпорт не замінює правильний поділ на серверні та клієнтські компоненти. Спочатку потрібно визначити, який код має виконуватися на сервері, а вже потім оптимізувати завантаження клієнтського коду.
Під час розробки зручно перевіряти:
чи має файл "use client";
які модулі він імпортує;
чи не імпортується серверний модуль у клієнтський;
чи потрібен конкретний пакет у браузері.
Для перевірки production-збірки виконайте:
npm run buildПісля цього зверніть увагу на інформацію Next.js про сторінки та розмір їхніх клієнтських JavaScript-файлів.
Також у браузері можна перевірити вкладку Network:
запустіть production-сервер;
відкрийте сторінку;
знайдіть JavaScript-файли, завантажені браузером;
перевірте, чи не завантажуються пакети, потрібні лише для сервера.
Розмір JavaScript — не єдина характеристика швидкодії, але він допомагає виявити випадкове розширення клієнтського графа залежностей.
Коли потрібно зменшити клієнтський бандл, дійте послідовно:
Знайдіть компонент із "use client".
Перевірте, чи справді йому потрібна інтерактивність.
Винесіть отримання даних у серверний компонент.
Винесіть серверні імпорти з клієнтського компонента.
Залиште в клієнтському компоненті лише стан, обробники подій і потрібну розмітку.
Для великої необов’язкової клієнтської частини використайте динамічний імпорт.
Повторно виконайте production-збірку та порівняйте результат.
"use client" у кореневому layout або сторінці без потребиЦе може зробити великий піддеревом клієнтським і збільшити кількість коду для браузера.
Як виправити: залишайте layout і сторінки серверними, а інтерактивність виносьте в невеликі дочірні компоненти.
Клієнтський компонент не повинен імпортувати модулі, які працюють із секретами, базою даних або серверними SDK.
Як виправити: отримайте дані на сервері й передайте клієнту лише необхідні значення.
Функція не є звичайним серіалізованим значенням для такого передавання.
Як виправити: передавайте дані, ідентифікатори та прості параметри. Обробник події оголошуйте в клієнтському компоненті.
Якщо серверний за задумом модуль імпортує клієнтський компонент або сам опиняється в його графі, межа залежностей змінюється.
Як виправити: аналізуйте весь ланцюжок імпортів від "use client".
"use client" лише через помилку типової інтерактивностіІноді "use client" додають до всієї сторінки, хоча інтерактивність потрібна одній кнопці.
Як виправити: перемістіть кнопку в окремий клієнтський компонент, а решту сторінки залиште серверною.
Серверний бандл виконується на сервері й не надсилається браузеру як клієнтський JavaScript.
Клієнтський бандл потрібен для стану, подій, React Hooks і браузерних API.
Компоненти App Router є серверними за замовчуванням.
"use client" створює межу клієнтського графа залежностей.
Імпорти під цією межею можуть збільшити клієнтський бандл.
Серверні дані та приватні залежності потрібно залишати в серверних компонентах.
Інтерактивні частини варто виносити в найменші можливі клієнтські компоненти.
Велику необов’язкову клієнтську частину можна завантажувати динамічно.
Production-збірка допомагає перевірити фактичний розмір клієнтського коду.