METABYTE
К списку статей

Снижаем размер бандла Next.js в 2026: продакшн-плейбук

Пошаговый план по уменьшению client-бандла в Next.js с фокусом на App Router, RSC и реальных продакшн-трейдоффах. Менее JS — быстрее загрузка и больше конверсий.

21 мая 202611 мин чтенияAI-research draft
Снижаем размер бандла Next.js в 2026: продакшн-плейбук

Если промахнуться с размером бандла в Next.js, вы платите тройную цену: конверсией (плохие Core Web Vitals), бюджетом (CDN/egress/CPU на мобильных), и скоростью выпуска фич (каждая зависимость тянет хвост). Эта статья — короткий путь от “ощущений” к воспроизводимой практике снижения JS-килобайт.

Короткий ответ: в 2026 году главный рычаг — строгий RSC-first (максимум логики на сервере), минимизация "use client"-областей, динамический импорт тяжёлых UI-кусочков, модульные импорты (modularizeImports) и жёсткие бюджеты на бандл в CI. Анализируйте маршруты @next/bundle-analyzer, режьте CJS-зависимости, выносите графики/редакторы в next/dynamic и не протаскивайте серверные утилиты в клиент.

Почему бандл разрастается и где прибыль от его сокращения

  • App Router и React Server Components уже дают шанс почти обнулить клиентский JS у контентных страниц. Но одна неаккуратная "use client" на верхнем уровне — и клиент получает сотни килобайт лишнего кода.
  • Библиотеки в CommonJS (CJS) ломают tree-shaking — берёте больше, чем нужно.
  • UI-«комбайны» (редакторы, дата-пикеры, графики) часто грузятся на каждой странице «просто потому, что импортнулись».
  • CSS-in-JS с рантаймом или тяжёлые utility-first слои без pruning превращаются в неявный налог на килобайты.

Отдача от оптимизации проста: быстрее TTFB не даёт вам бонуса в FCP, если браузер захлёбывается при парсинге JS. Сокращение клиентского бандла на 150–300 КБ обычно даёт заметный аплифт в LCP/INP на мобильных и убирает часть «случайных» отказов при слабой сети. В коммерческих терминах — больше завершённых корзин и ниже стоимость трафика при сопоставимой выручке.

Архитектурный принцип 2026: RSC-first и минимальные клиентские островки

Бейте по корню — "use client" только там, где это оправдано

  • По умолчанию — серверные компоненты (без "use client").
  • Любая интерактивность (формы, виджеты, драг-н-дроп) — в малых изолированных клиентских «островках» подальше от макета и данных.
  • Передавайте вниз только то, что нужно. Протащите JSON, не целые утилиты.

Пример: страница товара рендерится сервером. Виджет «добавить в корзину» — клиентский островок, загружается динамически только на видимой складке.

Инструменты контроля границ

  • Пакеты server-only и client-only помогут рано ловить утечки зависимостей.
  • Линтер с правилами, запрещающими "use client" в корне app/layout.tsx и в общих компонентах сетки.
  • Пересмотр layout-иерархии: чем больше кода в layout, тем чаще он попадает как shared chunk.

Небольшая ирония на ночь: «скинуть JS килограммы пробежкой нельзя — только диетой архитектуры».

Пайплайн: как выглядит производственный контур оптимизации

Архитектура сборки и контроля в типичной установке на Vercel/Cloudflare + GitHub Actions:

  • CI шаг «Analyze Bundles»: @next/bundle-analyzer для client/router chunks, сохранение HTML-отчётов как артефактов.
  • Бюджеты: PR защита — если главная или корзина превысили +50 КБ gzip по сравнению с main, блокируем мердж.
  • Маркировка пакетов: автокомментарий в PR с топ-10 импортов по весу.
  • Релиз: Sentry Performance/RUM метрики (LCP/INP/CLS) по версиям, алерт если LCP > 3.0s на p75 мобильного трафика.
  • Кэширование: CDN для статики, immutable headers и правильные contenthash в файлах.

Минимальная настройка next.config.js

// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});

module.exports = withBundleAnalyzer({
  reactStrictMode: true,
  experimental: {
    // Используйте с осторожностью: следите за релиз-нотами Next.js
    optimizePackageImports: [
      'lodash',
      'date-fns',
      'lucide-react',
    ],
  },
  modularizeImports: {
    lodash: {
      transform: 'lodash/{{member}}',
    },
    'date-fns': {
      transform: 'date-fns/{{member}}',
    },
  },
  images: {
    formats: ['image/avif', 'image/webp'],
  },
});
  • optimizePackageImports и modularizeImports гарантируют, что вы берёте ровно используемые подмодули.
  • Если библиотека в CJS и не шейкится — ищите ESM-альтернативу или меняйте точку входа на ESM.

