09 сент. 2025 г.·8 мин чтения

Библиотеки загрузки файлов Node.js для больших файлов и потоков

Сравните библиотеки загрузки файлов Node.js для больших файлов, а затем выберите multipart, потоковую или прямую загрузку в хранилище, чтобы держать расход памяти низким.

Библиотеки загрузки файлов Node.js для больших файлов и потоков

Почему большие загрузки ломают приложения

Большие загрузки срываются по скучным причинам, а не из-за чего-то экзотического. Один видеофайл на 3 ГБ быстро создаст проблемы для слабого процесса загрузки, особенно если приложение читает слишком много файла в память до того, как начнет что-то записывать. Если так делают два или три человека одновременно, расход памяти резко растет, сборка мусора начинает мешать, и сервер начинает тормозить задолго до падения.

Команды часто не ожидают этого, потому что обычные тесты проходят на маленьких файлах и быстром офисном интернете. Реальные пользователи делают наоборот. Они загружают файлы с телефонов, домашнего Wi‑Fi или из поезда, где связь пропадает на несколько секунд. Значит, запрос остается открытым долгое время, иногда на минуты, а процесс Node.js все это время удерживает сокеты, буферы и временные данные.

Повторные попытки только усугубляют ситуацию. Пользователь видит, что индикатор загрузки завис, обновляет страницу и начинает заново. Браузер тоже может повторить запрос. Если приложение уже записало часть файла или создало строку в базе данных, в хранилище могут появиться дубликаты объектов и дублирующиеся записи в приложении. Это не просто лишнее место. Это еще и дополнительная уборка и путаные обращения в поддержку.

Ограничения часто расходятся в местах, которые люди забывают проверить. Браузер отправляет большой multipart-запрос, прокси разрешает 100 МБ, приложение разрешает 500 МБ, а объектное хранилище приняло бы 5 ГБ. Запрос умирает на прокси, а логи приложения выглядят безобидно. В других схемах таймаут прокси короче, чем таймаут приложения, поэтому медленные загрузки падают, хотя код сервера выглядит нормально.

Хорошие библиотеки загрузки файлов Node.js помогают, но сами по себе они не исправляют плохой путь. Большие загрузки в Node.js остаются стабильными, когда вся цепочка согласована по размеру, таймаутам, повторным попыткам и тому, куда должны попадать байты. Если цепочка сделана неаккуратно, одна загрузка раздражает. Десять загрузок могут положить сервер.

Что происходит между браузером и хранилищем

Большая загрузка начинается еще до того, как ваш код Node.js увидит хоть один байт. Браузер открывает соединение и отправляет либо multipart/form-data, либо обычный поток. Multipart упаковывает файл вместе с обычными полями формы вроде "title" или "projectId". Обычный поток отправляет только тело файла, и это проще, когда нужен лишь сам файл.

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

Когда запрос доходит до Node.js, парсер читает входящие байты. При обработке multipart-загрузки у парсера две задачи одновременно: он извлекает поля формы и по мере поступления выдает куски файла. Хорошие парсеры делают это постепенно. Плохие схемы слишком много читают в память и превращают один видеофайл на 2 ГБ в проблему для сервера.

Затем приложение решает, куда отправить эти байты. Оно может записать их на локальный диск, передать в объектное хранилище или направить в другой внутренний сервис для проверки или обработки. Для больших загрузок в Node.js самый безопасный путь обычно выглядит так: поток от запроса до места назначения с минимумом буферизации между ними.

Простой пример помогает понять идею. Пользователь загружает видео на 4 ГБ. Браузер отправляет multipart-данные. Прокси принимает соединение. Парсер читает поле "title", а затем начинает выдавать куски видео. Приложение передает эти куски сразу в хранилище вместо того, чтобы собирать огромный Buffer. Если хранилище замедляется, поток тоже должен замедлиться. Эта обратная связь по давлению не дает памяти расти.

Ответ должен приходить только после того, как запись действительно завершится. Не отвечайте 200 OK, когда запрос уже закончился, если файл все еще записывается на диск или в хранилище. Возвращайте успех только после подтверждения от места назначения. Если запись обрывается посередине, клиенту нужна ошибка, а не ложный успех.

Какая библиотека подходит для какой задачи

