Утечки памяти в приложениях с использованием STOMP.js чаще всего связаны не с самой библиотекой, а с неправильным управлением жизненным циклом WebSocket-соединений, подписок, таймеров и пользовательских обработчиков.
В долгоживущих SPA-приложениях, административных панелях, системах мониторинга и чатах такие ошибки приводят к постепенному росту потребления памяти, увеличению количества активных сокетов и деградации производительности интерфейса.
Наиболее распространённые причины:
Одна из самых распространённых ошибок — создание нового экземпляра
Client при каждом открытии страницы, вкладки или компонента
без завершения предыдущего подключения.
Проблемный код:
import { Client } from '@stomp/stompjs';
function connect() {
const client = new Client({
brokerURL: 'ws://localhost:15674/ws'
});
client.activate();
}
Если функция вызывается многократно:
setInterval(connect, 5000);
то память будет постоянно расти:
Правильный подход:
import { Client } from '@stomp/stompjs';
let client = null;
function connect() {
if (client && client.active) {
return;
}
client = new Client({
brokerURL: 'ws://localhost:15674/ws'
});
client.activate();
}
async function disconnect() {
if (client) {
await client.deactivate();
client = null;
}
}
Каждый вызов client.subscribe() создаёт объект подписки
и callback-функцию.
Если подписки не удаляются:
client.subscribe('/topic/chat', message => {
console.log(message.body);
});
то:
Особенно опасна ситуация при повторном рендере компонентов.
STOMP.js возвращает объект подписки:
const subscription = client.subscribe(
'/topic/chat',
message => {
console.log(message.body);
}
);
Удаление выполняется через:
subscription.unsubscribe();
Полный пример:
let subscription = null;
function start() {
subscription = client.subscribe(
'/topic/chat',
onMessage
);
}
function stop() {
if (subscription) {
subscription.unsubscribe();
subscription = null;
}
}
В React наиболее частая проблема связана с
useEffect.
Неправильный вариант:
useEffect(() => {
const subscription = client.subscribe(
'/topic/data',
message => {
setData(JSON.parse(message.body));
}
);
}, []);
Подписка никогда не удаляется после размонтирования компонента.
Правильный вариант:
useEffect(() => {
const subscription = client.subscribe(
'/topic/data',
message => {
setData(JSON.parse(message.body));
}
);
return () => {
subscription.unsubscribe();
};
}, []);
Автоматический reconnect — полезный механизм, но при неправильной реализации он вызывает лавинообразный рост памяти.
Проблемный пример:
client.onWebSocketCl ose = () => {
connect();
};
Каждое закрытие создаёт новый клиент:
Правильный подход — использовать встроенный reconnect:
const client = new Client({
brokerURL: 'ws://localhost:15674/ws',
reconnectDelay: 5000
});
STOMP.js самостоятельно управляет повторным подключением.
STOMP.js создаёт внутренние таймеры heartbeat.
Если соединение закрывается некорректно:
client.webSocket.close();
то heartbeat может остаться активным.
Корректное завершение:
await client.deactivate();
Метод:
Часто сообщения сохраняются в массив:
const messages = [];
client.subscribe('/topic/logs', message => {
messages.push(message.body);
});
При большом потоке данных память будет бесконечно расти.
Особенно опасны:
Ограничение буфера:
const MAX_MESSAGES = 1000;
client.subscribe('/topic/logs', message => {
messages.push(message.body);
if (messages.length > MAX_MESSAGES) {
messages.shift();
}
});
Замыкания могут удерживать крупные объекты в памяти.
Проблемный пример:
const largeObject = {
data: new Array(100000).fill('test')
};
client.subscribe('/topic/data', () => {
console.log(largeObject.data.length);
});
Даже если largeObject больше не нужен, callback
удерживает его.
Лучше хранить только необходимые данные:
const size = largeObject.data.length;
client.subscribe('/topic/data', () => {
console.log(size);
});
Опасная ситуация:
const element = document.getElementById('logs');
client.subscribe('/topic/logs', message => {
element.innerHTML += message.body;
});
Если DOM-элемент удалён:
Безопаснее:
let element = document.getElementById('logs');
const subscription = client.subscribe(
'/topic/logs',
message => {
if (element) {
element.innerHTML += message.body;
}
}
);
function destroy() {
subscription.unsubscribe();
element = null;
}
Во Vue проблема аналогична React.
Неправильно:
mounted() {
this.subscription = client.subscribe(
'/topic/chat',
this.onMessage
);
}
Если компонент уничтожается, подписка остаётся.
Правильный вариант:
beforeUnmount() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
Angular-компоненты также требуют очистки подписок.
Пример:
export class ChatComponent implements OnDestroy {
private subscription: StompSubscription;
ngOnInit() {
this.subscription = client.subscribe(
'/topic/chat',
message => {
console.log(message.body);
}
);
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
Иногда STOMP-клиент хранится глобально:
window.stompClient = client;
или:
globalThis.client = client;
Это препятствует сборке мусора.
Лучше использовать:
Проблемный код:
eventBus.on('message', data => {
console.log(data);
});
Если listener не удаляется:
eventBus.off('message', handler);
то:
Типичный пример:
notifications.push({
id: Date.now(),
text: message.body
});
Если уведомления не очищаются:
setInterval(() => {
notifications.shift();
}, 5000);
массив становится бесконечным.
При работе с бинарными сообщениями проблема становится особенно критичной.
Пример:
client.subscribe('/topic/binary', message => {
const blob = new Blob([message.binaryBody]);
cache.push(blob);
});
Blob-объекты занимают значительный объём памяти.
Необходимо:
Проблемный пример:
const url = URL.createObjectURL(blob);
image.src = url;
Если не вызвать:
URL.revokeObjectURL(url);
память браузера не освобождается.
Отладочный режим может вызывать накопление строк.
Пример:
client.debug = str => {
logs.push(str);
};
При большом потоке сообщений:
Безопаснее:
client.debug = () => {};
или ограничивать размер лога:
const MAX_LOGS = 500;
Для поиска утечек используются:
Типичная последовательность:
Особое внимание уделяется:
Во вкладке Network → WS можно увидеть:
Если после закрытия страницы соединение остаётся активным — присутствует утечка.
STOMP.js активно использует:
Проверка:
const interval = setInterval(() => {}, 1000);
clearInterval(interval);
Любой пользовательский таймер должен очищаться при destroy/unmount.
Крупные приложения используют единый сервис:
class StompService {
client = null;
subscriptions = [];
connect() {
this.client = new Client({
brokerURL: 'ws://localhost:15674/ws',
reconnectDelay: 5000
});
this.client.activate();
}
subscribe(destination, callback) {
const sub = this.client.subscribe(
destination,
callback
);
this.subscriptions.push(sub);
return sub;
}
disconnect() {
this.subscriptions.forEach(sub => {
sub.unsubscribe();
});
this.subscriptions = [];
return this.client.deactivate();
}
}
Преимущества:
Для временных объектов можно использовать:
const cache = new WeakMap();
или:
const ref = new WeakRef(object);
Это снижает риск удержания объектов в памяти.
Наиболее характерные признаки:
Основные правила:
if (!client.active) {
client.activate();
}
subscription.unsubscribe();
await client.deactivate();
messages.splice(0, messages.length - MAX);
clearInterval(timer);
Лучше использовать:
Использовать:
reconnectDelay: 5000
client.debug = () => {};
URL.revokeObjectURL(url);