javascript

Поколение JSON: цена удобных абстракций и упадок культуры ресурсов

  • среда, 11 марта 2026 г. в 00:00:04
https://habr.com/ru/articles/1007800/

Цена одной строчки

const data = await fetch('/api/dashboard').then(r => r.json());

Безобидная строчка. Одна из миллиардов, написанных сегодня на планете. Студент с курса напишет её на третий день. Сеньор – с закрытыми глазами.

Но знаете ли вы, что эта строчка стоит? Не абстрактно – «ну, это сетевой вызов». А конкретно: в байтах, миллисекундах, рублях и часах дежурства инженера в три часа ночи по Москве.

Давайте разберём один fetch на атомы. Снимем слои абстракций и посмотрим, что на самом деле скрывается за фасадом «просто сетевого вызова».


Запрос уходит

Вы нажали кнопку «Открыть дашборд». Ваш код вызвал fetch('/api/dashboard'). Мозг разработчика на этом заканчивает работу: «данные придут». Но данные не «приходят». Они прорубаются через 7 слоёв инфраструктуры:

Шаг

Что происходит

Чего не видит фронтенд

1

Разрешение DNS

10-100 мс при «холодном старте»

2

TCP-рукопожатие

3 этапа установки связи, ещё ~30 мс

3

TLS-согласование

шифрование HTTPS, ещё ~40 мс

4

API Gateway

авторизация, лимиты, маршрутизация

5

Сервис «Dashboard»

веерная рассылка (fan-out): 4 вызова к другим микросервисам

6

Сериализация в JSON

нагрузка на CPU, блокирует event loop

7

Сжатие gzip и передача

упаковать и отправить

Один фетч – семь сетевых прыжков, три базы данных, две очереди. А на бэкенде ваш «один запрос» превращается в сложную оркестрацию десятка подсистем.

Мы часто об этом не задумываемся – и не потому, что мы плохие специалисты, а потому что абстракция слишком хорошо работает. Кнопка нажимается, данные приходят, а что там под капотом – «не моя зона ответственности».


Ответ приходит (и застревает в «Мосту»)

Вот он, ваш JSON. Представим, что вы пишете на React Native – ведь бизнес хочет «один код на все платформы».

{
  "user": { "id": 42, "name": "Иван", "avatar": "..." },
  "feed": [
    {
      "id": 1001,
      "type": "post",
      "content": { "text": "Как я перестал беспокоиться и полюбил JSON", "attachments": [...] },
      "author": { "name": "Admin", "reputation": 999, "badges": [...] },
      "stats": { "likes": 124, "comments": 42, "shares": 12 },
      "metadata": { "trackingId": "...", "abTest": "feed_v4", "renderHints": { ... } }
    }
    // ... еще 50 тяжелых объектов для бесконечной ленты
  ]
}

Этот объект весит 2.5 МБ. Но в React Native данные не просто «приходят» в память. Чтобы вы увидели этот список на экране, данные должны совершить путешествие через JS Bridge (Мост).

Да, новые версии React Native активно переходят на JSI (JavaScript Interface), избавляясь от асинхронного моста. Но миллионы строк легаси-кода в продакшене всё ещё гоняют JSON через Bridge, а затраты на первичный сетевой парсинг новая архитектура не отменяет.

Представьте два города. В одном (JavaScript) живет логика. В другом (Native) живут кнопки, скроллы и отрисовка. Между ними – узкая однополосная дорога.

Чтобы Native-сторона узнала, что нужно нарисовать карточку поста, JS-сторона должна:

  1. Взять ваш объект.

  2. Превратить его в JSON-строку (JSON.stringify).

  3. Протолкнуть эту строку через «Мост».

  4. Нативная сторона должна распарсить этот текст обратно в объекты.

Абсурд начинается при скролле. Каждое микродвижение пальца генерирует событие. Это событие упаковывается в JSON, летит через мост в JS-слой, рантайм которого вычисляет новую позицию, упаковывает команду в JSON и отправляет обратно.