Одни библиотеки загрузки файлов Node.js упрощают обычные формы. Другие помогают переносить большие файлы, не забивая память и не блокируя приложение. Лучший выбор зависит не столько от популярности, сколько от того, насколько сильно вам нужен контроль над потоком загрузки.

Multer подходит для простых Express-приложений. Если пользователи загружают аватары, PDF или несколько небольших вложений, Multer делает код коротким и понятным. Он начинает теснить, когда файлы становятся большими или когда вам нужен жесткий контроль над тем, куда попадают байты в момент поступления.

Busboy лучше подходит для больших загрузок в Node.js. Он дает прямой доступ к обработке multipart на уровне потоков, поэтому можно передавать данные на диск или в объектное хранилище сразу по мере поступления. Обычно это означает меньшее потребление памяти и меньше неприятных сюрпризов, когда кто-то загружает видео на 4 ГБ вместо картинки на 4 МБ.

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

Если вы используете Fastify, @fastify/multipart обычно оказывается самым удобным вариантом. Он хорошо сочетается с фреймворком и поддерживает потоковую загрузку файлов без навязывания middleware в стиле Express.

Быстрое сравнение поможет сориентироваться:

  • Выбирайте Multer, когда приложение простое, файлы небольшие, а Express уже обрабатывает ваши формы.
  • Выбирайте Busboy, когда важны производительность и прямой контроль над потоками, лимитами и хранением.
  • Выбирайте Formidable, когда один парсер для полей и файлов упрощает обработку запроса.
  • Выбирайте @fastify/multipart, когда приложение уже работает на Fastify.

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

Если вы ожидаете стабильный трафик или очень большие медиафайлы, низкоуровневая потоковая передача или прямая загрузка в хранилище обычно живут дольше, чем удобные middleware-решения.

Когда multipart-разбор все еще уместен

Multipart-разбор по-прежнему остается правильным выбором, когда файл приходит вместе с полями формы, которые серверу нужны сразу. Типичный пример — админ-панель, где вместе с CSV, изображением или PDF загружают user ID, project ID, теги или флаг модерации. Сервер может прочитать поля, проверить права и решить, куда должен попасть файл, прежде чем что-то сохранит.

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

Серверный разбор также помогает, когда метаданные нужно проверить заранее. Возможно, вы хотите отклонять файлы с неправильным MIME-типом, привязывать загрузку к существующей записи, проверять имя файла или применять правило вроде «только бухгалтеры могут загружать отчеты за конец месяца». Если приложению нужно принять это решение до сохранения, multipart-разбор остается полезным.

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

Задайте лимиты до того, как пустите реальный трафик. Начните с нескольких жестких правил:

  • ограничьте размер файла на одну загрузку
  • ограничьте количество файлов в одном запросе
  • ограничьте количество текстовых полей
  • ограничьте размер поля для длинных заметок или JSON-блоков
  • отклоняйте неожиданные имена полей

Эти ограничения важны даже для закрытой админки. Один случайный экспорт, одна перетащенная папка или одно огромное текстовое поле могут без причины занять ваш Node-процесс.

Простой пример: команда операционного отдела загружает видео продукта и одновременно вводит SKU, язык и дату публикации в одной форме. Multipart-разбор здесь уместен, потому что сервер должен проверить SKU, понять, кто вошел в систему, и привязать загрузку к нужной записи. Это гораздо проще, чем сначала отправить видео в хранилище, а потом исправлять метаданные.

Как пошагово организовать потоковую загрузку

Проведите аудит upload backend
Найдите места, где буферизация, временные файлы или слабая очистка создают нагрузку на ваше приложение Node.js.

Большинство библиотек загрузки файлов Node.js умеют либо буферизовать, либо передавать поток. Для больших загрузок в Node.js буферизация — это место, где начинаются проблемы. Видео на 4 ГБ быстро заполняет память, тормозит другие запросы и может положить небольшой сервер.

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

  1. Начинайте с потока запроса, а не с req.body. Если ваш фреймворк пытается сначала разобрать все тело, отключите это для данного маршрута.
  2. Сразу проверяйте доступ. Убедитесь, что пользователь имеет право, отклоните запрещенные типы файлов и остановите загрузку, если она превышает лимит, до того как вы запишете слишком много данных.
  3. Откройте поток назначения заранее. Это может быть временный файл, объектное хранилище или другой процесс, который занимается медиаобработкой.
  4. Считайте байты по мере движения данных. Если загрузка выходит за пределы допустимого, уничтожьте поток и очистите частично сохраненный файл.
  5. Ждите подтверждения от места назначения. Отправляйте успешный ответ только после того, как файл полностью сохранен и сработало финальное событие потока.

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

