PunchOluremi Tinubu seeks Christian unity, as CAN honours Obasanjo, Gowon, others at 50InquirerRed tide warning up over Samar’s Irong-Irong BayESPN DeportesChelsea consiguió tres puntos ante un Everton que no dejó de lucharCNN TürkTürkiye’de elektrikli araç şarj tüketimi eylülde yüzde 14,6 azaldıThe Jerusalem PostBritain’s lesson for Israel: The West Bank settlement policy has consequences - opinionESPNDropping your QB? Six fantasy replacements to pick up ASAP for Week 6Bollywood HungamaCountry Delight names Rishab Shetty as its new brand ambassadorRTP DesportoOuro renovado. Iúri Leitão volta a sagrar-se campeão mundial de ciclismo de pistaZDF heuteAktuelle Pressemitteilungen des ZDFLa PresseMontréal | Un homme de 49 ans manque à l’appelХабрShared memory в ROS 2 Humble: CycloneDDS + iceoryx на практикеScreen RantJames Cameron Reacts To Avengers: Endgame Box Office Record With Epic Image
The Daily Newsstand · Free, Always
Saturday, October 17, 2026

Vite, OOM и сервер на 4 ГБ: как мы снизили память сборки на 52%

Translate

Мы разрабатываем новую версию приложения на Vue. Локально всё собирается, на сервере обновление начинает занимать десятки минут. Один из запусков растянулся примерно на сорок. Затем ловим уже вполне определённую проблему:

Out of memory: Killed process 2454183 (node)
anon-rss: 1584424 kB

Ядро убило Node во время сборки Vite. Новая версия приложения оказалась недоступна.

На первый взгляд всё понятно: маленький сервер, большое приложение, нужно больше памяти. Но прежде чем увеличивать сервер или переносить сборку в CI, мы решили посмотреть, что можно сделать с самой сборкой. Ограничивали память Node, меняли импорты, убирали минификацию, пробовали другой движок. Некоторые изменения выглядели убедительно, пока мы не сравнили цифры.

В итоге на одном и том же приложении получили 802 MiB вместо 1681 MiB пикового RSS и 11,7 секунды вместо 41 секунды. Для этого добавили Rsbuild. Ниже — как к этому пришли и почему нескольких настроек Vite оказалось недостаточно.

На что не хватало четырёх гигабайт

У сервера четыре CPU, около 3,8 GiB физической памяти, доступной системе, и 2 GiB swap. На нём продолжают работать Docker, MySQL и остальные сервисы. Сборка запускается рядом с ними.

Новая версия — приложение на Vue 3 и TypeScript с UI-библиотеками, графиками и редакторами. В наших экспериментах Vite обрабатывал примерно 3700–3900 модулей. Использовали Vite 7.3.6 и Node.js 22.23.3. Кроме новой версии, при обновлении собирается легаси на Nuxt и упаковывается Docker-образ.

Поэтому первым делом пришлось разобраться с теми самыми сорока минутами. У обновления несколько этапов, и по общей длительности нельзя понять, какой из них тормозит.

Начали с обычных инструментов:

free -m
vmstat 1
ps -eo pid,ppid,pcpu,rss,comm --sort=-rss

В последующих наблюдениях установка зависимостей занимала примерно 5–9 секунд. Сборка и упаковка легаси могли работать несколько минут. Vite создавал отдельный пик памяти.

Полной разбивки сорокаминутного запуска у нас нет. Следующие измеренные обновления уже не занимали столько времени, поэтому приписывать все сорок минут Vite мы не можем. Зато один из отказов поймали непосредственно во время его работы: RSS процесса вырос примерно до 1,51 GiB, доступная RAM опустилась до 116 MiB, swap заполнился, затем сработал OOM killer.

При этом ещё до сборки dockerd мог занимать около 900 MiB. База данных и остальные процессы тоже никуда не исчезали. Дополнительные полтора гигабайта для сборщика оказывались существенной нагрузкой.