// Что происходит под капотом при каждом кадре (60 FPS):
Native: {"event": "scroll", "offsetY": 152.5}  --> BRIDGE --> JS
JS: {"command": "updateOffset", "value": 152.5} <-- BRIDGE <-- Native

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


Что происходит после того, как «Мост» раскалился

Три вещи, которые убивают ваш UX, пока вы ждете завершения fetch:

1. Эффект «Зловещей долины»

Вы когда-нибудь замечали, что кроссплатформенные приложения ведут себя «почти» как нативные, но что-то не так? Скролл чуть-чуть опаздывает. Кнопка нажимается с микрозадержкой. Список «заикается» при быстром пролистывании. Это цена сериализации. Когда через Мост летит тяжелый JSON (те самые 2.5 МБ ленты), дорога забита. События нажатия встают в очередь за данными. Результат – заикание интерфейса. Вы потеряли плавность интерфейса, потому что процессор был занят парсингом текста.

2. Батарея тает на глазах

Сериализация – это CPU-bound операция. Смартфон пользователя вынужден молотить гигагерцами, превращая объекты в текст и обратно, просто чтобы вы могли проскроллить список.
Типичный сеанс в "тяжелом" супераппе:

  • -> 40% времени CPU тратится на JS Bridge и парсинг JSON.

  • -> Смартфон греется.

  • -> Контроллер питания снижает частоту (тротлинг), чтобы не расплавиться.

  • -> Приложение начинает тормозить еще сильнее. Мы создали порочный круг, где неэффективность данных убивает производительность железа.

3. Сборщик мусора на сверхурочных

Каждая сериализация порождает горы временных строк. Когда вы проталкиваете 2.5 МБ через мост, движок JS создает огромную строку в памяти. Как только она передана – она больше не нужна. Сборщик мусора (GC) вынужден просыпаться каждые несколько секунд, чтобы вымести эти мегабайты «текстовых трупов». Каждое пробуждение – это микрофриз. Каждое «заикание» – это минус в карму вашего UX.


Экономика невидимого мусора

Мы привыкли считать, что байты бесплатны. Но давайте посчитаем «на пальцах» для среднего проекта в РФ.

Представьте, что у вас 15 000 активных пользователей в день – масштаб крепкого нишевого сервиса, региональной доставки или внутреннего корпоративного приложения. Каждый делает в среднем по 15 «тяжелых» запросов к API (лента, дашборд, детализация). В каждом запросе – всего 300 КБ «жира» (лишние поля, дубликаты метаданных, неэффективные вложенности).

  • Лишний трафик: 15 000 * 15 * 0.3 МБ = ~67.5 ГБ лишнего трафика в сутки.

  • Итог за месяц: около 2 ТБ «воздуха», переданного по сети.

  • Цена в облаке: в условном Yandex Cloud или Selectel стоимость исходящего трафика (сверх бесплатных лимитов) составляет около 1.5 - 2 рублей за ГБ. Выходит 3 000 - 4 000 рублей в месяц вы просто дарите провайдеру за передачу метаданных, которые никто никогда не увидит.

Кажется, немного? Но это цена аренды одного среднего сервера или пары лицензий на софт, которые вы просто сжигаете. А теперь добавьте сюда:

  1. Скрытые расходы: лишние ядра CPU на бэкенде для сериализации этого «жира» и на фронтенде для его парсинга.

  2. Стоимость хранения: если этот мусор хранится в логах или кэшах Redis, вы платите за него дважды.

Но самая высокая цена – это потеря лояльности. Если ваше приложение заикается на бюджетном Android-смартфоне из-за того, что парсинг JSON забил event loop, пользователь просто уйдет. И этот убыток не покажет ни один биллинг облака.


Феномен «Поколения JSON»

Всё, что описано выше – не баг. Это архитектурный стандарт. И это тип мышления, который я называю «Поколение JSON». Не возрастная группа и не ругательство, а скорее профессиональное состояние.

