Вы знаете, что такое прогрессивный JPEG? Можете почитать хорошее объяснение. Идея заключается в том, что вместо загрузки изображения сверху вниз оно сначала грузится размытым, а потом постепенно становится чётче.

Что, если мы применим тот же принцип к передаче JSON?

Допустим, у нас есть дерево JSON с какими-то данными:

{
  header: 'Welcome to my blog',
  post: {
    content: 'This is my article',
    comments: [
      'First comment',
      'Second comment',
      // ...
    ]
  },
  footer: 'Hope you like it'
}

А теперь представьте, что нам нужно передать его по сети. Так как это формат JSON, у нас не будет валидного дерева, пока не загрузится последний байт. Нам придётся ждать, пока загрузится весь файл, затем вызвать JSON.parse, и уже потом обрабатывать его.

Клиент не сможет ничего сделать с JSON, пока сервер не отправит последний байт. Если часть JSON сервер генерирует медленно (например, для загрузки всех comment потребуется долгое обращение к базе данных), то клиент не сможет начать никакую работу, прежде чем сервер не закончит всю работу.

Разве это хорошая архитектура? Тем не менее, таков статус-кво — именно так 99,9999%* приложений отправляет и обрабатывает JSON. Осмелимся ли мы улучшить ситуацию?

* Статистику я выдумал


Потоковая передача JSON

Можно попробовать оптимизировать работу, реализовав потоковый парсер JSON. Потоковый парсер JSON будет способен создать дерево на основании неполных данных:

