Как старый роутер съел 2.5 ГБ ОЗУ в моей вкладке, или cетевой инфаркт асинхронного кода
- воскресенье, 25 января 2026 г. в 00:00:03
Интро
Это история о том, как «кривой» роутер научил меня смотреть на память браузера иначе. Есть вкладка с ИИ-чатом, есть WebSocket/Streaming, есть обычный i5. И есть момент, когда все это превращается в кирпич: вкладка раздувается до гигабайтов, процессор залипает, страница оживает только на пару минут после перезагрузки.
Лид
Проблема оказалась не в нейросетях и не в JS. Виновник — старый домашний роутер, который не вывозил IPv6 и фрагментацию. Итог — застрявшие пакеты, нарастающий буфер в браузере и тысячи незавершенных async/await-машин в памяти.
TL;DR
- Вкладка с WebSocket раздувается из-за сетевых затыков.
- Роутер ломает MTU/IPv6, пакеты зависают, bufferedAmount растет.
- Асинхронные цепочки не завершаются и копятся в Heap.
- Фикс: MTU 1400 + отключение IPv6.
- В коде: мониторинг bufferedAmount, таймауты и AbortController.
Симптом
- Вкладка с ИИ-чатом пухнет до 1–2.5 ГБ.
- CPU уходит в 100%, интерфейс замирает.
- Перезагрузка помогает на 5 минут, дальше все повторяется.
Диагностика: спускаемся в сеть
Первый шаг — лезем не в код, а в сеть. Идем в логи роутера и видим:
- Exception: tNetTask
- Всплески потерь пакетов
- Пинг до 450 мс
Роутер Mercusys не вытягивал IPv6 и фрагментацию пакетов (MTU). В результате часть трафика «зависала» в пути. Браузер уже отправил данные, а они застряли в системных буферах.
Корень зла: как сеть убивает память
Это важный момент для понимания. В JavaScript async/await — это конечный автомат. Пока сетевой запрос не завершен, состояние машины живет в куче (Heap). Что происходит при «глючной» сети:
- Каждая отправка создает новую асинхронную цепочку.
- Ответ не приходит, цепочка не завершается.
- Состояния копятся в Heap.
- GC не может почистить память, потому что задачи не завершены.
- Task Scheduler ждет роутер, а роутер ждет чудо.
Итог: плохая сеть == утечка памяти даже в «правильном» асинхронном коде.
Лечение: грубая сила побеждает
Я сделал два радикальных шага:
1. Установил MTU 1400.
2. Полностью отключил IPv6.
После этого:
- вкладка стабилизировалась на ~200 МБ;
- роутер перестал зависать;
- сеть стала предсказуемой.
Да, это не «красивое» решение. Но оно спасло систему и вернуло контроль.
Минимальная защита в коде: bufferedAmount
Ниже — «сетевой предохранитель». Этот кусок должен быть в любом JS-приложении,
которое использует WebSocket или стриминг. Он проверяет, сколько данных уже
застряло в буфере браузера, и не позволяет бесконтрольно плодить задачи.
/**
* Функция безопасной отправки данных в WebSocket.
* Предотвращает раздувание памяти (Heap) при сетевых задержках.
*/
function safeSend(socket, data) {
const MAX_BUFFER = 1024 * 1024; // 1 МБ — критический порог затора
if (socket.readyState === WebSocket.OPEN) {
// Проверяем, сколько данных застряло в буфере браузера
if (socket.bufferedAmount > MAX_BUFFER) {
console.warn('Network congestion detected: skipping packet to save RAM.');
// Здесь можно реализовать логику пропуска кадров (LIFO)
// или уведомление пользователя "Проблемы с сетью"
return false;
}
try {
socket.send(data);
return true;
} catch (e) {
console.error('Send failed:', e);
return false;
}
}
return false;
}
// Пример использования в цикле (например, отправка весов или логов)
// Если safeSend вернул false — мы не плодим новые async-задачи и ждем
Почему это критично:
- bufferedAmount показывает, сколько данных уже ушло из JS, но не улетело в сеть.
- Если роутер завис на 30 секунд, буфер вырастет до гигабайтов.
- Этот if режет каскад и спасает память пользователя.
Мораль для разработчиков
- Следите за bufferedAmount в сокетах.
- Используйте AbortController и принудительные таймауты.
- Не рассчитывайте на «вечный» TCP-коннект.
Плохая сеть — это не частный случай. Это нормальное состояние мира. И если
мы не защищаем асинхронный код от сетевого хаоса, то наш код ломает машины
пользователей. Даже если он «правильный».