Пошук уроків, статей та іншого контенту
Системно оптимізуєте рендеринг, завантаження ресурсів, JavaScript і взаємодію для швидшого Next.js-застосунку.
Комплексна оптимізація Next.js-застосунку — це не одна настройка, а послідовна робота з усім шляхом від запиту до взаємодії користувача:
сервер формує HTML і дані;
браузер завантажує критичні ресурси;
Next.js завантажує JavaScript;
React гідрує клієнтські компоненти;
браузер обробляє дії користувача.
Основні показники:
LCP — коли з’являється найбільший видимий елемент;
CLS — наскільки зміщується макет під час завантаження;
INP — наскільки швидко застосунок реагує на взаємодію;
TTFB — час до отримання першого байта відповіді;
розмір JavaScript і кількість JavaScript, який потрібно виконати до взаємодії.
Оптимізація повинна спиратися на вимірювання. Спочатку визначте повільний сценарій, потім внесіть одну групу змін і повторіть вимірювання.
Перевіряйте застосунок у кількох режимах:
локальний development-режим — для пошуку проблем у коді;
production-збірка — для реальної оцінки розміру бандлів і швидкості;
повільна мережа та мобільний процесор;
холодне завантаження без кешу;
повторний перехід сторінками;
реальна взаємодія: пошук, фільтри, відкриття меню, відправлення форми.
Для аналізу використовуйте:
Performance у Chrome DevTools;
Network у Chrome DevTools;
Lighthouse;
профілювання React;
аналіз production-збірки Next.js.
Важливо розділяти:
лабораторні показники — отримані в контрольованому тесті;
польові показники — поведінка реальних користувачів на різних пристроях і мережах.
Покращення Lighthouse не завжди означає покращення INP для реальних користувачів. Наприклад, сторінка може швидко намалюватися, але довго обробляти введення через великий клієнтський компонент.
В App Router компоненти за замовчуванням є серверними. Це означає, що їхній код не потрібно завантажувати в браузер для відображення HTML.
Додавайте "use client" лише компонентам, яким потрібні:
useState, useEffect або інші клієнтські хуки;
обробники подій;
браузерні API;
клієнтські бібліотеки, що працюють із DOM.
Не робіть "use client" коренем великої сторінки без необхідності. Якщо клієнтський компонент знаходиться високо в дереві, у браузер може потрапити значно більше JavaScript.
Краще розділяти сторінку на серверні та клієнтські частини:
// app/page.tsx
import ProductFilters from "./ProductFilters";
const products = [
{ id: 1, name: "Механічна клавіатура", category: "Периферія" },
{ id: 2, name: "Монітор 27\"", category: "Монітори" },
{ id: 3, name: "USB-мікрофон", category: "Аудіо" },
];
export default function Page() {
return (
<main>
<h1>Каталог</h1>
<ProductFilters products={products} />
</main>
);
}// app/ProductFilters.tsx
"use client";
import { useDeferredValue, useMemo, useState } from "react";
type Product = {
id: number;
name: string;
category: string;
};
type ProductFiltersProps = {
products: Product[];
};
export default function ProductFilters({
products,
}: ProductFiltersProps) {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
const filteredProducts = useMemo(() => {
const normalizedQuery = deferredQuery.trim().toLowerCase();
if (!normalizedQuery) {
return products;
}
return products.filter((product) =>
`${product.name} ${product.category}`
.toLowerCase()
.includes(normalizedQuery),
);
}, [deferredQuery, products]);
return (
<section>
<label htmlFor="product-search">Пошук товарів</label>
<input
id="product-search"
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Введіть назву"
/>
<p aria-live="polite">
Знайдено: {filteredProducts.length}
</p>
<ul>
{filteredProducts.map((product) => (
<li key={product.id}>
{product.name} — {product.category}
</li>
))}
</ul>
</section>
);
}У цьому прикладі:
сторінка залишається серверним компонентом;
інтерактивність ізольована в ProductFilters;
useDeferredValue дозволяє не блокувати введення під час дорогого перерахунку списку;
useMemo не виконує фільтрацію повторно, якщо залежності не змінилися.
useMemo не є універсальною оптимізацією. Якщо обчислення просте, він може ускладнити код без помітної користі. Використовуйте його для справді дорогих обчислень або великих наборів даних.
Сторінка, яку можна підготувати під час збірки або повторно використовувати з кешу, зазвичай має менший TTFB, ніж сторінка, яка виконує всі запити на кожен HTTP-запит.
Для даних, які змінюються періодично, явно задавайте час оновлення кешу:
type Product = {
id: string;
name: string;
};
async function getProducts(): Promise<Product[]> {
const response = await fetch("https://api.example.com/products", {
next: {
revalidate: 300,
tags: ["products"],
},
});
if (!response.ok) {
throw new Error("Не вдалося завантажити товари");
}
return response.json();
}
export default async function ProductsPage() {
const products = await getProducts();
return (
<main>
<h1>Товари</h1>
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</main>
);
}Для даних, які не можна кешувати, використовуйте cache: "no-store":
const response = await fetch("https://api.example.com/account", {
cache: "no-store",
});Не використовуйте no-store за замовчуванням для всіх запитів. Це може змусити сервер повторно виконувати повільні запити під час кожного відвідування.
Якщо на сторінці є незалежні повільні частини, не обов’язково чекати на всі дані перед відправленням першого HTML. Розділіть їх через Suspense і покажіть резервний інтерфейс завантаження. Це дає змогу потоково передавати готові частини сторінки.
import { Suspense } from "react";
async function Recommendations() {
const response = await fetch("https://api.example.com/recommendations", {
next: { revalidate: 600 },
});
if (!response.ok) {
throw new Error("Не вдалося завантажити рекомендації");
}
const items: string[] = await response.json();
return (
<ul>
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
);
}
export default function Page() {
return (
<main>
<h1>Головна сторінка</h1>
<Suspense fallback={<p>Завантаження рекомендацій…</p>}>
<Recommendations />
</Suspense>
</main>
);
}Suspense корисний, коли повільний блок не потрібен для першого змістовного відображення. Не обгортаєте всю сторінку в один великий fallback, якщо це приховує вже готовий контент.
Послідовні await створюють водоспад:
const user = await getUser();
const orders = await getOrders(user.id);
const recommendations = await getRecommendations();Якщо запити незалежні, запускайте їх паралельно:
const [user, recommendations] = await Promise.all([
getUser(),
getRecommendations(),
]);
const orders = await getOrders(user.id);Запити, де другий залежить від результату першого, усе одно мають виконуватися послідовно. Мета — прибрати лише непотрібне очікування.
Використовуйте next/image, щоб Next.js міг:
генерувати відповідний розмір;
використовувати сучасні формати;
завантажувати зображення ліниво;
резервувати місце в макеті.
import Image from "next/image";
export default function Hero() {
return (
<section>
<Image
src="/hero.webp"
alt="Команда працює над застосунком"
width={1200}
height={630}
sizes="(max-width: 768px) 100vw, 1200px"
priority
/>
</section>
);
}priority використовуйте лише для критичного зображення, яке впливає на LCP. Не позначайте так усі зображення на сторінці.
Для зображень нижче першого екрана не задавайте примусове раннє завантаження. Так браузер зможе спочатку завантажити ресурси, потрібні для першого відображення.
Значення width, height або коректне співвідношення сторін допомагають уникати CLS. Якщо розмір відомий лише під час виконання, резервуйте його через CSS.
Підключайте шрифти через next/font, а не через довільні CSS-запити до зовнішнього сервера. Це дає змогу контролювати завантаження шрифту та зменшити залежність від додаткових мережевих з’єднань.
Завантажуйте лише потрібні накреслення та ваги. Підключення всіх варіантів шрифту збільшує розмір ресурсів і може затримати відображення тексту.
Сторонні скрипти часто впливають на TBT та INP. Аналітика, віджети підтримки й рекламні скрипти не повинні блокувати основний контент.
import Script from "next/script";
export default function Layout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
{children}
<Script
src="https://analytics.example.com/script.js"
strategy="lazyOnload"
/>
</>
);
}Вибирайте стратегію залежно від призначення:
beforeInteractive — лише для критичних скриптів, без яких сторінка не може працювати;
afterInteractive — для скриптів, потрібних після початкової гідрації;
lazyOnload — для некритичних скриптів;
worker — тільки якщо конкретна інтеграція сумісна з таким виконанням.
Найкраща оптимізація стороннього скрипту — не завантажувати його, поки він не потрібен.
Клієнтський бандл зменшується, якщо:
серверні компоненти залишаються серверними;
"use client" знаходиться якомога нижче в дереві;
великі бібліотеки не імпортуються на кожну сторінку;
рідко використовувані функції завантажуються динамічно;
не передається великий масив даних через межу серверного та клієнтського компонентів.
Передавання даних у клієнтський компонент також має ціну: ці дані серіалізуються і потрапляють у відповідь. Не передавайте об’єкт із тисячами полів, якщо компонент використовує лише два.
Для важких компонентів, які не потрібні під час першого відображення, застосовуйте dynamic:
import dynamic from "next/dynamic";
const Chart = dynamic(() => import("./Chart"), {
loading: () => <p>Завантаження графіка…</p>,
});
export default function DashboardPage() {
return (
<main>
<h1>Панель керування</h1>
<p>Основні показники вже доступні.</p>
<Chart />
</main>
);
}Це корисно для:
редакторів;
графіків;
складних таблиць;
компонентів із великими клієнтськими бібліотеками;
діалогових вікон, які відкриваються лише після дії користувача.
ssr: false використовуйте лише для компонента, який справді не може бути відрендерений на сервері через залежність від window, document або DOM. Безпідставне вимкнення SSR погіршує початкове відображення та SEO.
Імпортуйте лише необхідні частини бібліотеки, якщо це підтримується її API:
import debounce from "lodash/debounce";Замість імпорту всієї бібліотеки:
import _ from "lodash";Однак не оптимізуйте імпорти навмання. Перевіряйте результат через production-збірку: розмір залежить від конкретної бібліотеки, її формату модулів і налаштувань збирача.
INP погіршується, коли після події браузер повинен виконати довгу синхронну роботу:
відрендерити тисячі елементів;
виконати складне сортування;
перерахувати великий стан;
обробити JSON великого розміру;
запустити важку сторонню бібліотеку.
Розділяйте такі задачі:
негайно оновлюйте видимий стан;
важкий результат обчислюйте пізніше;
обмежуйте кількість елементів;
використовуйте пагінацію або віртуалізацію для дуже довгих списків;
не оновлюйте весь корінь застосунку через локальну дію.
Для пошуку та фільтрації useDeferredValue може зберегти швидку реакцію поля вводу, поки великий список оновлюється окремо.
Для переходів між станами, які не є терміновими, використовуйте useTransition:
"use client";
import { useState, useTransition } from "react";
export default function Tabs() {
const [tab, setTab] = useState("overview");
const [isPending, startTransition] = useTransition();
function selectTab(nextTab: string) {
startTransition(() => {
setTab(nextTab);
});
}
return (
<section>
<button onClick={() => selectTab("overview")}>
Огляд
</button>
<button onClick={() => selectTab("activity")}>
Активність
</button>
{isPending && <p>Оновлення…</p>}
<p>Поточна вкладка: {tab}</p>
</section>
);
}Не обгортаєте в startTransition усе підряд. Введення тексту, натискання кнопки та інші прямі реакції зазвичай повинні оновлюватися без затримки.
Пошук під час кожного натискання клавіші може створити багато запитів. Використовуйте debounce або запускайте пошук після явної дії користувача.
Також:
скасовуйте застарілі запити;
не оновлюйте стан, якщо відповідь вже не актуальна;
показуйте стан завантаження;
не блокуйте весь інтерфейс під час фонової операції;
для повторюваних даних використовуйте кешування на сервері або на клієнті відповідно до вимог застосунку.
Навіть швидко завантажена сторінка сприймається повільною, якщо її елементи зміщуються.
Щоб зменшити CLS:
резервуйте місце для зображень і відео;
не вставляйте банери над уже видимим контентом;
задавайте стабільні розміри для skeleton-компонентів;
не змінюйте висоту основного контейнера після завантаження шрифту;
використовуйте однакову структуру для станів завантаження та готового контенту.
Skeleton не повинен бути довільною анімацією. Його розміри мають відповідати майбутньому контенту, інакше після завантаження станеться зміщення макета.
Практичний порядок роботи:
Визначте повільну сторінку та конкретний сценарій.
Виміряйте LCP, INP, CLS, TTFB і розмір JavaScript.
Перевірте, чи не є сторінка безпідставно клієнтською.
Усуньте послідовні серверні запити, які можна виконати паралельно.
Налаштуйте кешування даних із чіткою стратегією оновлення.
Оптимізуйте LCP-зображення та критичні шрифти.
Відкладіть некритичні скрипти й важкі компоненти.
Перевірте довгі задачі JavaScript, що блокують головний потік.
Перевірте CLS під час завантаження зображень, шрифтів і динамічних блоків.
Повторіть вимірювання у production-збірці.
Оптимізуйте найбільші обмеження, а не всі рядки коду одночасно. Зменшення невеликого компоненту не має значення, якщо головна проблема — зображення в кілька мегабайтів або синхронний обробник на сотні мілісекунд.
"use client" у кореневому layout або сторінці збільшує клієнтський бандл і обсяг гідрації.
Виправлення: винесіть інтерактивність у найменші можливі дочірні компоненти.
useEffectТаке завантаження часто означає, що користувач спочатку отримує порожній HTML, а потім чекає на JavaScript і запит.
Виправлення: дані, потрібні для початкового відображення, отримуйте в серверному компоненті.
no-store використовується для кожного запитуЦе відключає корисне кешування і може збільшити TTFB та навантаження на API.
Виправлення: для кожного типу даних окремо визначте, чи потрібні no-store, revalidate або інша стратегія.
priority додано всім зображеннямРаннє завантаження великої кількості зображень створює конкуренцію за мережу з критичними ресурсами.
Виправлення: пріоритетним робіть лише зображення, яке справді є частиною першого екрану та впливає на LCP.
Графік або редактор, прихований у модальному вікні, не повинен збільшувати початковий JavaScript.
Виправлення: завантажуйте такі компоненти динамічно під час потреби.
useMemoМемоізація кожного виразу додає складність і власні витрати.
Виправлення: застосовуйте useMemo після вимірювання або для помітно дорогих обчислень.
Development-режим має додаткові перевірки та інший характер збірки.
Виправлення: оцінюйте продуктивність через production-збірку та на пристроях, близьких до цільової аудиторії.
Після завершення завантаження блок змінює висоту, і сторінка стрибає.
Виправлення: резервуйте реальний простір і підтримуйте стабільний макет.
Залишайте компоненти серверними, якщо їм не потрібна інтерактивність.
Обмежуйте межі "use client" і кількість даних, що передаються в браузер.
Використовуйте явну стратегію кешування для серверних запитів.
Виконуйте незалежні запити паралельно, а повільні частини передавайте потоково.
Оптимізуйте зображення через next/image, а шрифти — через next/font.
Відкладайте некритичні скрипти та важкі компоненти за допомогою динамічного імпорту.
Слідкуйте не лише за початковим завантаженням, а й за INP та довгими задачами JavaScript.
Резервуйте місце для динамічного контенту, щоб зменшити CLS.
Приймайте рішення на основі production-вимірювань, а не припущень.