Next.js: кастомизация Webpack через next.config.js

Next.js использует Webpack как основной сборщик (в классических версиях до Turbopack по умолчанию), при этом предоставляет контролируемый способ расширения и изменения конфигурации через файл next.config.js. В отличие от «чистого» Webpack-проекта, где вся конфигурация пишется вручную, здесь базовая сборка уже преднастроена, а разработчику доступен слой расширения, который позволяет точечно вмешиваться в процесс сборки без потери интеграции с SSR, маршрутизацией и оптимизациями фреймворка.

Кастомизация Webpack в Next.js строится вокруг функции webpack, которая возвращает изменённый конфигурационный объект. Эта функция вызывается на этапе сборки и получает доступ к исходной конфигурации, а также к контекстной информации о режиме выполнения.


Основная точка расширения находится в next.config.js:

module.exports = {
  webpack: (config, options) => {
    return config;
  }
};

Первый аргумент config — это готовая конфигурация Webpack, сформированная Next.js. Второй аргумент options содержит служебную информацию:

  • isServer — выполняется ли сборка на сервере
  • buildId — уникальный идентификатор сборки
  • dev — режим разработки
  • defaultLoaders — встроенные загрузчики Next.js (например, для Babel/SWC)

Именно через эти параметры принимаются решения о том, какие изменения применять к конфигурации.


Структура расширения next.config.js

Конфигурационный файл Next.js может содержать не только Webpack-логику, но и другие настройки:

module.exports = {
  reactStrictMode: true,
  swcMinify: true,
  webpack: (config, options) => {
    return config;
  }
};

Webpack-функция является частью общей конфигурации и не существует отдельно от фреймворка. Это важно, потому что любые изменения должны учитывать SSR, разделение клиентского и серверного бандлов и внутренние оптимизации Next.js.


Модификация базовой конфигурации

Наиболее распространённый сценарий — изменение уже существующего config. Типичный подход заключается в аккуратном расширении, а не в полной замене.

module.exports = {
  webpack: (config) => {
    config.mode = 'development';
    return config;
  }
};

Однако прямое переопределение свойств используется редко. Чаще применяются точечные изменения:

  • добавление новых resolve.alias
  • подключение дополнительных rules
  • регистрация плагинов
  • настройка оптимизации

Алиасы модулей

Одной из самых частых задач является настройка алиасов для упрощения импортов.

const path = require('path');

module.exports = {
  webpack: (config) => {
    config.resolve.alias['@'] = path.resolve(__dirname);
    config.resolve.alias['components'] = path.resolve(__dirname, 'components');

    return config;
  }
};

После этого импорт:

import Button from 'components/Button';

становится альтернативой относительным путям с множеством ../.

Важно учитывать, что Next.js уже имеет встроенные резолверы для js, ts, tsx, а также поддержку абсолютных импортов через baseUrl в jsconfig.json или tsconfig.json. Webpack-алиасы используются, когда требуется более сложная или динамическая логика.


Подключение кастомных loaders

Webpack в Next.js уже включает стандартные лоадеры для JavaScript и TypeScript, но иногда требуется обработка нестандартных файлов: YAML, Markdown, SVG как компонентов или кастомных форматов.

Пример добавления загрузчика для Markdown:

module.exports = {
  webpack: (config) => {
    config.module.rules.push({
      test: /\.md$/,
      use: 'raw-loader'
    });

    return config;
  }
};

Другой пример — обработка SVG как React-компонентов:

module.exports = {
  webpack: (config) => {
    config.module.rules.push({
      test: /\.svg$/,
      use: ['@svgr/webpack']
    });

    return config;
  }
};

При добавлении loaders важно учитывать порядок правил. Next.js уже включает обработку изображений, CSS и TypeScript, поэтому конфликтующие правила могут нарушить сборку.


Работа с плагинами Webpack

Webpack-плагины позволяют расширять процесс сборки: оптимизация, генерация файлов, анализ бандла.

Пример подключения Bundle Analyzer:

const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
  webpack: (config, { dev }) => {
    if (!dev) {
      config.plugins.push(new BundleAnalyzerPlugin());
    }

    return config;
  }
};

Плагины подключаются через массив config.plugins. Next.js уже использует множество внутренних плагинов для SSR, code splitting и оптимизации изображений, поэтому добавление новых должно быть аккуратным.


Учет серверной и клиентской сборки

Одно из ключевых различий Next.js по сравнению с обычным Webpack-проектом — разделение сборки на server и client bundles.

module.exports = {
  webpack: (config, { isServer }) => {
    if (isServer) {
      config.externals = [...config.externals];
    } else {
      config.resolve.fallback = {
        fs: false
      };
    }

    return config;
  }
};