Я вижу три характерных черты:

  1. Слепота к весу: «это же просто поле. Текст лёгкий, правда?»

  2. Чёрный ящик: бэкенд – волшебная труба. Дёрнул fetch – пришёл JSON. Что внутри трубы – не моё дело.

  3. Иллюзия бесконечных ресурсов: «серверы масштабируются. Kubernetes разберётся. CDN закэширует».

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


Откуда берётся это мышление: три ловушки

Мы все через это проходили. Это не злой умысел, а система, в которой мы работаем.

1. Ловушка курсов. Типичный курс «Веб-разработчик за 6 месяцев» учит fetch и useState, но часто не объясняет, что такое TCP, как работает GC и почему .filter().map().reduce() – это три прохода по массиву вместо одного. Курсы часто делают операторов фреймворков, а не инженеров. Профильное IT-образование не панацея, но оно даёт базу: сложность алгоритмов, стек, куча.

2. Ловушка рынка. Product Manager получает бонус за новую фичу – не за то, что API стал отдавать на 40% меньше данных. Оптимизация невидима для менеджмента. Она не помещается на красивый слайд. Она не двигает DAU напрямую. До тех пор, пока приложение не начинает «умирать» под собственным весом.

3. Ловушка абстракций. React, Vue, Next.js – прекрасно справляются с задачей: спрятать от разработчика всё, что происходит под капотом. И любопытство часто засыпает, потому что абстракция работает. Зачем лезть внутрь, если кнопка нажимается? Но когда абстракция ломается – а она ломается в самый неподходящий момент – ты стоишь перед чёрным ящиком, зная только где педаль газа.


Личный кейс: как я «убил» сервер архитектурой фронтендера

Когда-то я строил бэкенд для одного застройщика. Будучи опытным фронтендером, я рассуждал просто: «Бэкенд – это та же логика на JS, только вместо window у нас fs, а вместо кнопок – API».

Подробно этот эпик-фейл я разбирал в своей первой статье «Как я строил бэкенд с ментальностью фронтендера», но сегодня я хочу взглянуть на него под другим углом – через призму избыточности данных.

Я выбрал связку Strapi + MongoDB. Почему? Потому что JSON – это же «просто объекты», а Mongo – это «просто хранилище объектов». Идеальное совпадение ментальных моделей!

Я спроектировал базу данных как зеркало фронтенд-компонентов. Нужен блок «Похожие квартиры»? Окей, создаем вложенную структуру: Page -> Section -> Apartment -> Building -> District. На фронтенде это выглядит как элегантное дерево пропсов. В JSON-ответе это – красивые фигурные скобки.

Но реальность постучала в дверь при 50 пользователях онлайн.

Чтобы собрать этот «красивый JSON» для одной страницы, база данных выполняла рекурсивные джойны (populate) глубиной в 6 уровней. Сервер захлебывался в попытках склеить множество объектов в одну текстовую простыню.

  • Результат: TTFB (время до первого байта) – 7 секунд.

  • Решение «Поколения JSON»: я не пошел переделывать схему данных (это же долго и скучно). Я просто вкрутил Redis для кэширования всего ответа.

Проблема не решилась, она просто «спряталась» за кэшем. Первый пользователь после очистки кэша всё еще ждал 7 секунд, а мой JSON-монстр продолжал пожирать ресурсы CPU просто на этапе сборки строки. Это и есть ловушка: когда ты видишь в данных только «удобный формат», ты перестаешь видеть за ними дисковые операции, индексы и реальную нагрузку на железо.


Историческая справка: 128 КБ в шторм

15 ноября 1988 года. Космодром Байконур. Штормовой боковой ветер, порывы до 20 м/с. Видимость – нулевая.

Орбитальный корабль «Буран» совершает полностью автоматическую посадку. Без пилота. Бортовой компьютер «Бисер-4»: 370 тысяч операций в секунду, 128 КБ оперативной памяти. Даже ваш умный чайник на кухне мощнее.

Автоматика самостоятельно скорректировала траекторию из-за ветра и посадила 80-тонную махину на скорости 300 км/ч с отклонением менее 5 метров от осевой линии – при полосе шириной 80 метров. Ювелирная точность для 128 килобайт.

