Проблема: производительность и нагрузка на клиент, когда JSON занимает более 15Mb

"Техдир собрал нас и стал трясти раскаленным телефоном с зависшей заставкой нашего приложения. Такую же сцену ему устроил собственник. На любые наши возражения он говорил, что бэкенд не трогайте, там все идеально."

Что мы сделали Стали судорожно перебирать библиотеки для парсинга JSON. На устройстве мерили не только миллисекунды, но и нагрев при повторяющейся нагрузке, как у пользователя. Зафиксировали следующую картину: BSON с параллельным парсингом ~1900 ms на тестовом объёме; Zippy ~480 ms, но сильнее греет; системный JSONSerialization ~500 ms и спокойнее по температуре. Слепая замена одной библиотекой на другую - не наш вариант. Для крупных доменных моделей переключились на решения на C/C++. Они по-прежнему рулят по быстродействию, но даже их можно и нужно правильно использовать. simdjson – популярная библиотека, но interop с Swift от Apple не стыкуется с многомодульным приложением. А так все было очень быстро – ondemand-разбор в нативном коде и выдача наверх Swift-модели. Взяли yyjson: стали использовать ручной обход узлов (итератор на JSON и разбор по известным ключам). Нет лишней рефлексии и промежуточных Dictionary, все парсится в том порядке, в котором лежит в JSON. Пример: var iterator = yyjson_obj_iter_with(object) var key: UnsafePointer? var value: UnsafePointer? while yyjson_obj_iter_next(&iterator, &key, &value) { switch jsonKey(key) { case .title: title = parseString(value) case .imageURL: imageURL = parseURL(value) case .items: items = parseItems(value) case .unknown: continue } } Такой код менее «красивый» в академическом смысле, чем один try decoder.decode(Response.self, from: data). Но на горячем пути у него меньше неявной работы.

Но и на этом не остановились. Решили уменьшить количество ветвлений в важном коде. На горячих путях (разбор JSON, маппинг полей, мелкие хелперы в моделях) сознательно уменьшали «лесенку» из if/else и опциональных цепочек там, где код вызывается тысячи раз на один ответ. Банально, но ускоряет: - switch по закрытому набору ключей (CodingKeys, @frozen enum для блоков конфига) вместо длинных цепочек сравнений строк. - @inline(__always) на узких функциях в парсере и сетевом клиенте – меньше вызовов, проще компилятору держать горячий путь горячим. Смысл для быстродействия: современные ядра CPU предсказывают ветки. Чем предсказуемее структура кода в цикле по полям 15-мегабайтного ответа, тем меньше pipeline stall. Это не заменяет алгоритмическую сложность, но на объёме сотен тысяч полей в сумме за сессию разница ощутима вместе с другими оптимизациями.

Итог: приложение остаётся отзывчивым на больших данных; меньше жалоб типа «телефон греется» и «после обновления всё тормозит». Это напрямую бьёт по удержанию в consumer-продукте.