Пошук уроків, статей та іншого контенту
Зменште JavaScript Bundle, залишаючи більшу частину UI серверною та ізолюючи клієнтську логіку.
У Next.js з App Router компоненти за замовчуванням є Server Components. Вони виконуються на сервері під час рендерингу, а їхній код не потрапляє до браузерного JavaScript Bundle.
Це означає, що Server Component може:
отримувати дані на сервері;
працювати з базою даних або серверними модулями;
виконувати складні обчислення;
формувати HTML для користувача;
не додавати свій код до JavaScript, який завантажує браузер.
// app/page.tsx
export default function HomePage() {
return (
<main>
<h1>Каталог товарів</h1>
<p>Цей компонент є Server Component за замовчуванням.</p>
</main>
);
}У цьому файлі немає директиви "use client", тому компонент виконується на сервері.
JavaScript Bundle — це код, який Next.js передає браузеру для виконання на клієнті. Він потрібен для:
обробників подій;
стану компонентів;
ефектів;
інтерактивних UI-елементів;
доступу до браузерних API.
Статичну розмітку, текст і дані не обов’язково обробляти в браузері. Якщо частина інтерфейсу не потребує інтерактивності, її можна залишити Server Component.
Порівняйте:
// Небажаний варіант для статичного компонента
"use client";
export default function ProductDescription() {
return (
<section>
<h2>Ноутбук</h2>
<p>Потужний ноутбук для щоденної роботи.</p>
</section>
);
}Директива "use client" змушує Next.js включити цей компонент і його залежності до клієнтського Bundle.
Якщо компонент не має інтерактивної логіки, директива не потрібна:
// Кращий варіант
export default function ProductDescription() {
return (
<section>
<h2>Ноутбук</h2>
<p>Потужний ноутбук для щоденної роботи.</p>
</section>
);
}Client Component потрібен, коли компонент використовує можливості браузера або інтерактивність:
useState;
useEffect;
обробники подій, наприклад onClick;
window, document, localStorage;
інші клієнтські API.
Щоб позначити компонент як клієнтський, на початку файлу використовують "use client":
// components/AddToCartButton.tsx
"use client";
import { useState } from "react";
type AddToCartButtonProps = {
productId: number;
};
export default function AddToCartButton({
productId,
}: AddToCartButtonProps) {
const [added, setAdded] = useState(false);
function handleClick() {
setAdded(true);
console.log(`Товар ${productId} додано до кошика`);
}
return (
<button type="button" onClick={handleClick}>
{added ? "Додано" : "Додати до кошика"}
</button>
);
}Тут "use client" потрібна, тому що компонент використовує useState і onClick.
Директива "use client" створює клієнтську межу. Компонент, позначений цією директивою, а також його імпорти формують клієнтську частину дерева.
Тому не слід позначати "use client" на високому рівні без потреби:
// Велика клієнтська сторінка
"use client";
import ProductDescription from "@/components/ProductDescription";
import AddToCartButton from "@/components/AddToCartButton";
export default function ProductPage() {
return (
<main>
<ProductDescription />
<AddToCartButton productId={1} />
</main>
);
}У цьому випадку вся сторінка та імпортовані компоненти можуть опинитися в клієнтській частині, хоча інтерактивним є лише AddToCartButton.
Краще залишити сторінку серверною і зробити клієнтським лише потрібний компонент:
// app/products/page.tsx
import AddToCartButton from "@/components/AddToCartButton";
export default function ProductsPage() {
return (
<main>
<section>
<h1>Ноутбук</h1>
<p>Потужний ноутбук для щоденної роботи.</p>
<AddToCartButton productId={1} />
</section>
</main>
);
}У такій структурі:
ProductsPage виконується на сервері;
текст і розмітка не потребують клієнтського JavaScript;
AddToCartButton завантажує JavaScript лише для власної інтерактивності.
Server Component може передавати Client Component дані через props. Значення мають бути серіалізованими — наприклад:
рядки;
числа;
булеві значення;
масиви;
прості об’єкти.
// app/products/page.tsx
import AddToCartButton from "@/components/AddToCartButton";
const product = {
id: 1,
name: "Ноутбук",
price: 42000,
};
export default function ProductsPage() {
return (
<main>
<h1>{product.name}</h1>
<p>Ціна: {product.price} грн</p>
<AddToCartButton productId={product.id} />
</main>
);
}// components/AddToCartButton.tsx
"use client";
import { useState } from "react";
type AddToCartButtonProps = {
productId: number;
};
export default function AddToCartButton({
productId,
}: AddToCartButtonProps) {
const [added, setAdded] = useState(false);
return (
<button type="button" onClick={() => setAdded(true)}>
{added ? `Товар ${productId} додано` : "Додати до кошика"}
</button>
);
}Не потрібно передавати до Client Component функцію, оголошену в Server Component:
// Так робити не слід
<AddToCartButton onAdd={() => console.log("Додано")} />Функції та інші несеріалізовані значення не можна передавати звичайним способом через межу Server Component і Client Component. Краще передати дані, а обробку події реалізувати всередині клієнтського компонента.
Розглянемо сторінку каталогу. Список товарів не має бути клієнтським, але кнопка кожного товару повинна реагувати на натискання.
// components/ProductList.tsx
import AddToCartButton from "@/components/AddToCartButton";
type Product = {
id: number;
name: string;
price: number;
};
const products: Product[] = [
{
id: 1,
name: "Ноутбук",
price: 42000,
},
{
id: 2,
name: "Монітор",
price: 12000,
},
{
id: 3,
name: "Клавіатура",
price: 3500,
},
];
export default function ProductList() {
return (
<ul>
{products.map((product) => (
<li key={product.id}>
<h2>{product.name}</h2>
<p>{product.price} грн</p>
<AddToCartButton productId={product.id} />
</li>
))}
</ul>
);
}ProductList не використовує стан або обробники подій, тому залишається Server Component.
// components/AddToCartButton.tsx
"use client";
import { useState } from "react";
type AddToCartButtonProps = {
productId: number;
};
export default function AddToCartButton({
productId,
}: AddToCartButtonProps) {
const [isAdded, setIsAdded] = useState(false);
function handleAddToCart() {
setIsAdded(true);
console.log(`Товар ${productId} додано до кошика`);
}
return (
<button type="button" onClick={handleAddToCart}>
{isAdded ? "У кошику" : "Додати до кошика"}
</button>
);
}// app/products/page.tsx
import ProductList from "@/components/ProductList";
export default function ProductsPage() {
return (
<main>
<h1>Каталог товарів</h1>
<ProductList />
</main>
);
}У результаті статична частина каталогу залишається серверною, а JavaScript потрібен лише для кнопок із локальним станом.
Починайте з Server Components і додавайте Client Components лише там, де потрібна інтерактивність.
Зручно дотримуватися такого підходу:
Створіть сторінку без "use client".
Знайдіть елементи, які використовують стан, ефекти або браузерні API.
Винесіть ці елементи в окремі Client Components.
Передайте їм тільки необхідні серіалізовані props.
Не піднімайте "use client" до спільного батьківського компонента без потреби.
Наприклад, у формі пошуку інтерактивним може бути лише поле:
// app/search/page.tsx
import SearchField from "@/components/SearchField";
export default function SearchPage() {
return (
<main>
<h1>Пошук товарів</h1>
<p>Введіть назву товару для пошуку.</p>
<SearchField />
</main>
);
}// components/SearchField.tsx
"use client";
import { useState } from "react";
export default function SearchField() {
const [query, setQuery] = useState("");
return (
<label>
Пошуковий запит
<input
type="search"
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Наприклад, монітор"
/>
<span>Символів: {query.length}</span>
</label>
);
}Сама сторінка та її статичний текст не стають клієнтськими лише через те, що вона рендерить SearchField.
Server Components не означають, що браузер не отримає жодного JavaScript. Next.js все одно передає код для:
Client Components;
навігації;
гідрації інтерактивних частин;
іншої необхідної клієнтської логіки.
Перевага полягає в тому, що код Server Components не додається до клієнтського Bundle. Чим менша клієнтська частина дерева, тим менше JavaScript потрібно завантажити та виконати в браузері.
Варто оцінювати не тільки кількість компонентів, а й їхні залежності. Якщо великий Client Component імпортує бібліотеку, ця бібліотека може збільшити Bundle разом із компонентом.
"use client" до кожної сторінкиЦе нівелює переваги Server Components. Сторінка має бути клієнтською лише тоді, коли сама використовує клієнтські можливості.
Server Component не може використовувати useState або useEffect. Інтерактивну частину потрібно винести в окремий Client Component.
Код із window, document або localStorage повинен виконуватися в Client Component.
Server Component не може передати звичайну локальну функцію Client Component через props. Передавайте дані, а події обробляйте на клієнті.
Якщо "use client" знаходиться у великому спільному компоненті, до клієнтської частини може потрапити значно більше коду, ніж потрібно. Виносьте інтерактивні елементи якомога ближче до їхнього фактичного використання.
Client Components усе одно потребують JavaScript. Мета підходу — не прибрати JavaScript повністю, а залишити на клієнті тільки необхідну логіку.
У Next.js App Router компоненти за замовчуванням є Server Components.
Код Server Components не додається до клієнтського JavaScript Bundle.
"use client" потрібно використовувати лише для інтерактивних компонентів.
Client Component має починатися з директиви "use client".
Server Component може передавати Client Component серіалізовані props.
Щоб зменшити Bundle, залишайте сторінки, списки та статичний UI серверними.
Інтерактивну логіку ізолюйте в маленьких Client Components.
Чим нижче і точніше розташована клієнтська межа, тим менше JavaScript потрібно браузеру.