Проверка content type помогает, но полагаться только на нее нельзя. Браузер может заявить, что файл — это video/mp4, хотя это не так. Для чувствительных данных проверяйте сигнатуру файла после первого куска или передавайте загрузку в воркер, который сможет безопасно ее проверить.

Потоковая загрузка файлов также упрощает контроль затрат. Если у вас экономная инфраструктура, вам не нужно, чтобы загрузки висели в памяти приложения, пока сервер ждет. Oleg Sotnikov часто подталкивает команды именно к такому подходу по одной причине: приложение остается отзывчивым, и вам не нужны слишком мощные машины только для того, чтобы выдержать поток загрузок.

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

Когда прямая загрузка в хранилище работает лучше

Если приложению нужно только разрешить загрузку и запомнить, что это за файл, пропускать его через сервер Node.js часто не лучший вариант. Лучше позволить браузеру отправить файл напрямую в объектное хранилище. Приложение остается легким, а сервер не тратит минуты на обслуживание одного запроса на 5 ГБ.

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

Приложение также может создать запись еще до начала загрузки. В этой записи можно указать имя файла, размер, тип контента, project ID и ожидаемый путь в хранилище. Так вы аккуратно отслеживаете загрузки, не превращая свой API в канал для передачи файлов.

Простой поток выглядит так:

  • Пользователь просит загрузить файл.
  • Приложение возвращает подписанный URL для загрузки или временные учетные данные для хранилища.
  • Браузер отправляет файл прямо в хранилище.
  • Браузер снова обращается к приложению для подтверждения завершения, либо хранилище отправляет событие.
  • Приложение проверяет, что объект существует, и помечает загрузку как готовую.

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

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

Если вам нужно просканировать файл, переписать его или отклонить содержимое после глубокой проверки до сохранения, оставьте загрузку в своем собственном конвейере. В противном случае этот подход обычно выигрывает у большинства библиотек загрузки файлов Node.js для очень больших файлов, потому что снижает нагрузку на память, уменьшает риск таймаута и оставляет приложение свободным для своей основной работы.

Простой пример с загрузкой большого видео

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

Клиенту нужно загрузить учебное видео на 4 ГБ через слабый гостиничный Wi‑Fi. Если ваше приложение Node.js попытается принять весь этот файл, удержать его в памяти и потом переслать в хранилище, проблемы начнутся очень быстро. Одно нестабильное соединение может занять сервер на минуты, а то и намного дольше.

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

Это небольшое изменение очень важно. Приложение обрабатывает короткий auth-запрос, а не поток на 4 ГБ. Если Wi‑Fi в отеле ненадолго пропадет, клиент загрузки сможет повторить попытку или продолжить без того, чтобы Node-процесс крутил огромным телом запроса.

Практический поток выглядит так:

  • Пользователь нажимает Upload, и приложение запрашивает у backend Node временное разрешение на загрузку.
  • Backend проверяет, кто пользователь, какой файл он хочет отправить и каким может быть его размер.
  • Браузер загружает видео прямо в хранилище и показывает прогресс в интерфейсе.
  • Хранилище подтверждает загрузку, и только после этого приложение создает финальную запись в базе данных.

Пока файл передается, приложение может по-прежнему отслеживать прогресс. Браузер сообщает, сколько байт отправлено, а интерфейс показывает процент завершения. Для долгих загрузок такая обратная связь важна. Пользователи спокойнее, когда видят движение, а не зависший индикатор.

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

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

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

Ошибки, которые вызывают скачки памяти

Многие проблемы с загрузкой начинаются еще до того, как файл попадет в хранилище. Сервер слишком много читает в память, слишком долго ждет, прежде чем отклонить плохой ввод, или оставляет наполовину готовые файлы после ухода клиента. При больших загрузках в Node.js маленькие решения очень быстро складываются в большую проблему.

Самая частая ошибка — буферизовать весь файл в middleware до проверки чего-либо. Видео на 2 ГБ не должно сидеть в RAM только для того, чтобы приложение проверило тип, размер или токен пользователя. Если ваш путь загрузки сначала превращает каждый файл в Buffer, память может резко вырасти даже при небольшой нагрузке.

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

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