Серверная сборка может включать Node.js-модули, тогда как клиентская не имеет доступа к системным API. Поэтому часто требуется:

  • отключать polyfills для Node.js модулей на клиенте
  • исключать серверные библиотеки из client bundle
  • добавлять fallback для fs, path, net

Оптимизация сборки и бандла

Webpack-конфигурация в Next.js позволяет вмешиваться в оптимизацию:

module.exports = {
  webpack: (config) => {
    config.optimization.splitChunks = {
      chunks: 'all'
    };

    return config;
  }
};

Также можно контролировать:

  • разделение vendor-кода
  • кеширование модулей
  • tree shaking
  • минимизацию

Next.js уже применяет агрессивные оптимизации, включая автоматическое code splitting по страницам, поэтому ручная настройка требуется только в специфических случаях.


Интеграция с CSS и стилями

Хотя Next.js имеет встроенную поддержку CSS, Sass и CSS Modules, Webpack-расширения иногда используются для:

  • добавления PostCSS плагинов
  • кастомной обработки CSS
  • интеграции CSS-in-JS инструментов

Пример добавления PostCSS loader:

module.exports = {
  webpack: (config) => {
    config.module.rules.push({
      test: /\.css$/,
      use: [
        'style-loader',
        'css-loader',
        'postcss-loader'
      ]
    });

    return config;
  }
};

Однако в Next.js прямое переопределение CSS pipeline может конфликтовать с встроенной системой. Более корректный путь — расширение через postcss.config.js.


Работа с изображениями и ассетами

Next.js использует встроенный Image Optimization, но Webpack всё равно участвует в обработке ассетов.

Добавление кастомных правил:

module.exports = {
  webpack: (config) => {
    config.module.rules.push({
      test: /\.(png|jpg|gif)$/,
      type: 'asset/resource'
    });

    return config;
  }
};

При этом важно не конфликтовать с next/image, который использует собственный pipeline.


Использование переменных окружения

Webpack в Next.js автоматически прокидывает process.env, но кастомные переменные могут быть добавлены через DefinePlugin:

const webpack = require('webpack');

module.exports = {
  webpack: (config) => {
    config.plugins.push(
      new webpack.DefinePlugin({
        'process.env.CUSTOM_VAR': JSON.stringify('value')
      })
    );

    return config;
  }
};

Next.js также поддерживает .env.local, .env.production и .env.development, что делает прямое использование DefinePlugin менее необходимым, но он остаётся полезным для сложных случаев.


Типичные проблемы при кастомизации Webpack

При модификации конфигурации часто возникают проблемы, связанные с нарушением внутренних соглашений Next.js:

  • конфликт loaders с встроенными правилами Next.js
  • поломка SSR из-за клиентских зависимостей
  • дублирование React из-за неправильных externals
  • ошибки hydration при неправильной сборке модулей
  • ухудшение производительности из-за избыточных правил

Особенно критично вмешательство в babel/swc pipeline, так как Next.js постепенно уходит от Babel в сторону SWC.


Контекст выполнения и порядок применения изменений

Webpack-функция вызывается каждый раз при сборке, и её результат должен быть детерминированным. Любые условия внутри функции должны опираться на options, а не на внешнее состояние.

module.exports = {
  webpack: (config, { dev, isServer }) => {
    if (dev && !isServer) {
      config.devtool = 'source-map';
    }

    return config;
  }
};

Такой подход позволяет поддерживать разные конфигурации для development и production без разделения конфигов.


Расширение возможностей через цепочку модификаций

Конфигурация Webpack в Next.js часто строится как последовательность изменений:

module.exports = {
  webpack: (config, options) => {
    config.resolve.alias['@shared'] = path.resolve(__dirname, 'shared');

    config.module.rules.push(customRule);

    config.plugins.push(customPlugin);

    config.optimization = {
      ...config.optimization,
      minimize: !options.dev
    };

    return config;
  }
};

Такой подход позволяет постепенно наращивать функциональность, не разрушая базовую структуру сборки Next.js.


Взаимодействие с монорепозиториями

В монорепозиториях Webpack-конфигурация Next.js часто требует:

  • настройки resolve.modules
  • корректной работы с symlinks
  • исключения дублирования зависимостей
module.exports = {
  webpack: (config) => {
    config.resolve.symlinks = false;

    return config;
  }
};

Это особенно актуально при использовании Yarn Workspaces или pnpm, где структура node_modules отличается от стандартной.


Роль Webpack в современном Next.js

Хотя Next.js активно внедряет альтернативный сборщик (Turbopack), Webpack остаётся важным компонентом экосистемы. Большинство проектов по-прежнему используют именно его, особенно в production-сценариях, где важна предсказуемость и совместимость с существующими плагинами.

Кастомизация через next.config.js остаётся основным способом адаптации сборки под специфические требования проекта, сохраняя баланс между контролем и автоматизацией.