BackНазад к блогу

Улучшения использования памяти в Webpack для Next.js 8

Недавно была выпущена версия Next.js 8. Этот релиз включал значительное снижение использования памяти во время сборки. В этой статье мы расскажем, как нам удалось оптимизировать webpack для всего сообщества.

Недавно была представлена Next.js 8. Этот релиз включал значительное снижение использования памяти во время сборки. В этой статье мы расскажем, как нам удалось оптимизировать webpack для всего сообщества.

Next.js — это фреймворк с нулевой конфигурацией, построенный на таких инструментах, как webpack и Babel. Его цель — помочь вам сосредоточиться на важном: коде вашего приложения.

Современные веб-приложения состоят из одной или нескольких страниц. Например, главная страница, блог, панель управления или список товаров.

В Next.js эти страницы становятся файлами в специальной директории pages в корне вашего проекта.

Например: файл pages/about.js соответствует URL /about.

Одно из ключевых ограничений фреймворка — он должен хорошо работать как для одной страницы, так и для тысяч страниц.

Во время реализации Serverless Next.js быстро стало очевидно, что запуск next build для проекта с сотнями страниц приводит к высокому использованию памяти. Иногда превышая лимит кучи памяти Node.js примерно в 1,4 ГБ.

Мы начали профилировать использование памяти во время сборки с помощью инструментов разработчика Chrome.

В полученных профилях мы обнаружили момент, когда webpack выделял 548 МБ памяти за один раз.

Объем выделяемой памяти напрямую зависел от количества страниц: больше страниц — больше использование памяти.

Инструмент профилирования памяти Chrome Developer Tools показал выделение 548 МБ за один раз

Инструмент профилирования памяти Chrome Developer Tools показал выделение 548 МБ за один раз

Изучив стек вызовов в профиле памяти, мы смогли найти функцию, которая вызывала скачок выделения памяти.

Само выделение происходило при вызове source.source(), который генерирует итоговый файл и сохраняет его в память.

Однако, если посмотреть выше на функцию, вызывающую source(), можно увидеть, что compilation.assets перебирался с помощью asyncLib.forEach. Это означает, что предоставленная функция вызывалась для каждого файла в массиве compilation.assets одновременно.

Таким образом, если, например, есть 100 страниц, и каждая страница должна быть записана на диск, приведенный выше код пытался бы записать все 100 одновременно, включая генерацию всех 100 файлов сразу.

Решение этой проблемы — использование семафора для ограничения количества одновременных записей. Обычно мы используем для этого async-sema, но в данном случае в webpack уже был подходящий метод в neo-async:

asyncLib.forEach(compilation.assets, (source, file, callback) => {
  // и т.д.
});

Предыдущий код, который выполнял функцию для всех ассетов одновременно

asyncLib.forEachLimit(compilation.assets, 15, (source, file, callback) => {
  // и т.д.
});

Новый код, который выполняет функцию одновременно максимум для 15 ассетов

После реализации этого ограничения параллелизма и повторного профилирования использования памяти мы увидели, что выделение памяти разделилось на меньшие части по 34 МБ.

Профайлер теперь показывал выделение памяти порциями по 34 МБ с течением времени

Профайлер теперь показывал выделение памяти порциями по 34 МБ с течением времени

Это изменение показало очень многообещающие результаты, но на практике сборка все равно выходила за пределы памяти, поэтому мы продолжили профилирование и исследование проблемы.

Дальнейший анализ профиля памяти показал, что после вызова source.source() память не очищалась (не происходила сборка мусора).

В webpack ассеты обычно являются экземплярами классов Source. Эти классы реализуют метод source(), который генерирует исходный код файла.

Профиль показал, что многие ассеты были экземплярами CachedSource. CachedSource работает так, что при вызове source() результат кэшируется в памяти до тех пор, пока ассет не будет удален.

Проверка плагинов webpack, используемых в Next.js, показала, что у нас не было плагинов, вызывающих source() после того, как webpack записывал файл, а значит, кэширование записанного значения не давало преимуществ.

После совместной работы с Тобиасом Копперсом он реализовал новую опцию output.futureEmitAssets, которая позволяет включить новое поведение записи ассетов.

С этим новым поведением выделяемые блоки памяти сократились до 182 КБ с течением времени.

После всех оптимизаций профайлер показывает выделение памяти порциями по 184 КБ с течением времени

После всех оптимизаций профайлер показывает выделение памяти порциями по 184 КБ с течением времени

Next.js 8 уже включает все эти оптимизации. При использовании Next.js ничего менять не нужно.

Эта оптимизация была внедрена в webpack, а значит, не только пользователи Next.js, но и все пользователи webpack получат выгоду от этих улучшений.

Мы продолжим активно улучшать использование памяти и производительность Next.js и webpack.