{
  header: 'Welcome to my blog',
  post: {
    content: 'This is my article',
    comments: [
      'First comment',
      'Second comment'

Если мы запросим результат на этом этапе, то потоковый парсер передаст нам следующее:

{
  header: 'Welcome to my blog',
  post: {
    content: 'This is my article',
    comments: [
      'First comment',
      'Second comment'
      // (Остальная часть комментариев отсутствует)
    ]
  }
  // (Отсутствует свойство footer)
}

Однако это тоже не так уж здорово.

Один из недостатков такого подхода заключается в том, что объекты формируются неверно. Например, объект верхнего уровня должен был иметь три свойства (headerpost и footer), но footer отсутствует, потому что пока не встречался в потоке. Свойство post должно было содержать три comment, но мы не можем определить, поступят ли новые comment или этот был последним.

В каком-то смысле, это неотъемлемое свойство потоковой передачи — разве мы не стремились получить неполные данные? — но это сильно усложняет использование данных в клиенте. Ни один из типов не «согласуется» из-за отсутствующих полей. Мы не знаем, что загрузилось полностью, а что нет. Именно поэтому потоковая передача JSON популярна только в нишевых ситуациях. Слишком сложно воспользоваться преимуществами такого подхода в логике приложения, которая обычно предполагает, что типы корректны, «готово» означает «завершено» и так далее.

В аналогии с JPEG такой наивный подход к потоковой передаче соответствует механизму загрузки сверху вниз. Вы видите чёткое изображение, но только только одну десятую его часть сверху. Поэтому, несмотря на высокое качество, вы не видите, что же находится на изображении.

Любопытно, что по умолчанию именно так работает сам потоковый HTML. При загрузке HTML-страницы по медленному каналу она будет потоково передаваться в порядке документа:

<html>
  <body>
    <header>Welcome to my blog</header>
    <article>
      <p>This is my article</p>
        <ul class="comments">
          <li>First comment</li>
          <li>Second comment</li>

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

Повторим: при потоковой передаче элементов по порядку единственный медленный элемент откладывает загрузку всего, что идёт после него. Можно ли придумать какой-то способ решения этой проблемы?


Прогрессивный JSON

Потоковую передачу можно реализовать иначе.

Ранее мы передавали элементы с сортировкой по глубине. Мы начинали со свойств объекта верхнего уровня, затем заходили в свойство post этого объекта, затем в свойство comments уже этого объекта и так далее. Если что-то оказывается медленным, то приостанавливается всё остальное.

Однако мы можем передавать данные и с сортировкой по ширине.

Допустим, мы отправляем объект верхнего уровня следующим образом:

{
  header: "$1",
  post: "$2",
  footer: "$3"
}

Здесь "$1""$2""$3" обозначают элементы информации, которые ещё не были переданы. Это подстановочные символы, которые можно прогрессивно заменять позже по потоку.

Например, предположим, что сервер отправляет в поток ещё несколько строк данных:

{
  header: "$1",
  post: "$2",
  footer: "$3"
}
/* $1 */
"Welcome to my blog"
/* $3 */
"Hope you like it"

Обратите внимание, что мы не обязаны отправлять строки в каком-то конкретном порядке. В показанном выше примере мы передали лишь строки $1 и $3, но строка $2 по-прежнему ожидается!

Если бы клиент попробовал воссоздать дерево на этом этапе, оно могло бы выглядеть так:

{
  header: "Welcome to my blog",
  post: new Promise(/* ... not yet resolved ... */),
  footer: "Hope you like it"
}

Представим ещё не загруженные части в виде Promise.

Теперь представим, что сервер смог передать ещё несколько строк:

{
  header: "$1",
  post: "$2",
  footer: "$3"
}
/* $1 */
"Welcome to my blog"
/* $3 */
"Hope you like it"
/* $2 */
{
  content: "$4",
  comments: "$5"
}
/* $4 */
"This is my article"

С точки зрения клиента они заполнят недостающие элементы:

{
  header: "Welcome to my blog",
  post: {
    content: "This is my article",
    comments: new Promise(/* ... not yet resolved ... */),
  },
  footer: "Hope you like it"
}

Promise для post теперь ресолвит объект. Однако мы всё ещё не знаем, что находится внутри comments, поэтому теперь уже они представлены в виде Promise.

Далее загружаются и комментарии:

{
  header: "$1",
  post: "$2",
  footer: "$3"
}
/* $1 */
"Welcome to my blog"
/* $3 */
"Hope you like it"
/* $2 */
{
  content: "$4",
  comments: "$5"
}
/* $4 */
"This is my article"
/* $5 */
["$6", "$7", "$8"]
/* $6 */
"This is the first comment"
/* $7 */
"This is the second comment"
/* $8 */
"This is the third comment"

Теперь с точки зрения клиента готово всё дерево:

{
  header: "Welcome to my blog",
  post: {
    content: "This is my article",
    comments: [
      "This is the first comment",
      "This is the second comment",
      "This is the third comment"
    ]
  },
  footer: "Hope you like it"
}

Благодаря отправке данных блоками с сортировкой по ширине мы получили возможность прогрессивно обрабатывать их в клиенте. Если клиент может справляться с тем, что некоторые части ещё «не готовы» (представлены в виде Promise), и обрабатывать остальные, то мы таким образом улучшим ситуацию!


Встраивание

Теперь, когда у нас есть базовый механизм, можно оптимизировать его для более эффективного вывода. Давайте ещё раз взглянем на всю последовательность потоковой передачи из предыдущего примера:

{
  header: "$1",
  post: "$2",
  footer: "$3"
}
/* $1 */
"Welcome to my blog"
/* $3 */
"Hope you like it"
/* $2 */
{
  content: "$4",
  comments: "$5"
}
/* $4 */
"This is my article"
/* $5 */
["$6", "$7", "$8"]
/* $6 */
"This is the first comment"
/* $7 */
"This is the second comment"
/* $8 */
"This is the third comment"

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

Допустим, у нас есть две медленные операции: загрузка поста и загрузка комментариев к нему. В этом случае логично будет отправлять три блока.

Сначала мы отправим внешнюю оболочку:

{
  header: "Welcome to my blog",
  post: "$1",
  footer: "Hope you like it"
}

В клиенте она сразу же примет следующий вид:

{
  header: "Welcome to my blog",
  post: new Promise(/* ... not yet resolved ... */),
  footer: "Hope you like it"
}

Затем мы отправим данные post (но без comments):

{
  header: "Welcome to my blog",
  post: "$1",
  footer: "Hope you like it"
}
/* $1 */
{
  content: "This is my article",
  comments: "$2"
}

С точки зрения клиента:

{
  header: "Welcome to my blog",
  post: {
    content: "This is my article",
    comments: new Promise(/* ... not yet resolved ... */),
  },
  footer: "Hope you like it"
}

Наконец, мы одним блоком отправляем комментарии:

{
  header: "Welcome to my blog",
  post: "$1",
  footer: "Hope you like it"
}
/* $1 */
{
  content: "This is my article",
  comments: "$2"
}
/* $2 */
[
  "This is the first comment",
  "This is the second comment",
  "This is the third comment"
]

Таким образом мы создадим в клиенте полное дерево:

{
  header: "Welcome to my blog",
  post: {
    content: "This is my article",
    comments: [
      "This is the first comment",
      "This is the second comment",
      "This is the third comment"
    ]
  },
  footer: "Hope you like it"
}

Это более компактное решение, реализующее ту же цель.

В целом, этот формат даёт нам свободу решать, когда отправлять элементы в одном или в нескольких блоках. Если у клиента не возникает проблем с получением блоков не по порядку, сервер может выбирать различные эвристики разбиения на группы и блоки.


Вынесение

Любопытное следствие из применения этой методики заключается в том, что мы естественным образом можем снизить повторяемость в выходном потоке. Если мы сериализуем объект, который уже встречали ранее, то можем просто вынести его в отдельную строку и использовать повторно.

Например, пусть у нас есть такое дерево объекта:

const userInfo = { name: 'Dan' };
 
[
  { type: 'header', user: userInfo },
  { type: 'sidebar', user: userInfo },
  { type: 'footer', user: userInfo }
]

Если бы мы сериализовали обычный JSON, то нам бы пришлось повторять { name: 'Dan' }:

[
  { type: 'header', user: { name: 'Dan' } },
  { type: 'sidebar', user: { name: 'Dan' } },
  { type: 'footer', user: { name: 'Dan' } }
]

Однако если мы передаём JSON прогрессивно, то можем решить вынести его:

[
  { type: 'header', user: "$1" },
  { type: 'sidebar', user: "$1" },
  { type: 'footer', user: "$1" }
]
/* $1 */
{ name: "Dan" }

Также можно попробовать реализовать более сбалансированную стратегию — например, чтобы по умолчанию объекты встраивались (ради компактности), пока мы не встретим повторное многократное использование какого-то объекта. В этом случае мы отправим его отдельно и устраним его дублирование в остальной части потока.

Это означает ещё и то, что в отличие от обычного JSON, мы можем обеспечить поддержку сериализации цикличных объектов. Цикличный объект просто будет иметь свойство, указывающее на его собственную «строку» в потоке.


Потоковая передача данных и потоковая передача UI

По сути, по описанной выше схеме работают React Server Components.

Допустим, мы пишем страницу с React Server Components:

function Page() {
  return (
    <html>
      <body>
        <header>Welcome to my blog</header>
        <Post />
        <footer>Hope you like it</footer>
      </body>
    </html>
  );
}
 
async function Post() {
  const post = await loadPost();
  return (
    <article>
      <p>{post.text}</p>
      <Comments />
    </article>
  );
}
 
async function Comments() {
  const comments = await loadComments();
  return <ul>{comments.map(c => <li key={c.id}>{c.text}</li>)}</ul>;
}

React будет передавать вывод Page в виде потока прогрессивного JSON. В клиенте он будет воссоздаваться в виде прогрессивно загружаемого дерева React.

Изначально дерево React в клиенте будет выглядеть так:

<html>
  <body>
    <header>Welcome to my blog</header>
    {new Promise(/* ... not resolved yet */)}
    <footer>Hope you like it</footer>
  </body>
</html>

Затем, когда на сервере произойдёт ресолвинг loadPost, выполнится дополнительная потоковая передача:

<html>
  <body>
    <header>Welcome to my blog</header>
    <article>
      <p>This is my post</p>
      {new Promise(/* ... not resolved yet */)}
    </article>
    <footer>Hope you like it</footer>
  </body>
</html>

Далее, когда на сервере произойдёт ресолвинг loadComments, клиент получит остальное:

<html>
  <body>
    <header>Welcome to my blog</header>
    <article>
      <p>This is my post</p>
      <ul>
        <li key="1">This is the first comment</li>
        <li key="2">This is the second comment</li>
        <li key="3">This is the third comment</li>
      </ul>
    </article>
    <footer>Hope you like it</footer>
  </body>
</html>

Однако здесь есть одна тонкость.

Нам не нужно, чтобы страница произвольно прыгала в процессе потокового получения данных. Например, возможно, вам не нужно, чтобы страница вообще отображалась без содержимого поста.

Именно поэтому React не отображает «дыры» вместо ожидаемых Promise. Вместо этого он отображает наиболее близкое декларативное состояние загрузки, обозначенное как <Suspense>.

В показанном выше примере в дереве нет границ <Suspense>. Это означает, что хотя React будет получать данные в виде потока, он не будет отображать пользователю «скачущую» страницу. Он дождётся полной загрузки страницы.

Однако можно выбрать прогрессивно отображаемое загружаемое состояние, обернув часть дерева UI в <Suspense>. Это не изменит способ передачи данных (они всё равно будут передаваться максимально «потоково»), но изменит время отображения их React пользователю.

Пример:

import { Suspense } from 'react';
 
function Page() {
  return (
    <html>
      <body>
        <header>Welcome to my blog</header>
        <Post />
        <footer>Hope you like it</footer>
      </body>
    </html>
  );
}
 
async function Post() {
  const post = await loadPost();
  return (
    <article>
      <p>{post.text}</p>
      <Suspense fallback={<CommentsGlimmer />}>
        <Comments />
      </Suspense>
    </article>
  );
}
 
async function Comments() {
  const comments = await loadComments();
  return <ul>{comments.map(c => <li key={c.id}>{c.text}</li>)}</ul>;
}

Теперь с точки зрения пользователя последовательность загрузки будет состоять из двух этапов:

  • Сначала покажется пост с заголовком, сносками и заглушкой вместо комментариев. Заголовок и сноски никогда не отображаются сами по себе.

  • Затем отдельно покажутся комментарии.

Иными словами, этапы отображения UI отвязаны от процесса поступления данных. Данные потоково передаются тогда, когда становятся доступными, но отображаются элементы пользователю только согласно явно выбранным состояниям загрузки.

В каком-то смысле эти Promise в дереве React работают почти как throw, а <Suspense> — почти как catch. Данные поступают с максимально возможной для них скоростью и в том порядке, в котором сервер готов их отправлять, но React сам занимается правильным представлением последовательности загрузки, позволяя разработчику управлять визуальным отображением.

Всё описанное выше никак не связано с SSR и HTML. Я описал общий механизм потоковой передачи дерева UI, представленного в виде JSON. Можно превратить это дерево JSON в прогрессивно отображаемый HTML (и React способен на это), но этот принцип шире, чем HTML и применим также к навигации в духе SPA.


В заключение

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

Я бы хотел призвать разработчиков других инструментов реализовывать прогрессивную потоковую передачу данных. Если у вас возникла ситуация, когда вы не можете начать что-то делать в клиенте, пока сервер не закончит что-то делать, то это как раз пример того, в чём вам может помочь потоковая передача. Если что-то одно может замедлить всё после него, то это ещё один предупреждающий признак.

Как я показал в этом посте, самой по себе потоковой передачи недостаточно — вам ещё нужна модель разработки программ, способная воспользоваться преимуществами потоковой передачи и без проблем обрабатывать неполную информацию. React решает эту проблему благодаря явным состояниям загрузки <Suspense>. Если вам знакомы системы, решающие эту задачу иначе, то напишите мне о них!

Комментарии (32)


  1. Dhwtj
    04.06.2025 13:48

    почему бы не пересылать несколько раздельных json?

    гидратация так и делается, не?


  1. pnmv
    04.06.2025 13:48

    выше, пишут про гидратацию, а я подумал о линеаризации данных - преобразовании сложных структур в простую монотонную простыню.

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

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

    а что такого? фронтенд справится. калькуляторы, у пользователей, теперича, очень мощные стали, и по большей части простаивают. интернет-каналы тоже толстые. так что...


    1. ivan386
      04.06.2025 13:48

      Простыня при недогрузе будет грузится заново после обновления страницы и при медленном и нестабильном соединении обрываться в разных местах. А когда json поделен на несколько небольших частей будут загружаться только те которые не попали в кэш браузера.


      1. pnmv
        04.06.2025 13:48

        понятно, что мы о разных сценариях работы. я имел в виду не критику такого подхода, а более хороший вариант - перейти на что-нибудь, не вида json/xml.


  1. Akuma
    04.06.2025 13:48

    Очень сложно.

    Просто отправляйте отдельные элементы по SSE и всё тут. 100% поддержка чем угодно, скорость, никакой магии и выдумок.


  1. Fragster
    04.06.2025 13:48

    Все не читал, но в некоторых языках есть инструменты именно что потокового чтения (как раз чтобы при загрузке гигабайтных json не упасть по ограничению памяти и не дожидаться окончательного чтения по сети), полностью аналогичные чтению XML. При этом объекты получить можно на любом уровне (например если в корне - массив с миллиародом элементов, то читать по одному объекту массива, для более сложных случаев - спускаться до нужного уровня, по пути обрабатывая данные). В JS тоже, наверняка, есть библиотеки для этого, а не только JSON.parse.


    1. dvmuratov
      04.06.2025 13:48

      Такой потоковый парсер для любого формата пишется на любом языке сравнительно не долго в виде машины состояний. В него хоть по одному байту подавай бесконечный json. Более высокий уровень должен будет обрабатывать события и на нужном уровне вложенности начинать забирать данные.


  1. CrazyOpossum
    04.06.2025 13:48

    Можно перейти на toml - передавай узлы один за другим в любом порядке.


    1. vdudouyt
      04.06.2025 13:48

      Тогда уж на newline-separated JSON.


    1. mayorovp
      04.06.2025 13:48

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


      1. CrazyOpossum
        04.06.2025 13:48

        А тут без вариантов. В любом случае, прогрессивность на совести автора протокола, и ему решать когда можно начинать обработку частичных данных, а когда ждать ещё что-то.


  1. SadOcean
    04.06.2025 13:48

    Идея, разумеется, не безнадежная, хотя не так элегантна как с jpeg.
    C картинками она хороша, потому что картинки сравнительно большие, но однородные.
    Простой перестановкой мы получаем прогрессивность фактически забесплатно - просто передаем не первую строку, а каждый 64-й (16-й) пиксель.
    Так мы получаем дешевую возможность создать превью.

    Основная проблема со сложными данными - мы должны учитывать их структуру и определять некоторое поведение прогрессивного отображения.
    Это на самом деле очень дорого (даже у вас каждый раз сноски, тут мы покажем элемент вот так, тут подождем пока не загрузится).
    Оправданно при этом это будет лишь в действительно сложных эдж-кейсах, например отобразить основной каркас статьи, контент статьи и комментарии.
    При этом концептуальной разницы между схемы с несколькими запросами на самом деле не будет.
    В любом случае такое изменение требует вдумчивого и домен специфического дизайна.

    В таком случае вместо такого прогрессивного json можно сделать некоторую "прогрессивную структуру" - например, разбить ваш контент на некоторые сущности - контентные единицы, и добавить возможность загружать их в любом порядке - сразу или с задержкой. То есть архитектурно заложить, что объект может быть дозагружен сейчас или позже.
    Тогда для страницы в целом нужно сделать как можно более простой каркас-загрузчик и после этого объекты могут быть доставлены любым способом - слеплены в один запрос, в несколько, по требованию и т.д.
    Получается прогрессивный способ загрузки, но без требований к формату (он прогрессивный на уровне контента)


  1. Kahelman
    04.06.2025 13:48

    А зачем создавали вногоуровневые вложенные форматы? Ежу же было понятно что их придется по сети передавать и парсить как-то. Для видео же придумали потоковый формат, и для данных надо было нечто подобное делать.


    1. DirectoriX
      04.06.2025 13:48

      Смысл потоковых форматов не только в том, что они преобразуются в корректное, хоть и не полное, состояние автоматигески просто при линейном чтении (выше уже писали, что парсер на событиях написать несложно, да и условный RapidJSON имеет как DOM, так и SAX-интерфейсы).

      Потоковые форматы ещё можно "промотать" - и они всё равно будут собираться в корректное состояние. Вы можете закодировать условный видеофайл так, что у вас будет ровно один ключевой кадр в начале, а потом только дельты - тогда у вас просто не будет выбора, вам придётся декодировать 2 часа фильма, чтобы посмотреть только сцену после титров. Но этот же условный видеофайл может быть разбит на кучу отдельных блоков со своими ключевыми кадрами и необходимыми метаданными - тогда он очень даже потоко-пригодный. Видео и аудио позволяют такие "шалости" потому что в общем случае текущее состояние (кадр или частота+громкость) очень мало зависят от того, что было давно, потому что новые данные "перезатирают" старое состояние. Это очень редкое свойство для абстрактных данных в вакууме, и почти всегда решается не на уровне формата, с на уровне использования этого формата. Та же пагинация - это по сути создание "потокового" представления контента, что особенно наглядно на страницах с бесконечной прокруткой, типа лент соцсетей или чатов в мессенджерах.


    1. MANAB
      04.06.2025 13:48

      "Создай проблему, реши проблему, живи на разницу."
      Современный frontend является "прекрасной" реализацией этого выражения.


  1. Gromin
    04.06.2025 13:48

    Интересный вариант решения проблемы когда есть ограничения, препятствующие другим вариантам (как то же разбиение на мелкие JSONы).


  1. jakobz
    04.06.2025 13:48

  1. Abzac
    04.06.2025 13:48

    Я понимаю, что это очень интересно всё, конечно, но почему бы не передавать построчно серию JSON-ов, как в ClickHouse сделали формат JSONEachRow? https://clickhouse.com/docs/interfaces/formats/JSONEachRow

    Пример:

    {"num":42,"str":"hello","arr":[0,1]}
    {"num":43,"str":"hello","arr":[0,1,2]}
    {"num":44,"str":"hello","arr":[0,1,2,3]}

    Например, первой строкой отправили хедер, второй — футер, всеми последующими — отдельные посты (всё, что требует пагинации)


    1. Gromilo
      04.06.2025 13:48

      Это же json lines!


      1. Abzac
        04.06.2025 13:48

        Да, он и есть)


  1. DrMefistO
    04.06.2025 13:48

    Моя профдеформация мне говорит, что для эксплуатации такой реализации можно в ещё не загруженном элементе ссылаться на уже загруженный, т.е. произвести зацикливание ссылок.


    1. slonopotamus
      04.06.2025 13:48

      Это заявлено в тексте как фича.


      1. DrMefistO
        04.06.2025 13:48

        Может я просмотрел, но там наоборот: сначала грузится ссылка, а потом её содержимое.


        1. slonopotamus
          04.06.2025 13:48

          В смысле, заявлена как фича сама возможность описывать циклы.


  1. GamePad64
    04.06.2025 13:48

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


    1. vdudouyt
      04.06.2025 13:48

      Может лучше не надо, а?


  1. Andy_U
    04.06.2025 13:48

    Все равно же не провалидировать, пока все не скачается?


  1. Format-X22
    04.06.2025 13:48

    но мы не можем определить, поступят ли новые comment или этот был последним.

    В бинарных протоколах для такого передается ожидаемое число элементов, а потом уже сами элементы.

    Однако если мы передаём JSON прогрессивно, то можем решить вынести его

    До первой гонки потоков. Что если пока мы эти данные выкачивались они уже изменились и из трех вынесенных элементов один уже поменялся? А веб это реалтайм и оно из коробки всегда имеет на это шанс.

    например, чтобы по умолчанию объекты встраивались (ради компактности)

    Вообще gzip сам делает это и всё там компактно по сети летит, а вот когда приходит - уже распаковывается и там 100500 ключей и прочего. То есть смыла особо нет. Но вот вам гиперкомпакное предложение - а зачем ключи именные если изначально известна схема? Всегда можно передавать массивы массивов если точно знаешь схему. Но читать будет больно и проще тогда grpc. Лучшая передача структуры - её отсутствие в передаваемых данных и контракт у отправителя и принимающего.

    Если вам знакомы системы, решающие эту задачу иначе, то напишите мне о них!

    Вот вам способ без $1 и прочих меток. Передаете данные JSON стримом где первым чанком заходят верхнеуровневые данные, дальше чанками заходят вложенные. При этом вы не ставите стабы из $, а просто подразумеваете что второй чанк будет вот с этим, третий с этим и прочее. И начальный скелет

    { header: "$1", post: "$2", footer: "$3"}

    вам будет не нужен. А если беспокоитесь о том что порядок разный - можете передать объект с одним ключем-именем данных, а в значениях сами данные

    { comments: [.....]}

    И так то даже изобретать новое не нужно - просто настроить старое, возможно либу для удобства написать. Но решение уже есть и оно работает - JSON-стрим с правильной подготовкой.


  1. izibrizi2
    04.06.2025 13:48

    Возьмите jsonl и в начале передавайте наиболее важные данные, а далее подсасывайте всё остальное. Можно даже кодиповать идентификатор узла, например:

    { node: header}

    {content: header text}

    { block: comments }

    { id:1, text: comment text }

    И можно, например, генерировать события, что начался такой то блок, а получатель уже будет отрисовывать части интерфейса. Но если честно, за такое олимпиадное программпование нужно бить по рукам. Единственное где это может пригодиться, это передача данных в файле, чтобы при парсинге не забивать память, храня всё дерево. Ну и про gzip не стоит забывать, который вам сожмет json в 5-10 раз, уменьшив время передвчи по сети.

    Но тут конечно от задачи зависит, возможно комменты лучше подсосать отдельным запросом


  1. rutexd
    04.06.2025 13:48

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

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


  1. LlmOxygen
    04.06.2025 13:48

    Пагинация комментов (или какая там будет длинная однородная структура?..) и отдельные запросы на особо тяжёлые структуры в моменты доматывания до них. И всё, вместо одного гигантского json у нас несколько небольших.


  1. xFFFF
    04.06.2025 13:48

    Гораздо проще разбить JSON на части, и загружать по мере просмотра страницы. Может человек вообще не долистает до комментариев, и их можно будет не загружать.