У веб-застосунках JavaScript є найдорожчим ресурсом з точки зору обчислювальної потужності пристрою користувача. На відміну від растрових зображень однакової ваги, кожен кілобайт клієнтського JS-коду потребує не лише передачі мережею, але й подальшого розпакування, синтаксичного парсингу та JIT-компіляції рушієм V8 перед безпосереднім виконанням.
Роздутий клієнтський бандл призводить до негайної деградації ключових метрик:
Фреймворк Next.js реалізує нативне Code Splitting за сегментами маршрутів та алгоритм Tree Shaking. Проте неконтрольований імпорт сторонніх npm-бібліотек (наприклад, монолітних утиліт чи повних бібліотек іконок) та використання Barrel Files можуть випадково затягнути в клієнтський бандл сотні кілобайтів мертвого коду.
/dashboard, розташовується в окремому чанку і ніколи не завантажується користувачем, який переглядає /blog.First Load JS shared by all, що агресивно кешуються браузером між переходами.import та export. Якщо імпортовано лише одну функцію з ESM-модуля, збирач видаляє всі інші невикористані частини пакета.Запуск продакшн-збірки зі змінною ANALYZE=true для генерації інтерактивних treemap-звітів клієнтських та серверних чанків.
Пошук аномально великих візуальних блоків, застарілих CJS-пакетів (moment.js, lodash) або дубльованих наборів іконок.
Перенесення важких обчислювальних пакетів у Server Components та налаштування optimizePackageImports у next.config.ts.
Огортання важких графіків, модальних вікон та WYSIWYG-редакторів у next/dynamic з плейсхолдерами завантаження.
Звірка First Load JS у логах збірки. Блокування злиття pull request, якщо вага перевищує цільовий бюджет 100 КБ.
@next/bundle-analyzer#Для генерації інтерактивної карти пам'яті (treemap) використовується офіційний плагін @next/bundle-analyzer:
pnpm add -D @next/bundle-analyzer cross-envПідключення плагіна в next.config.ts:
import type { NextConfig } from 'next';
import withBundleAnalyzer from '@next/bundle-analyzer';
const nextConfig: NextConfig = {
// Експериментальні оптимізації компілятора
experimental: {
// Автоматичний глибокий Tree Shaking для barrel-пакетів
optimizePackageImports: ['lucide-react', 'date-fns', 'lodash-es'],
},
};
const analyzer = withBundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
});
export default analyzer(nextConfig);Для запуску профілювання та відкриття візуального звіту в браузері виконайте:
cross-env ANALYZE=true pnpm run buildnext/dynamic#Для вилучення важких клієнтських бібліотек (наприклад, пакетів візуалізації Recharts або редакторів TipTap) із початкового бандла застосовується функція dynamic():
'use client';
import React, { useState } from 'react';
import dynamic from 'next/dynamic';
// Увага: опція { ssr: false } дозволена ВИКЛЮЧНО в клієнтських компонентах ('use client')
const DynamicAnalyticsChart = dynamic(
() => import('@/components/HeavyChart').then((mod) => mod.HeavyChart),
{
loading: () => (
<div className="w-full h-64 bg-bg-surface animate-pulse rounded-xl border border-border flex items-center justify-center">
<span className="text-xs text-text-muted font-mono">Завантаження модуля графіків...</span>
</div>
),
ssr: false, // Вимикає серверний рендеринг (бібліотека потребує window / canvas)
}
);
export function DashboardMetrics() {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="space-y-4">
<button
onClick={() => setIsOpen(!isOpen)}
className="px-4 py-2 rounded-lg bg-accent-violet text-white text-sm font-medium hover:bg-accent-violet/90 transition-colors"
>
{isOpen ? 'Приховати аналітику' : 'Показати аналітику'}
</button>
{/* Код чанка завантажується браузером лише в момент першого рендерингу */}
{isOpen && <DynamicAnalyticsChart />}
</div>
);
}Розробник у серверному компоненті app/analytics/page.tsx пише: const Chart = dynamic(() => import('./Chart'), { ssr: false });. Що відбудеться під час next build?
У React Server Components код бібліотек виконується виключно на сервері. Залежності, імпортовані в серверні компоненти, конструктивно виключаються з клієнтського JavaScript-бандла.
// Серверний компонент (без директиви 'use client')
import { getPostBySlug } from '@/lib/posts';
// Пакет 'marked' важить ~40 КБ, але він НІКОЛИ не буде переданий клієнту
import { marked } from 'marked';
import sanitizeHtml from 'sanitize-html';
export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPostBySlug(slug);
// Обчислення виконуються на сервері; клієнт отримує чистий безпечний HTML
const rawHtml = await marked.parse(post.content);
const cleanHtml = sanitizeHtml(rawHtml);
return (
<article className="prose dark:prose-invert max-w-3xl mx-auto py-8">
<div dangerouslySetInnerHTML={{ __html: cleanHtml }} />
</article>
);
}moment (~290 КБ, CJS без підтримки Tree Shaking) на date-fns або нативний Intl.DateTimeFormat.lodash (70 КБ CommonJS) на модульний lodash-es або нативні методи масивів стандарту ES2024.Застосунок імпортує іконку: import { Check } from 'lucide-react'. Чому без спеціальної оптимізації це може збільшити клієнтський бандл на сотні кілобайтів і як допомагає optimizePackageImports?
Виклик dynamic(..., { ssr: false }) у файлі без директиви 'use client' спричиняє фатальну помилку компіляції: ssr: false is not allowed with next/dynamic in Server Components.
Рішення: У Server Components виклик dynamic() завжди рендериться на сервері. Якщо потрібно вимкнути серверний рендеринг через звернення до window, виносьте dynamic(..., { ssr: false }) в окремий клієнтський компонент-обгортку з директивою 'use client'.
Бібліотеки у форматі CommonJS (moment.js, класичний lodash) не оптимізуються через Tree Shaking. Імпорт однієї функції import { debounce } from 'lodash' затягує весь моноліт вагою 70+ КБ.
Рішення: Використовуйте сучасні аналоги (lodash-es, date-fns) або вмикайте optimizePackageImports: ['lodash'] у next.config.ts.
Створення загального файлу components/index.ts з директивами export * from './HeavyChart' та export * from './Button' призводить до того, що імпорт простої кнопки import { Button } from '@/components' затягує у бандл важкий графік.
Рішення: Використовуйте прямі імпорти import { Button } from '@/components/Button' або додавайте пакет до experimental.optimizePackageImports у next.config.ts.
Додавання 'use client' на початку app/page.tsx перетворює всю гілку компонентів на клієнтську. Усі утиліти, валідатори та залежності передаються в браузер користувача.
Рішення: Залишайте сторінки та макети серверними. Директиву 'use client' розміщуйте виключно на інтерактивних листках дерева (кнопки, форми, перемикачі).
Огортання дрібних кнопок або тултипів у dynamic() є антипатерном: кожен динамічний імпорт створює окремий HTTP-запит. Мережевий оверхед на передачу заголовків перевищує вигоду від розділення коду.
Рішення: Застосовуйте next/dynamic виключно для компонентів вагою понад 30–50 КБ.
Використання npm install або yarn add у монорепозиторії на базі pnpm руйнує узгодженість файлу блокування та ігнорує глобальне сховище пакетів.
Рішення: Завжди використовуйте команду pnpm add -D <package>.