Резать по месту: динамический импорт тяжёлых компонентов

Пример: графики, редакторы, карты, платежные виджеты

// app/(shop)/product/[id]/AddToCartClient.tsx
"use client";
import dynamic from 'next/dynamic';
import { Suspense } from 'react';

const Chart = dynamic(() => import('./ChartClient'), {
  ssr: false, // график не нужен на сервере, важна скорость интерактива
  loading: () => <div style={{height: 200}}>Загрузка…</div>,
});

export function AddToCartClient({ productId }: { productId: string }) {
  return (
    <div>
      {/* Мелкий интерактив и только он */}
      <button data-id={productId}>Добавить в корзину</button>
      {/* График спроса — только когда нужно и только в клиенте */}
      <Suspense fallback={<div style={{height: 200}}></div>}>
        <Chart productId={productId} />
      </Suspense>
    </div>
  );
}
// app/(shop)/product/[id]/ChartClient.tsx
"use client";
import { useEffect, useRef } from 'react';

export default function ChartClient({ productId }: { productId: string }) {
  const host = useRef<HTMLCanvasElement>(null);
  useEffect(() => {
    let chart: any;
    (async () => {
      const { default: ChartJS } = await import('chart.js/auto');
      chart = new ChartJS(host.current!, {/* options, data by productId */});
    })();
    return () => chart?.destroy?.();
  }, [productId]);
  return <canvas ref={host} width={600} height={200} />;
}
  • Вы не тянете chart.js на каждую страницу — только где он действительно отображается.
  • Для редакторов (@tiptap/*, monaco-editor) — аналогично: динамический импорт с ssr: false и ленивой инициализацией.

Скрипты третьих сторон

Используйте next/script и strategy="lazyOnload" или afterInteractive, чтобы не фризить критический путь. По возможности — загружайте их по событию намерения пользователя (клик «показать карту»), а не по первому пейджлоаду.

Работа с зависимостями: ESM, дупликации и “невидимый” вес

Избегаем CJS и “всё-в-одном” точек входа

  • date-fns вместо moment, lodash-es с модульным импортом вместо lodash полного.
  • Ищите ESM билды: многие пакеты в 2026 отдают exports с module/import полями. Например, import { throttle } from 'lodash-es' с modularizeImports.
  • Если пакет только в CJS и вам нужен один метод — подумайте о микро-утилите на 10 строк.

Дедупликация и турбулентность монореп

  • pnpm/yarn с resolutions/overrides помогут свести несколько версий одной либы к одной.
  • Аккуратно с transpilePackages в Next — это может тянуть внутрь клиента код, который вы рассчитывали оставить на сервере.

CSS и иконки

  • SVG-иконки как инлайновые компоненты или иконочный шрифт? В 2026 предпочитаем инлайн SVG + шейкинг. Бандлер убирает неиспользуемые иконки, если импортировать поимённо (lucide-react по компонентам).
  • Utility-first CSS: будьте внимательны к pruning. Если вы думаете об отказе от избыточных слоёв, посмотрите наш разбор переезда без лишней CSS-структуры.

Сравнение подходов: что даёт реальную экономию

ПодходПлюсыМинусыКогда применять
RSC-first (минимум "use client")Сильно снижает клиентский JS, проще кэшированиеСложнее ментальная модель, ограничения на хук-апи в сервереКонтентные и e-commerce страницы, дашборды с островками интерактива
next/dynamic для тяжёлых виджетовЛокализует вес, ускоряет initial renderВозможны сдвиги макета/метрики, если не зарезервировать местоГрафики, редакторы, карты, платежи
modularizeImports + ESMТочный импорт, хороший tree-shakingТребует проверки пакетов, может ломаться с CJSЛюбые UI/утилиты со множеством экспортов
Переход на Preact compatСущественное снижение веса React-слояМожет ломать RSC/App Router, несовместимость с экосистемойТолько для SPA/микросайтов без RSC
CSS-in-JS без рантайма (vanilla-extract/linaria)Нулевой рантайм-весСложнее DX, билдовый оверхедДизайн-системы и крупные фронты

Важно: Preact-замена в App Router+RSC проектах — неочевидный путь и часто не стоит риска. Сосредоточьтесь на архитектуре и зависимостях.

Что ломается в продакшене

  • Гидратация: перемещение клиента в глубокий подкомпонент без сохранения одинаковой разметки вызывает «hydration mismatch». Лекарство — стабильный HTML-скелет и suspense-fallback такого же размера.
  • Общие layout-чанки: большой код в app/layout.tsx попадёт почти во все маршруты. Выносите тяжёлые куски из layout в дочерние сегменты.
  • CJS внутри клиента: теряете tree-shaking и внезапно получаете +100–300 КБ. Замена на ESM или локальный утилитарный код.
  • next/dynamic({ ssr: false }) повсюду: SEO падает, пользователи видят пустые блоки дольше. Применяйте точечно и резервируйте место.
  • Скрипты аналитики: неправильная стратегия загрузки съедает INP. Ставьте defer/lazyOnload, используйте серверные события, если возможно.
  • Font-bloat: подключение 6 начертаний — это не «красиво», это «тяжело». Субсеттинг и display: swap — база.

Паттерны кода, которые экономят килобайты

1) Подменяем «всё-в-одном» на поимённый импорт

// Плохо
import _ from 'lodash';
const map = _.map;

// Лучше
import { map } from 'lodash-es';

// Идеально (SWC modularize)
// import { map } from 'lodash'  => перепишется в 'lodash/map'

2) Не тащите данные в клиент, если можно в сервер

// Хорошо: серверный компонент грузит данные и отдаёт уже подготовленный JSON
// app/(shop)/product/[id]/page.tsx
import { getProduct, getRecommendations } from '@/server/data'; // server-only
import ProductView from './ProductView'; // серверный компонент

export default async function Page({ params }) {
  const [product, recs] = await Promise.all([
    getProduct(params.id),
    getRecommendations(params.id),
  ]);
  return <ProductView product={product} recs={recs} />;
}

3) Отложенные зависимости

// Загружаем платежный виджет только после явного клика
"use client";
import dynamic from 'next/dynamic';
import { useState } from 'react';

const Payment = dynamic(() => import('./PaymentWidget'), { ssr: false });

export function CheckoutPay() {
  const [open, setOpen] = useState(false);
  return open ? (
    <Payment />
  ) : (
    <button onClick={() => setOpen(true)}>Оплатить</button>
  );
}

Контроль метрик и бюджеты

  • Ставьте бюджеты на route-level: главная ≤ 120 КБ gzip JS, карточка товара ≤ 170 КБ, чекаут ≤ 220 КБ. Числа — ориентиры; установите свои на основе аналитики устройств.
  • Авто-отчёты в PR: top-ровень импортов по весу, дифф в килобайтах.
  • Lighthouse CI и WebPageTest — валидируем LCP/INP для мобильной сети (4G Throttling). RUM-сбор (например, через собственный web-vitals и отправку на бэкенд) — факт, а не синтетика.

Если вы строите мультиарендный SaaS на Next.js, дисциплина бандлов помогает не только витрине, но и админке/обучающему онбордингу. См. смежный разбор из бэкенд-угла: как строить multi-tenant SaaS на Prisma + Postgres.

Частные случаи: иконки, шрифты, изображения, карты

  • Иконки: импортируйте строго используемые компоненты (import { Home, Search } from 'lucide-react'). Если коллекция своя — генерируйте реэкспорт с трешейкаемыми именованными экспортами.
  • Шрифты: next/font с локальным хостингом, субсеттинг под латиницу/кириллицу отдельно, ограничьте начертания. Один variable-шрифт зачастую лучше трёх статических.
  • Изображения: next/image со статической шириной/высотой, sizes для правильного выбора варианта. Сжатие в CI (sharp/squoosh) — меньше тянется в JS и меньше перерисовок.
  • Карты: mapbox-gl/leaflet в динамическом импорте, включайте только на страницах, где карта реально нужна. Подумайте об iframe для простых случаев — иногда это честнее и легче.

Бизнес-контекст: сроки, стоимость, эффект

Сколько это стоит и что даёт в деньгах — вопрос не праздный.

  • Базовый аудит (1–2 дня): интеграция анализатора, бюджеты, карта крупных зависимостей, быстрые выигрыши (модульные импорты, динамический импорт виджетов). Типовая стоимость: ощутимо меньше недели разработки одной средней фичи. Эффект: −50…150 КБ gzip на ключевых маршрутах.
  • Средний объём работ (1–2 недели): рефакторинг "use client"-границ, раскрой layout-чанков, замена 2–3 тяжёлых библиотек на лёгкие, настройка шрифтов/иконок. Эффект: −150…400 КБ gzip, улучшение LCP/INP в p75 на мобильных; измеримый аплифт конверсии страниц списков/карточек.
  • Глубокая переработка (4–6 недель): RSC-first переукладка UI, миграция с рантайм CSS-in-JS на компилируемое решение, деобфускация общего кодшеринга, вынос аналитики/скриптов в отложенную загрузку. Эффект: стабильные Core Web Vitals, снижение отказов, экономия на egress/CDN и меньше инцидентов.

Риски и дорогостоящие ошибки:

  • «Оптимизировали всё динамическим импортом» — потеряли SEO/впечатление первым экраном. Должен быть баланс: критический контент — сервером, тяжелые интерактивы — позже.
  • «Заменили React на Preact» — сломали App Router/RSC/окружение. Цена выезда команды выше, чем экономия десятков килобайт.
  • «Притащили CJS-пакеты в клиент» — tree-shaking не работает, рост веса незаметно. Сначала анализ импорта, потом — добавление.

Практический чек-лист перед релизом

  • app/layout.tsx и app/page.tsx — серверные, без "use client".
  • @next/bundle-analyzer — отчёт по конкретным маршрутам; топ-3 тяжёлых куска идентифицированы и изолированы.
  • modularizeImports включён; импорт из lodash, date-fns, иконпаков — поимённо.
  • Графики/редакторы/карты — через next/dynamic и ленивую инициализацию.
  • Аналитика/чат-виджеты — next/script со стратегией lazyOnload/по действию пользователя.
  • Шрифты — next/font, ограниченный набор начертаний, субсеттинг.
  • Роутовые бюджеты — настроены, CI блокирует регресс.

И да, скриншот зелёных графиков в PR — не метрика. RUM или это не считается.

FAQ

Как понять, что именно утяжеляет мой бандл в Next.js?

Включите @next/bundle-analyzer и соберите продакшн (ANALYZE=true next build). Смотрите отчёты по страницам и shared чанкам: крупные зависимости и пересечения между маршрутами сразу видны. Дополнительно прогоните source-map-explorer для конкретного чанка.

Можно ли просто перевести проект на Preact и «стать легче»?

Только если у вас SPA без App Router/RSC. В App Router-проектах это часто ломает экосистему и даёт больше проблем, чем пользы. Основной выигрыш даст RSC-first и чистка зависимостей.

Стоит ли отключать SSR у тяжёлых компонентов через ssr: false?

Точечно да — для чисто интерактивных виджетов (редактор, график). Но критический контент должен рендериться на сервере. Помните о SEO и пользовательском восприятии первого экрана.

Как бороться с CJS-пакетами, которые не шейкятся?

Ищите ESM-аналоги, импортируйте подмодули напрямую, или заменяйте на локальные микроутилиты. Если без пакета нельзя — изолируйте его за динамическим импортом там, где он действительно нужен.

Нужен ли мне Turbopack в продакшне ради размера бандла?

Turbopack ускоряет дев-сборку. На размер продакшн-бандла ключевое влияние оказывают архитектура (RSC-first), модульные импорты и динамическое расщепление, а не сам инструмент сборки.

Где провести границу "use client" на дашборде?

Верх — серверный: фреймы, навигация, фильтры как серверные компоненты с передачей параметров в URL/формы. Отдельные виджеты (графики, редакторы фильтров) — клиентские островки с динамическим импортом.

Ключевые выводы

  • Стратегия RSC-first и минимальные клиентские «островки» — главный рычаг снижения размера бандла в Next.js в 2026.
  • Динамический импорт тяжёлых виджетов экономит десятки и сотни килобайт на маршрутах, где они не нужны.
  • ESM + modularizeImports восстанавливают tree-shaking; CJS в клиенте почти всегда дорогая ошибка.
  • Роутовые бюджеты и анализ в CI превращают оптимизацию из «разовой акции» в процесс.
  • Шрифты/иконки/analytics легко прячут лишние килобайты — контролируйте их загрузку и формат.
  • Не оптимизируйте «вслепую»: валидируйте эффект через RUM (LCP/INP) и дифф веса чанков в PR.

Если вы строите Next.js-продукт и нужно сократить бандл без срыва релизов — мы в MTBYTE на стороне прагматичных решений. Напишите нам через /contact — разберём ваш конкретный роут-мап и дадим план с оценками сроков и отдачи.

СЛЕДУЮЩИЙ ШАГ

Понравилось как мыслим?

Применяем те же принципы в клиентских проектах: AI, автоматизации, продукты, которые не умирают после релиза.