Заполненный swap сам по себе ещё не показывает, что сервер прямо сейчас тратит время на обмен страницами с диском. Поэтому смотрели и si/so в vmstat. В тяжёлых фазах такой обмен действительно происходил.

Эта картина определила задачу: уменьшить пик сборки и оставить больше памяти работающим сервисам.

Сначала убрали лишнюю нагрузку

До изменений Vite запускался после Docker-сборки легаси. К этому моменту dockerd сохранял высокий RSS, и поверх него появлялся ещё один тяжёлый процесс.

Поменяли порядок: сначала собираем новую версию, затем легаси. Это уменьшает наложение нагрузки, но память самого Vite от перестановки не ограничивается. Один из OOM мы зафиксировали уже после этого изменения.

В легаси обнаружилась другая особенность: Nuxt запускал несколько Terser workers, каждый из которых занимал сотни мегабайт. Ограничили параллелизм через terser.parallel: 1. На небольшом сервере одновременная работа нескольких таких процессов обходится дорого.

Здесь нужно учитывать, какой минификатор используется. Настройку Terser мы меняли в легаси. В нашей конфигурации Vite 7 минификацией занимался esbuild — у него другие параметры. Это разделение есть в документации Vite 7.

В самом Vite отключили расчёт gzip-размеров для итогового отчёта:

build: {
  reportCompressedSize: false,
},

Минификация от этого продолжает работать. Убирается дополнительный расчёт сжатых размеров, который на больших файлах тоже требует времени. Описание опции.

Заодно проверили установку зависимостей. Она выглядела вероятным виновником долгого обновления, но в измеренных запусках завершалась за те самые 5–9 секунд, ещё до старта Vite. Оптимизация этого этапа не могла убрать пик следующего.

Все эти изменения имели понятный смысл. Но изолированного замера выигрыша каждой настройки у нас нет, а основной пик Vite остался. Следующим шагом ограничили память Node.

Меньше heap — не всегда лучше

Запустили сборку с лимитом:

node --max-old-space-size=1280 \
  ./node_modules/vite/bin/vite.js build

В одной из локальных серий пик снизился примерно с 1,89 до 1,46 GiB. Время выросло примерно с 12 до 19 секунд. Такой обмен нас устраивал: сборка стала медленнее, зато требовала заметно меньше памяти.

Дальше возникла естественная идея — уменьшить лимит ещё. Проверили её в отдельной локальной серии:

Лимит old space

Результат

Время

Наблюдаемый пик RSS

1280 MiB

Успешно

16,4 с

≈1482 MiB

1248 MiB

Успешно

50,5 с

≈1461 MiB

1216 MiB

JavaScript heap out of memory

Падение примерно через 70 с

≈1415 MiB

Ради экономии примерно 21 MiB пришлось ждать втрое дольше. Ещё один шаг вниз — и сборка перестала завершаться. Оставили 1280 MiB.

У этой настройки есть важная граница применимости. Она ограничивает старое поколение кучи V8. В RSS процесса дополнительно входят другие области памяти, нативные структуры, код и буферы. Дочерний esbuild работает отдельным процессом. Поэтому лимит 1280 MiB вполне совместим с суммарным RSS выше полутора гигабайт. Подробнее устройство памяти описано в документации Node.js.

У нас успели случиться два разных отказа. В первом случае память закончилась у системы, и Linux убил Node. Во втором мы сами слишком сильно ограничили кучу, и V8 завершился с JavaScript heap out of memory. Исправление одного ограничения легко упиралось в другое.

Попробовали уменьшить граф импортов

Лимит позволил снизить расход памяти, но хотелось уменьшить и объём работы сборщика. В Vite 7 за production-сборку отвечает Rollup. Он обрабатывает граф модулей приложения, преобразует код и формирует чанки. Большая библиотека может означать сотни дополнительных модулей, даже если в нашем коде ей соответствует одна строка импорта.

Начали с PrimeVue. Заменили корневые импорты:

import { Button, Select } from 'primevue'

на прямые:

import Button from 'primevue/button'
import Select from 'primevue/select'