Несколько ранних признаков видно сразу:

  • Обработчики загрузки создают Buffer для целого файла
  • Каталоги с временными файлами растут и не уменьшаются
  • Лимиты reverse proxy выше, чем лимиты приложения
  • Отмененные загрузки оставляют файлы после себя
  • Память растет во время медленных загрузок по сети

Настройки прокси часто скрывают проблему. Если nginx или другой прокси пропускает больший файл, чем разрешает приложение, запрос может пройти глубоко в стек, прежде чем приложение его отклонит. Это зря тратит трафик, CPU и память. Ставьте лимиты и в прокси, и в приложении, и держите их в согласии.

Отмененные загрузки тоже требуют очистки. Если браузер закрыл вкладку посреди загрузки на 4 ГБ, приложение должно остановить запись и сразу удалить временный файл. Команды, которые этого не делают, в итоге получают сиротские файлы и заполненные диски. В системах вроде тех, которые Oleg строит для экономных production-стеков, такая очистка не является опцией. Она удерживает расходы на низком уровне и помогает серверу оставаться спокойным под нагрузкой.

Быстрые проверки перед релизом

Проверьте обработку ошибок
Пусть Oleg проверит обработку потоков, частичные записи и очистку после сбоев до релиза.

Большие загрузки обычно ломаются в скучных местах: лимиты размера, очистка, настройки таймаута и путаница с повторными попытками. Функция может выглядеть отлично с тестовым файлом на 20 МБ, а потом развалиться, когда кто-то отправит видео на 4 ГБ через слабый Wi‑Fi.

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

Короткая предрелизная проверка помогает поймать большинство дорогих ошибок:

  • Замедлите сеть и загрузите большой файл медленно. Следите за таймаутами, зависшими запросами и ростом памяти, который не исчезает.
  • Начните загрузку, а затем закройте вкладку браузера или оборвите соединение. Проверьте, закрывает ли приложение потоки и освобождает ли место на диске сразу.
  • Принудительно вызовите ошибку парсера или хранилища посреди загрузки. Временные файлы должны исчезать, а не накапливаться на сервере.
  • Фиксируйте базовые данные для каждой попытки: сколько длилась, сколько байт пришло, где остановилась запись и какая ошибка завершила процесс.
  • Выберите одно место для повторных попыток. Если браузер уже повторяет запрос, приложение не должно еще и повторять отправку в хранилище в фоне.

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

Если вы используете библиотеки загрузки файлов Node.js, которые слишком много буферизуют, эти тесты быстро покажут проблему. Память растет, временные папки увеличиваются, а зависшие загрузки слишком долго остаются открытыми. Хороший код загрузки быстро завершает неудачу, убирает за собой и оставляет в логах след, который человек может понять за десять секунд.

Проводите эти проверки с теми же лимитами и настройками инфраструктуры, которые планируете использовать в продакшне. Именно там всплывают неприятные баги.

Что делать дальше

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

Для большинства проектов выбор простой:

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

Это решение важнее, чем конкретные библиотеки загрузки файлов Node.js, которые вы сравниваете. Хорошая библиотека не спасет архитектуру, которая заставляет сервер приложения держать огромные загрузки в памяти.

Дальше зафиксируйте жесткие лимиты до релиза. Установите максимальный размер файла, максимальное число файлов в запросе и максимальное время запроса. Пропишите эти числа в коде, в reverse proxy и в правилах хранилища. Если продукт допускает видео по 2 ГБ, скажите об этом прямо. Если разрешены только пять изображений размером до 20 МБ каждое, применяйте это ограничение рано и возвращайте простое сообщение об ошибке.

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

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

Если загрузки начинают влиять на продуктовые правила, выбор инфраструктуры и расходы на облако, попросите внешнюю оценку. Oleg Sotnikov помогает стартапам и небольшим командам разбираться с архитектурой, инфраструктурой и AI-driven development в роли Fractional CTO advisor. Такой разбор обычно окупается, когда загрузки одновременно влияют на стоимость хранения, доступность и время разработчиков.

Примите одно решение, задайте лимиты и проверьте худший сценарий. Именно там обычно и проявляются сломанные серверы.