В веб-приложениях JavaScript является самым дорогим ресурсом с точки зрения вычислительной мощности клиентского устройства. В отличие от растровых изображений одинакового веса, каждый килобайт JS-кода требует не только передачи по сети, но и последующей распаковки, синтаксического парсинга и компиляции движком V8 перед исполнением.
Раздутый клиентский бандл приводит к немедленной деградации ключевых метрик:
Фреймворк Next.js реализует автоматический Code Splitting по сегментам маршрутов и алгоритм Tree Shaking. Однако неаккуратное использование внешних npm-библиотек (например, монолитного Lodash, библиотек иконок) и создание так называемых Barrel Files способны случайным образом подтянуть в клиентский бандл сотни килобайт мертвого кода.
/dashboard, компилируется в отдельный чанк и никогда не скачивается пользователем, находящимся на странице /blog.First Load JS shared by all, кэшируемые браузером между переходами.import и export. Если из библиотеки импортирована только одна функция, компилятор стремится вырезать все остальные неиспользуемые части пакета.Запуск сборки с переменной ANALYZE=true. Генерация интерактивных treemap-отчетов для клиентских и серверных чанков.
Поиск аномально больших визуальных блоков (устаревшие библиотеки moment.js, lodash, дублирование иконок).
Перенос тяжелых вычислительных пакетов в Server Components и настройка optimizePackageImports в next.config.ts.
Оборачивание модальных окон, графиков и WYSIWYG-редакторов в next/dynamic с SSR-заглушками.
Сравнение размера First Load JS в терминале. Блокировка регрессий, если вес превышает бюджет 100 КБ.
@next/bundle-analyzer#Для визуализации состава чанков используется официальный плагин @next/bundle-analyzer. В проектах на базе pnpm установка и запуск производятся следующими командами:
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, // Отключает рендеринг на сервере (библиотека использует canvas / window)
}
);
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 });. К чему это приведет при сборке?
В React Server Components код библиотек выполняется исключительно на сервере. Это означает, что зависимости, используемые в серверных компонентах, физически не включаются в клиентский бандл.
// Серверный компонент (без '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 КБ без возможности Tree Shaking) на date-fns или нативный браузерный Intl.DateTimeFormat.lodash (70 КБ CommonJS) на модульный lodash-es либо нативные методы массивов ES2024.В проекте используется библиотека иконок: import { Check } from 'lucide-react'. Почему без специальной оптимизации бандл клиента может увеличиться на сотни килобайт и как помогает optimizePackageImports?
Попытка объявить динамический импорт со свойством ssr: false внутри серверного компонента (в файле без 'use client') приводит к ошибке сборки Next.js: ssr: false is not allowed with next/dynamic in Server Components.
Решение: В серверных компонентах вызов dynamic() всегда работает с SSR. Если вам необходимо полностью отключить рендеринг тяжелой клиентской библиотеки на сервере (например, из-за обращения к window), вынесите вызов dynamic(..., { ssr: false }) в отдельный клиентский компонент-обертку с директивой 'use client'.
Пакеты, скомпилированные в устаревший формат CommonJS (например, классический lodash вместо lodash-es, либо moment.js), не могут быть оптимизированы через Tree Shaking. Даже при импорте одной функции import { debounce } from 'lodash' в бандл попадает весь файл размером 70+ КБ.
Решение: Используйте современные ESM-альтернативы (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-запрос (чанк). Сетевой оверхед на рукопожатия и заголовки HTTP превышает выигрыш от разделения кода.
Решение: Применяйте next/dynamic строго к компонентам размером более 30–50 КБ.
Использование npm install или yarn add в современном монорепозитории или проекте на pnpm нарушает структуру lock-файла и игнорирует глобальное хранилище пакетов.
Решение: Всегда используйте команду pnpm add -D <package>.