Количество обрабатываемых модулей уменьшилось с 3896 до 3730. Такой подход соответствует рекомендации Vite избегать лишних barrel-файлов — промежуточных модулей с множеством реэкспортов.

Однако нужного запаса памяти это не дало. Позже изменение откатили; в финальном сравнении оба сборщика работали на одинаковых исходниках без этой переделки.

Попробовали пройти дальше и заменить больше промежуточных импортов. Такая переделка затрагивала бы около четырёхсот файлов. Граф при этом уменьшился с 3730 до 3723 модулей, а пик RSS — примерно на 3,6 MiB. Ради такого результата большой рефакторинг не имел смысла. Его тоже вернули назад.

С Element Plus первоначальная гипотеза оказалась ещё проще для проверки. Хотелось подключать только нужный DatePicker. Посмотрели импорты — компоненты уже подключались напрямую. Остальное приходило через их внутренние зависимости: календарь, выбор времени, локализацию и вспомогательный код. У одного компонента тоже может быть немаленькая стоимость.

Продолжили локальные эксперименты. В этой таблице — отдельная исследовательская серия, её числа не нужно смешивать с финальными серверными замерами:

Вариант

Обрабатываемые модули

Пиковый RSS, примерно

Базовый Vite 7 в этой серии

3730

1,44 GiB

Дополнительная переделка barrel-файлов

3723

1,44 GiB

Монолитный импорт ECharts

3134

1,48 GiB

Эксперимент с Vite 8 / Rolldown

—

1,62 GiB

Временное исключение ECharts и Element Plus

2230

1,31 GiB

Самый полезный результат здесь — строка с ECharts. Модулей стало меньше примерно на шестьсот, а потребление памяти немного выросло. Значит, по одному числу модулей мы не могли предсказать выигрыш. Модули отличаются размером и содержимым, а результат зависит и от работы сборщика с ними.

Исключение ECharts и Element Plus снизило пик. Но вместе с ними исчезали нужные функции приложения. Это была проверка гипотезы о стоимости частей графа; для поставки такой вариант не подходил. Экспериментальные изменения из таблицы откатили.

Попробовали и отключить минификацию. В одном из других локальных запусков RSS остался около 1,9 GiB. Размер результата увеличивался, а необходимой экономии памяти мы не получали.

Разбиение крупных зависимостей на чанки и ленивая загрузка частей приложения тоже не дали нужного снижения пика. Эти приёмы полезны для загрузки приложения в браузере. Во время production-сборки весь входящий в приложение граф всё равно нужно обработать.

К этому моменту у нас было несколько проверенных ограничений. Дальше уменьшать heap невыгодно. Большая переделка импортов почти ничего не даёт. Удалять нужные библиотеки нельзя. Проба Vite 8 с Rolldown в этой серии тоже потребовала больше памяти. Мы решили проверить другую систему сборки на том же приложении.

Добавили Rsbuild и перепроверили первый результат

Добавили Rsbuild 2.2.13, который использует Rspack, и плагины для Vue и Sass. Vite сохранили для разработки и как доступный вариант production-сборки.

Для сравнения оставили все графики, редакторы и компоненты. В обоих вариантах сохранили минификацию, отключили production sourcemaps и согласовали целевые браузеры. Проверили результат в браузере: проверенные сценарии работали с обоими сборщиками.

Первое наблюдение на сервере показало около 575 MiB RSS у Rsbuild. Выглядело очень хорошо, но состояние процессов мы снимали примерно раз в шесть секунд. Для короткой сборки это слишком редкий опрос: заметный пик мог целиком пройти между снимками.

Повторили замер с интервалом 100 мс. Получили 802 MiB. Разница достаточно большая, чтобы пересмотреть способ наблюдения, прежде чем делать выводы о сборщике.

При каждом опросе складывали RSS основного процесса и всех его потомков, затем брали максимум этой суммы. Такой подход учитывает и отдельный процесс esbuild рядом с Vite. В финальном замере на пике получилось:

Node       1434 MiB
esbuild     247 MiB
------------------
Всего      1681 MiB

