Привет!
Недавно в рамках одного из проектов на стеке KMP, Ktor и Kotlin Serialization мы с командой решили провести эксперимент и определить возможность и целесобразность минификации тел запросов / ответов на Json.
Да, мы знаем про GraphQL, Protobuf и др., но в нашем случае имел место необузданный интерес наколхозить такое решение. И при всей его наивности удалось сократить средний размер итоговых джсонов (после всех внутренних оптимизаций) на 15–20%.
Вводные данные:
Большое приложение на KMP с таргетами iOS, Android, Web и Desktop;
Фронтенд и бэкенд написаны на Ktor и швыряются Json'ами по HTTP;
Монорепа и по сути один KMP проект.
Структура проекта:
frontend‑app — модуль с реализацией фронтенда на KMP;
backend‑app — модуль с реализацией бэкенда на Kotlin + Ktor.
http‑model — модуль с моделями, общими между бэкендом и фронтендом; чистый Kotlin и Kotlin Serialization.
Задача
У нас есть списочный Json, который весит 4 754 байт. По сути единственной сущностью списка является следующий объект:
{
"interestId": "f5092d67-1ba7-4e7a-8eed-75ba2726c242",
"title": "Антенны дальнего действия",
"imageUrl": null,
"category": {
"interestCategoryId": "6ac16b9f-9d2b-4bd4-b2aa-a5d35d727ecd",
"title": "Аналог",
"interestCategoryOrder": 0
},
"interestOrder": 0
}
Объект и его структура (включая ключи) дублируются n кол‑во раз, где n — размер списка, что логично для Json формата.
А вот наша модель Interest, общая между бэкендом и фронтендом, лежащая в модуле common‑model:
@Serializable
data class Interest(
val interestId: Uuid,
val title: String,
val imageUrl: String?,
val category: InterestCategory,
val interestOrder: Int
)
Повторюсь, модель общая, т. е. бэкенд и фронтенд сериализуют и десериализуют эту модель по идентичным правилам. Кроме того, в нашем кейсе модель даже находится в рамках одного проекта.
Рождается предположение: «А зачем нам сохранять читаемость Json? Для кого?»
Соответственно, обновленная модель:
@Serializable
data class Interest(
@SerialName("iid")
val interestId: Uuid,
@SerialName("t")
val title: String,
@SerialName("iu")
val imageUrl: String?,
@SerialName("c")
val category: InterestCategory, // В InterestCategory аналогично
@SerialName("io")
val interestOrder: Int
)
Обновленный объект Json:
{
"iid": "f5092d67-1ba7-4e7a-8eed-75ba2726c242",
"t": "Антенны дальнего действия",
"iu": null,
"c": {
"ici": "6ac16b9f-9d2b-4bd4-b2aa-a5d35d727ecd",
"t": "Аналог",
"ico": 0
},
"io": 0
}
Итоговый вес изначального Json'а с минифицированным объектом — 3 890 байт, т. е. ~ 80% от исходного. 20% веса банально занимал нейминг ключей. А ведь зависимость ~ O(nk), где n — размер списка, а k — вложенность объекта списка.
Какие могут быть проблемы?
Единственное за чем необходимо было следить, чтобы в рамках одной модели не было двух идентичных значений @SerialName.
При грамотном версионировании и пряморуком деливери, никаких других проблем не будет.
Использовали бы мы это в проде?
Уже использовали в рамках того же проекта. Работает хорошо, на метрики повлияло в позитивном ключе. В каких‑то местах менее существенно, в каких‑то ощутимо.
Для нас это был безболезненный и дешёвый вариант: на всё про всё ушла ~ 1 человеко‑ночь под пивом, в то время как банальная миграция на Protobuf или GraphQL была бы на порядок сложнее, запутаннее и менее привлекательной для заказчиков.
Внимание!
С нашим мультиплатформенным стеком это действительно было неплохим решеним, однако если, скажем, Ваш проект состоит из нескольких команд: веба на JS, отдельно нативных мобилок и отдельно бэка на, например, Java, нужно серьёзно задуматься.
Стоят ли эти 15–20% прироста производительности написания очень качественной документации и спецификации и постоянного синка нечитаемого нейминга в случае каких‑то изменений?
Моё мнение — не стоит. Зависит от масштаба проекта, команды и радикальности руководства.
Комментарии (10)
FireWind
02.08.2025 20:26А потом меняется команда разработчиков и следующие проклинают предыдущих за "оптимизацию" кода
akozlovskiy Автор
02.08.2025 20:26Почему? Документация есть, бас фактор в пределах нормы. Если поменялась вся команда разработчиков, аналитиков, РП в момент и не осталось ни одной наскальной надписи о применяющемся решении, то это уже сюр. У меня, к счастью, такого опыта не было)
А так это никому не мешает, сами джсоны на проде мы на корректность не дебажим, да и команда на этом проекте небольшая и все знали об эксперименте)
Evgenij_Popovich
02.08.2025 20:26QA будуть "в восторге" от такой оптимизации. Отладка каких-то багов с некорректной обработкой АПИ вызовов тоже затрудняется. А gzip вам не подошёл?
Djaler
Для удобного дебага, чтения логов и т.д.
Статья про то, что в Kotlin Serialization есть аннотация, позволяющая управлять именем сериализуемого свойства? Как и у любой другой библиотеки для работы с JSON?
akozlovskiy Автор
Спасибо за комментарий)
Мы логи на сервере пишем после десериализации запросов и до сериализации ответов, и соответственно в них всё чистенько и читаемо, т. к. пишем по неймингу пропертей, а не полей в джсоне. Зачем дебажить сам джсон на корректность сериализации/десериализации — не знаю. С таким же успехом можно подебажить протокол HTTP. А если идея с помощью, например, чарлика подменять запросы, то для таких вещей есть дев и стейдж окружения, где минификации нет)
И нет, статья не про аннотацию, а про экспериментальный, не самый очевидный вариант её использования, который может быть не жизнеспособен в других обстоятельствах. Если вы где-то ещё видели такой вариант использования — буду рад ознакомиться и понять, что не только мы такие альтернативно одарённые. В целом статья больше для настроения, а для понимания аннотации можно почитать документацию.
panzerfaust
API Binance, Bybit и т.д. В теории любое приложение с публичным WS API и мощным потоком данных.
Так смотря о чем речь. Биржевые или финансовые данные сохраняют формат десятилетиями. Почему б и не оптимизировать? Особенно когда ты их стримишь по 10к в секунду. А волатильные форматы не стоит, конечно.
akozlovskiy Автор
Да, в таких кейсах, конечно, есть смысл использовать, как в статье написано, зависит от кейса и команды. Мне кажется, там нужно что-то постабильнее
Djaler
А ещё логи бывают вне приложения. Например, на балансерах/гейтвеях.
У вас замена имен у свойств при сериализации вшита в описание самих структур. Каким образом это отключается в разных окружениях?
Кажется, вам нужно было просто включить сжатие ответов и всё https://docs.spring.io/spring-boot/how-to/webserver.html#howto.webserver.enable-response-compression
akozlovskiy Автор
В статье есть блок в конце, обратите на него внимание, пожалуйста. Это решение даже не рассматривается в проектах с балансерами и т.
Отключается с помощью настроек конфигурации Kotlin Serialization. Плюс, в статье демо пример, на самом деле всё чуточку сложнее. Привести весь листинг кода не было целью.
P. S. Спасибо за статью про сжатие, обязательно почитаем. Только у нас в этом проекте не Spring, а Ktor, но ничего, поменяем стек.