В протоколе STOMP каждое сообщение передаётся в виде фрейма. Фрейм содержит:
SEND, SUBSCRIBE,
MESSAGE и другие);При интенсивном обмене сообщениями размер каждого фрейма начинает напрямую влиять на:
Особенно критична минимизация фреймов в системах:
Даже уменьшение сообщения на 20–30 байт при сотнях тысяч сообщений в минуту может существенно снизить сетевые издержки.
Типичный STOMP-фрейм выглядит следующим образом:
SEND
destination:/topic/chat
content-type:application/json
{"message":"Hello"}
^@
С точки зрения объёма передаваемых данных фрейм состоит из:
| Часть | Размер |
|---|---|
| Команда | Небольшой |
| Заголовки | Часто основной источник раздувания |
| Пустая строка | 1 байт |
| Тело сообщения | Основная полезная нагрузка |
| NULL-терминатор | 1 байт |
Наибольший вклад в увеличение размера обычно дают:
Многие приложения автоматически добавляют избыточные заголовки:
client.publish({
destination: '/topic/data',
headers: {
'content-type': 'application/json',
'x-client-version': '1.0.0',
'x-user-agent': 'browser',
'x-debug': 'true'
},
body: payload
});
В высоконагруженных системах подобные заголовки создают существенный overhead.
Минималистичный вариант:
client.publish({
destination: '/topic/data',
body: payload
});
Некоторые брокеры позволяют использовать пользовательские короткие заголовки:
headers: {
t: 'json',
v: '1'
}
Вместо:
headers: {
'content-type': 'application/json',
'api-version': '1'
}
Особенно полезно при миллионах сообщений.
content-typeЕсли приложение всегда использует один формат данных, заголовок:
content-type:application/json
становится лишним.
Например:
client.publish({
destination: '/queue/data',
body: JSON.stringify(data)
});
Слишком длинные пути увеличивают каждый фрейм.
Плохо:
destination:/application/production/europe/chat/messages/general
Лучше:
destination:/chat/gen
На больших объёмах трафика это даёт заметную экономию.
Плохо:
{
"userId": 15,
"userName": "alex",
"userStatus": "online",
"userMessage": "hello"
}
Лучше:
{
"id": 15,
"n": "alex",
"s": 1,
"m": "hello"
}
Текстовые значения занимают больше места.
Плохо:
{
"status": "connected"
}
Лучше:
{
"s": 1
}
Где:
| Код | Значение |
|---|---|
| 0 | disconnected |
| 1 | connected |
| 2 | reconnecting |
Нельзя отправлять prettified JSON:
JSON.stringify(data, null, 2)
Нужно:
JSON.stringify(data)
Разница особенно заметна при больших объектах.
Плохо:
{
"temperatureValue": 25
}
Лучше:
{
"t": 25
}
Плохо:
{
"name": "Alex",
"email": null,
"phone": null
}
Лучше:
{
"name": "Alex"
}
Иногда вместо JSON выгоднее использовать компактные строковые протоколы.
JSON:
{
"x": 10,
"y": 20,
"z": 30
}
Компактный формат:
10|20|30
Base64 увеличивает размер данных примерно на 33%.
Плохо:
body: btoa(binaryData)
Если брокер и транспорт поддерживают бинарные данные, лучше использовать бинарную передачу напрямую.
MessagePack значительно компактнее JSON.
Пример:
import msgpack from '@msgpack/msgpack';
const binary = msgpack.encode(data);
client.publish({
destination: '/topic/bin',
binaryBody: binary
});
Преимущества:
Плохо:
{
"id": 10,
"name": "Alex",
"online": true,
"score": 1500,
"rank": 12
}
Если изменился только score:
{
"id": 10,
"score": 1501
}
В real-time системах выгодно отправлять изменения:
{
"dx": 2,
"dy": -1
}
вместо полного состояния:
{
"x": 1052,
"y": 440
}
Вместо:
send(event1);
send(event2);
send(event3);
лучше:
send([event1, event2, event3]);
Это уменьшает:
Heartbeat создают постоянный служебный трафик.
Плохо:
heartbeatIncoming: 1000,
heartbeatOutgoing: 1000
Лучше:
heartbeatIncoming: 10000,
heartbeatOutgoing: 10000
или:
heartbeatIncoming: 0,
heartbeatOutgoing: 0
если инфраструктура допускает отключение heartbeat.
При нестабильной сети клиент может генерировать большое число служебных кадров.
Плохо:
reconnectDelay: 100
Лучше:
reconnectDelay: 5000
Избыточные подписки увеличивают трафик.
Плохо:
client.subscribe('/topic/a', cb);
client.subscribe('/topic/b', cb);
client.subscribe('/topic/c', cb);
Лучше:
client.subscribe('/topic/all', cb);
с фильтрацией на стороне клиента.
Плохо:
component.mount();
client.subscribe(...);
component.unmount();
component.mount();
client.subscribe(...);
Это создаёт лишние:
SUBSCRIBE;UNSUBSCRIBE.Лучше использовать пул подписок или shared subscription manager.
Большинство WebSocket-серверов поддерживают компрессию:
permessage-deflate
Это позволяет уменьшать размер STOMP-фреймов автоматически.
Особенно эффективно для:
Компрессия может быть вредна при:
В некоторых случаях сжатие увеличивает latency.
Плохо:
client.subscribe('/queue/data', callback, {
ack: 'client'
});
Каждое сообщение требует ACK-фрейма.
Лучше:
client.subscribe('/queue/data', callback, {
ack: 'auto'
});
если допустима потеря части сообщений.
Некоторые брокеры позволяют подтверждать сразу несколько сообщений.
Это снижает количество:
Меньшие сообщения:
Крупные фреймы могут:
Компактные сообщения проходят через сеть эффективнее.
Меньшие фреймы:
Неоптимально:
client.publish({
destination: '/application/production/chat/general',
headers: {
'content-type': 'application/json',
'x-app-version': '1.5.0',
'x-platform': 'web'
},
body: JSON.stringify({
userIdentifier: 100,
userName: 'Alex',
userCurrentStatus: 'online',
userMessageText: 'Hello world'
}, null, 2)
});
Оптимизированный вариант:
client.publish({
destination: '/c/g',
body: JSON.stringify({
i: 100,
n: 'Alex',
s: 1,
m: 'Hello world'
})
});
destination:/application/production/chat/general
content-type:application/json
x-app-version:1.5.0
x-platform:web
{
"userIdentifier":100,
"userName":"Alex",
"userCurrentStatus":"online",
"userMessageText":"Hello world"
}
Размер может превышать 250 байт.
destination:/c/g
{"i":100,"n":"Alex","s":1,"m":"Hello world"}
Размер — менее 70 байт.
Экономия:
Максимальный эффект достигается при сочетании:
Слишком агрессивное сокращение ухудшает поддержку кода.
Плохо:
{
"a": 1,
"b": 2,
"c": 3
}
Без документации такие структуры быстро становятся нечитаемыми.
Компрессия 20-байтных сообщений может:
Чрезмерный batch приводит к:
Плохо:
JSON.stringify(JSON.stringify(data))
Это:
Можно заранее договориться:
| Ключ | Поле |
|---|---|
| i | id |
| n | name |
| s | status |
| t | timestamp |
Это позволяет резко уменьшить payload.
При компактных схемах важно хранить версию:
{
"v": 2,
"d": {...}
}
Иначе изменение структуры приведёт к несовместимости клиентов.
Минифицированные структуры требуют декодирования:
function decode(data) {
return {
id: data.i,
name: data.n,
status: data.s
};
}
Это сохраняет читаемость внутреннего API приложения.
Плохо:
client.debug = console.log;
Логирование:
Лучше:
client.debug = () => {};
Плохо:
client.publish({
destination: '/topic/data',
body: JSON.stringify(createNewObject())
});
Лучше использовать object pooling при экстремальных нагрузках.
Избыточная конкатенация строк:
body: '{ "x": ' + x + ', "y": ' + y + '}'
может создавать лишние временные объекты.
Предпочтительнее:
body: JSON.stringify({ x, y })
Для оценки минимизации обычно измеряют:
| Метрика | Значение |
|---|---|
| bytes per message | размер сообщения |
| messages per second | throughput |
| latency | задержка |
| GC pauses | паузы сборщика |
| bandwidth usage | расход сети |
| reconnect frequency | стабильность |
| broker CPU usage | нагрузка брокера |
Используются:
Причина — экстремальные требования к latency.
Часто применяются:
Для устройств с плохой связью критичны:
Компактные STOMP-фреймы: