Пошук уроків, статей та іншого контенту
Розберете склад JavaScript-бандла Next.js, його клієнтську частину та вплив залежностей на розмір.
JavaScript-бандл — це набір JavaScript-файлів, підготовлених Next.js для запуску застосунку в браузері.
Під час збірки Next.js:
знаходить модулі, які імпортує застосунок;
будує граф залежностей;
об'єднує модулі у чанки;
оптимізує код;
розділяє код між маршрутами, де це можливо.
Користувач не обов'язково завантажує весь JavaScript застосунку одразу. Next.js може завантажувати лише код поточного маршруту, а інші частини — за потреби.
Наприклад, застосунок може мати такі файли:
app/
├── page.js
├── about/
│ └── page.js
└── components/
└── Counter.jsДля головної сторінки браузеру не обов'язково завантажувати JavaScript, який потрібен лише для /about.
У Next.js з App Router компоненти за замовчуванням є серверними.
Серверний компонент виконується на сервері. Його JavaScript-код не потрібно завантажувати браузеру для виконання компонента на клієнті.
// app/page.js
import Counter from "./components/Counter";
export default function Page() {
return (
<main>
<h1>Головна сторінка</h1>
<Counter />
</main>
);
}У цьому прикладі page.js є серверним компонентом. Він може відрендерити заголовок і використати клієнтський компонент Counter.
Клієнтський компонент позначається директивою "use client" на початку файлу:
// app/components/Counter.js
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Натиснуто: {count}
</button>
);
}Директива "use client" означає, що компонент потребує браузерного JavaScript. Наприклад, це потрібно для:
useState та інших клієнтських хуків;
обробників подій, таких як onClick;
доступу до window, document або localStorage;
інтерактивної поведінки.
У бандл потрапляє не лише сам Counter.js, а й модулі, які він імпортує. У прикладі до клієнтської частини потрапляє React-код, потрібний для роботи стану компонента.
Директива "use client" створює межу між серверною та клієнтською частинами.
Усі модулі, імпортовані клієнтським компонентом, можуть стати частиною клієнтського графа залежностей:
// app/components/ProductButton.js
"use client";
import { useState } from "react";
import { formatPrice } from "../lib/formatPrice";
export default function ProductButton({ price }) {
const [added, setAdded] = useState(false);
return (
<button onClick={() => setAdded(true)}>
{added ? "Додано" : `Додати за ${formatPrice(price)}`}
</button>
);
}Якщо formatPrice.js не містить серверного коду, його функція може бути включена до клієнтського бандла.
Водночас серверний компонент може імпортувати серверний модуль, і цей модуль не обов'язково потрапить до JavaScript, який завантажує браузер.
// app/lib/formatPrice.js
export function formatPrice(value) {
return new Intl.NumberFormat("uk-UA", {
style: "currency",
currency: "UAH",
}).format(value);
}Такий модуль може використовуватися і на сервері, і в клієнтському компоненті. Якщо його імпортує клієнтський компонент, потрібна йому функція буде доступною в клієнтській частині.
Нижче наведено мінімальний приклад для Next.js з App Router:
// app/page.js
import Counter from "./components/Counter";
export default function Page() {
return (
<main>
<h1>Каталог товарів</h1>
<p>Цей текст може бути відрендерений на сервері.</p>
<Counter />
</main>
);
}// app/components/Counter.js
"use client";
import { useState } from "react";
export default function Counter() {
const [quantity, setQuantity] = useState(1);
return (
<section>
<p>Кількість: {quantity}</p>
<button type="button" onClick={() => setQuantity(quantity + 1)}>
Додати
</button>
<button
type="button"
onClick={() => setQuantity(Math.max(1, quantity - 1))}
>
Прибрати
</button>
</section>
);
}У цьому прикладі:
app/page.js є серверним компонентом;
Counter.js є клієнтським компонентом;
JavaScript для useState та обробників подій потрібен браузеру;
статичний заголовок і текст не потребують окремої клієнтської логіки;
клієнтська частина обмежена компонентом Counter та його залежностями.
Next.js не зобов'язаний створювати один великий файл для всього застосунку. Під час збірки код розділяється на чанки.
Чанк — це JavaScript-файл, який містить частину бандла. Чанки можуть містити:
код конкретного маршруту;
спільний код кількох маршрутів;
код React і Next.js;
код залежностей;
службовий код завантаження модулів.
Коли користувач відкриває маршрут, браузер завантажує необхідні для нього чанки. Під час переходу на інший маршрут Next.js може завантажити додатковий чанк, якого ще немає в браузері.
Це дає змогу не завантажувати весь JavaScript наперед.
Залежність — це пакет або модуль, який імпортує ваш код.
Наприклад:
"use client";
import { useState } from "react";
import { format } from "date-fns";
export default function DateLabel({ date }) {
const [currentDate] = useState(date);
return <p>{format(currentDate, "dd.MM.yyyy")}</p>;
}У клієнтський бандл може потрапити код, необхідний для format та його залежностей. Якщо така залежність використовується в кількох клієнтських компонентах, Next.js намагатиметься винести спільний код у відповідний чанк.
На розмір впливають:
кількість імпортованого коду;
розмір самої залежності;
внутрішні залежності пакета;
можливість tree shaking;
спосіб імпорту;
те, чи використовується пакет у серверному або клієнтському коді.
Tree shaking — це видалення невикористаного коду під час збірки. Він працює найкраще, коли пакет і код використовують статичні ES-модулі.
Наприклад, імпорт конкретної функції часто дає збирачу більше можливостей для оптимізації:
import { format } from "date-fns";Порівняно з імпортом усього простору імен:
import * as dateFns from "date-fns";Це не означає, що другий варіант завжди створить більший бандл. Фактичний результат залежить від пакета та налаштувань збирача. Розмір потрібно перевіряти після production-збірки.
Одна й та сама бібліотека може по-різному впливати на застосунок залежно від місця імпорту.
Якщо пакет використовується лише у серверному компоненті, його код не обов'язково потрібно доставляти браузеру.
// app/products/page.js
import { readFile } from "node:fs/promises";
export default async function ProductsPage() {
const data = await readFile("products.json", "utf8");
const products = JSON.parse(data);
return (
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}Якщо ж той самий пакет імпортувати з клієнтського компонента, він може стати частиною клієнтського графа. Це може збільшити розмір JavaScript і навіть спричинити помилку, якщо пакет використовує лише серверні API.
Тому великі залежності, потрібні тільки для сервера, не варто імпортувати в клієнтські компоненти.
Для перевірки production-бандла виконайте:
npm run buildУ типовому проєкті команда запускає next build.
Після завершення Next.js показує інформацію про маршрути та JavaScript, зокрема:
розмір сторінки;
розмір спільного JavaScript;
оцінку JavaScript для першого завантаження.
Для точного аналізу також можна переглянути створені файли в каталозі:
.next/
└── static/
└── chunks/Каталог .next є результатом збірки, а не вихідним кодом застосунку. Не слід редагувати його вручну.
Перевіряйте розмір саме production-збірки. Режим розробки використовує інший процес підготовки модулів і не показує остаточний розмір оптимізованого бандла.
Для початку достатньо дотримуватися кількох правил:
не додавайте "use client" без потреби;
залишайте серверними компоненти, яким не потрібна інтерактивність;
тримайте клієнтську межу якомога нижче в дереві компонентів;
не імпортуйте великі бібліотеки в клієнтські компоненти без необхідності;
перевіряйте, які функції пакета справді використовуються;
після змін порівнюйте результат npm run build.
Наприклад, краще зробити клієнтською лише кнопку:
// app/products/page.js
import AddToCartButton from "../components/AddToCartButton";
export default function ProductsPage() {
return (
<main>
<h1>Товар</h1>
<p>Опис товару може залишатися серверним HTML.</p>
<AddToCartButton />
</main>
);
}// app/components/AddToCartButton.js
"use client";
import { useState } from "react";
export default function AddToCartButton() {
const [added, setAdded] = useState(false);
return (
<button type="button" onClick={() => setAdded(true)}>
{added ? "Товар додано" : "Додати в кошик"}
</button>
);
}Це краще, ніж позначати "use client" весь великий компонент сторінки, якщо інтерактивність потрібна лише одній кнопці.
"use client" до кожного компонентаНе кожен компонент має працювати в браузері. Якщо компонент не використовує стан, ефекти, обробники подій або браузерні API, "use client" може бути зайвою.
Імпорт залежності в клієнтському компоненті може збільшити JavaScript, який завантажить браузер. Спочатку перевірте, чи потрібна ця залежність саме на клієнті.
Результат next dev не є розміром production-бандла. Для перевірки використовуйте next build.
Сучасний збирач може видалити невикористаний код. Проте результат залежить від конкретного пакета та способу його публікації. Не робіть висновки лише з рядка імпорту — перевіряйте production-збірку.
Якщо додати "use client" до кореневого компонента, клієнтська межа може охопити значно більшу частину дерева. Інтерактивні компоненти краще виділяти окремо.
JavaScript-бандл складається з чанків, які Next.js створює під час збірки.
У Next.js з App Router компоненти за замовчуванням є серверними.
"use client" додає компонент і його клієнтські залежності до частини, потрібної браузеру.
Клієнтський бандл може містити код імпортованих залежностей.
Розділення коду дає змогу завантажувати JavaScript лише для потрібних маршрутів.
Розмір бандла слід оцінювати після production-збірки за допомогою npm run build.
Менша клієнтська межа та обережний вибір залежностей допомагають зменшити JavaScript для браузера.