128 КБ. Шторм. Автопосадка из космоса.

А сегодня устройство с нейродвижком на 35 триллионов операций в секунду и гигабайтами оперативки тормозит при открытии списка контактов.


Наследие избыточности

Сравнение с «Бураном» нечестное? Безусловно. Современный UI на порядок сложнее текстового терминала. Но контраст подсвечивает главное: мы перестали уважать ресурсы. Те инженеры считали каждый байт, потому что были вынуждены. Мы перестали – потому что решили, что ресурсы бесконечны.

Разница между 1988-м и 2026-м не в том, что мы стали глупее. Мы стали богаче – и это нас расслабило. Мы покупаем железо, чтобы компенсировать лень в архитектуре. Но железо не бесконечно: закон Мура замедляется, а требования к софту растут экспоненциально.

Мы не должны возвращаться к 128 КБ. Но, кажется, нам пора вернуть себе тот уровень инженерной ответственности, который был у создателей «Бисера-4». Инженерия – это не выбор библиотеки. Это понимание того, какую цену платит система за каждое ваше решение.

JSON – отличный формат. Зло – мышление, в котором данные невесомы.


Рецепт: 5 вопросов перед каждым fetch

Я не предлагаю возвращаться к ассемблеру или бинарным протоколам (хотя gRPC – отличный инструмент). Я предлагаю начать с гигиены мышления:

#

Вопрос

Смысл

1

Сколько весит ответ?

DevTools -> Network -> Size. 10 секунд вашего времени сэкономят гигабайты пользователю.

2

Всё ли из этого я рисую?

Если используете 3 поля из 40 – запрашивайте 3. GraphQL, Sparse Fields или отдельные эндпоинты – это норма.

3

Сколько раз в секунду это происходит?

Цена поля = байты x RPS x 86,400 сек. Одно лишнее поле при большой нагрузке превращается в десятки ГБ/день.

4

Кто ещё потребляет этот эндпоинт?

Если мобилка и веб бьют в один API – один из них точно получает лишний мусор.

5

Как приложение переживёт «битый» ответ?

Что если JSON придет неполным? Упадет ли весь экран или только один блок? Изоляция ошибок в данных – признак взрослой архитектуры.


Эпилог

Поколение JSON – не приговор. Это привычка. И она меняется не новым фреймворком, не курсом по Rust и не миграцией на gRPC. Она меняется привычкой думать о том, что происходит за пределами фигурных скобок.

Сложность систем будет только расти. JSON со временем сменится чем-то другим, а ИИ будет писать за нас тысячи строк кода в секунду. Но разница между «оператором фреймворка» и инженером останется прежней: инженер знает, какую цену платит пользователь за каждый байт.

В следующий раз, когда напишете fetch(), откройте вкладку Network. Посмотрите на размер ответа. Посчитайте, сколько из этого реально нужно. Если он оправдан – это прогресс. Если нет – это просто шум, за который платит ваш пользователь.

Давайте перестанем быть просто перекладчиками данных и вспомним, что мы, прежде всего, инженеры.


Этот текст – часть философии книги «Поколение JSON», где я препарирую всё, что мы сломали в IT: от раздутого софта и корпоративных ритуалов до npm-зависимостей и фейковой архитектуры.

Если описанные выше симптомы показались вам знакомыми – загляните в мой Telegram-канал. В закрепе лежит бесплатный PDF-чек-лист «5 вопросов к вашему API, которые никто не задаёт»: вес ответа, мусорные поля, стоимость сериализации. Проверьте свой API, пока он ещё влезает в оперативку.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
А как часто вы заглядываете в размер ответа в Network tab?
25%Проверяю каждый запрос, борюсь за каждый байт12
54.17%Смотрю только если начинает явно тормозит26
0%Доверяю бэкенду/фреймворку, там всё оптимизировано0
20.83%У пользователя безлимит и новый iPhone, какая разница?10
Проголосовали 48 пользователей. Воздержались 9 пользователей.