Если смотреть только на Node, часть потребления сборки потеряется. Одновременно с RSS мы фиксировали доступную память системы, занятый swap и изменение общесистемных счётчиков обмена со swap.

Сумма RSS тоже имеет ограничения: общие страницы могут учитываться несколько раз, поэтому это не PSS и не точный объём уникальных физических страниц. Очень короткий пик между опросами можно пропустить даже при интервале 100 мс. Для сравнения обоих сборщиков использовали один метод.

Что получилось на сервере

Финальные прогоны сделали на одном сервере и одном коммите приложения. У обоих сборщиков был лимит old space 1280 MiB. Дерево процессов опрашивали раз в 100 мс.

Здесь измерено время самой сборки. Установка зависимостей, тесты и Docker-сборка легаси в него не входят. MiB равен 1024 × 1024 байтам.

Показатель

Vite

Rsbuild

Время сборки

41,0 с

11,7 с

Пиковый суммарный RSS

1681 MiB

802 MiB

Минимальная доступная память системы

201 MiB

938 MiB

Занятый swap перед сборкой

1892 MiB

1868 MiB

Запись системы в swap за интервал сборки

≈169 MiB

≈26 MiB

В этой паре запусков Rsbuild потребовал примерно на 52% меньше RSS и завершился в 3,5 раза быстрее.

Для нашего сервера особенно полезна третья строка. При Vite системе в худший момент оставался примерно 201 MiB доступной памяти. При Rsbuild — 938 MiB. У работающих рядом сервисов появлялся больший запас.

Здесь доступная память — это MemAvailable. Linux оценивает, сколько памяти можно предоставить новым приложениям без дополнительного обмена со swap. Оценка учитывает и часть освобождаемого кеша, поэтому её нельзя приравнивать к полностью свободной RAM. Определение в документации Linux.

Условия запусков стоит учитывать вместе с результатом. Сервер продолжал обслуживать остальные процессы. Состояние памяти и кешей было близким, но не идентичным: например, занятый swap до запусков отличался примерно на 24 MiB. Vite наблюдали при обычном обновлении, Rsbuild — в отдельном повторном запуске. Запись в swap посчитана для всей системы, включая активность остальных сервисов.

Это одна пара серверных прогонов. Десятков повторений с медианой и доверительными интервалами у нас нет. Ранее локально мы тоже сравнивали оба сборщика одинаковым методом: получили примерно 1588 MiB и 35,7 секунды у Vite против 1016 MiB и 15,5 секунды у Rsbuild. Направление совпало, но объединять локальные и серверные числа в одну выборку нельзя.

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

Что в итоге изменилось

Из настроек Node ощутимо помог лимит old space 1280 MiB. Он уменьшил пик ценой времени сборки. Дальнейшее уменьшение лимита давало слишком маленькую экономию и в конце концов приводило к падению.

Порядок этапов, ограничение параллелизма в легаси и отключение расчёта gzip-размеров оставили как способы убрать лишнюю нагрузку. Они не устранили основной пик Vite; отдельного численного выигрыша каждой из этих мер мы не измерили.

Наибольший выигрыш в финальном сравнении дал Rsbuild: 802 MiB вместо 1681 MiB при сохранении нужной функциональности. Для этого приложения на этом сервере он оказался более экономным вариантом. Доступную сборку Vite сохранили.

Общее обновление при этом не стало быстрее в 3,5 раза. После новой версии всё ещё собиралось легаси. В одном из наблюдаемых запусков этот этап занимал несколько минут, RSS dockerd доходил примерно до 1,45 GiB, а swap снова заполнялся почти полностью. У следующего этапа оставалось собственное узкое место.

Если бы я начинал это расследование заново, раньше перешёл бы к трём измерениям: времени конкретного этапа, пику RSS всего дерева процессов и минимальному MemAvailable. С ними стало видно, какие изменения действительно оставляют серверу больше памяти и за какую цену. Например, двадцать мегабайт, сэкономленные ценой трёхкратного замедления, перестали выглядеть полезной оптимизацией.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.