Недавно была представлена 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 МБ за один раз
Изучив стек вызовов в профиле памяти, мы смогли найти функцию, которая вызывала скачок выделения памяти.
Само выделение происходило при вызове 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 МБ с течением времени
Это изменение показало очень многообещающие результаты, но на практике сборка все равно выходила за пределы памяти, поэтому мы продолжили профилирование и исследование проблемы.
Дальнейший анализ профиля памяти показал, что после вызова source.source()
память не очищалась (не происходила сборка мусора).
В webpack ассеты обычно являются экземплярами классов Source. Эти классы реализуют метод source()
, который генерирует исходный код файла.
Профиль показал, что многие ассеты были экземплярами CachedSource
. CachedSource
работает так, что при вызове source()
результат кэшируется в памяти до тех пор, пока ассет не будет удален.
Проверка плагинов webpack, используемых в Next.js, показала, что у нас не было плагинов, вызывающих source()
после того, как webpack записывал файл, а значит, кэширование записанного значения не давало преимуществ.
После совместной работы с Тобиасом Копперсом он реализовал новую опцию output.futureEmitAssets
, которая позволяет включить новое поведение записи ассетов.
С этим новым поведением выделяемые блоки памяти сократились до 182 КБ с течением времени.
После всех оптимизаций профайлер показывает выделение памяти порциями по 184 КБ с течением времени
Next.js 8 уже включает все эти оптимизации. При использовании Next.js ничего менять не нужно.
Эта оптимизация была внедрена в webpack, а значит, не только пользователи Next.js, но и все пользователи webpack получат выгоду от этих улучшений.
Мы продолжим активно улучшать использование памяти и производительность Next.js и webpack.