graphql/graphql-spec

Binary protocol

tinganho opened this issue Β· 12 comments

I'm wondering if there is any plans to add an alternative binary protocol to GraphQL? I think it is good if it is optional and if one can just opt-in to it.

An example:

We manually byte encode all fields:

type RootQuery {
  starship = 0
}

type Starship {
  id: ID! = 0
  name: String! = 1
  length(unit: LengthUnit = METER): Float = 2
}

Instead of sending this query:

query {
   startship {
      id
      name
      length
   }
}

We send this compact one instead:

0 { # query
   0 { # starhsip
      0 # id
      1 # name
      2 # length
   }
}

And instead of receiving JSON

{
  "data" : {
    "starship": {
      "id": "1",
      "name": "Albert Einstein",
      "length": "1",
    }
  }
}

We receive a byte encoded response:

0 { # starship
    0 "1" # id
    1 "Albert Einstein" # name
    2 "1" # length
  }
}

The binary protocol should not replace the textual one. I still think it is good with JSON. Though, I think there is a good use case to have both. I've seen many use-cases where one use gRPC for internal communication(microservice-to-microservice) and GraphQL for external API. And it would be practical if GraphQL just allowed one to use a binary protocol, so devs don't have to use both.

GraphQL intentionally does not require any specific serialization or transport. JSON is preferred, but explicitly not required (http://facebook.github.io/graphql/October2016/#sec-Serialization-Format) so any existing binary protocol can be used with GraphQL. There are plenty in the gRPC space that would work well alongside GraphQL for the use cases you're describing.

There are no plans for the GraphQL spec to explicitly specify any specific binary protocol

JSON is preferred, but explicitly not required (http://facebook.github.io/graphql/October2016/#sec-Serialization-Format)

I doubt there will be any implementation of an alternative to JSON, unless GraphQL supports(blesses) it. Though this is just for the response side. You are still missing the request side?

gRPC space that would work well alongside GraphQL

I'm not sure what you mean here. To enable both, gRPC and GraphQL. You have to define your types(or messages) for both. Which requires a lot of work. GraphQL is just lacking the optimizations that gRPC has, why can't you just add it as an opt-in feature? I think it makes perfect sense, no?

@tinganho he probably meant protobuf, the encoding format underlying the gRPC protocol.
It's not hard to imagine a graphql schema coevolving in sync with a protobuf schema.
These days, since gRPC compatibility isn't actually a feature to the GraphQL usecase, something like flatbuffers is probably better.

I implemented a proof-of-concept binary protocol, currently only query encoding\decoding:
https://github.com/esseswann/graphql-binary
People willing to make it a real thing are welcome

srghma commented

there are protocols that:

  1. transfer data with schema: json, cbor, MessagePack, Universal Binary JSON
  2. and schema is separate: FlatBuffers, protobuf, Avro, Cap’n Proto, Thrift

more info

https://en.wikipedia.org/wiki/Comparison_of_data-serialization_formats
https://news.ycombinator.com/item?id=14067607
msgpack/msgpack#258

seems like cbor is easiest to use

benjie commented

Deflate is surprisingly effective; so far we've not found any of the binary serialization formats to have a significant edge over the combination of JSON+deflate that would justify the inherent complexity added.

To add some actual evidence to this, I picked a random query result from the Graphile Crystal test suite and encoded it with JSON, JSON+gzip, JSON+deflate, CBOR, CBOR+gzip and CBOR+deflate. The JSON+deflate came out smallest; 11 bytes smaller than the CBOR+deflate result.

Test code
const cbor = require("cbor");
const { deflateSync, gzipSync } = require("zlib");

const data = {
  allPosts: {
    edges: [
      {
        cursor: "WyI2Nzc2ZDM3ZjcwIiwxXQ==",
        node: {
          id: 1,
          headline:
            "No… It’s a thing; it’s like a plan, but with more greatness.",
          headlineTrimmed: "No… It’s …",
          author: {
            id: 2,
            name: "Sara Smith",
            firstName: "Sara",
            firstPost: {
              id: 1,
              headline:
                "No… It’s a thing; it’s like a plan, but with more greatness.",
              headlineTrimmed: "No… It’s …",
              author: {
                id: 2,
                name: "Sara Smith",
                firstName: "Sara",
              },
            },
            friends: {
              nodes: [
                {
                  id: 3,
                  name: "Budd Deey",
                  firstName: "Budd",
                },
                {
                  id: 4,
                  name: "Kathryn Ramirez",
                  firstName: "Kathryn",
                },
              ],
              totalCount: 2,
              pageInfo: {
                startCursor: "WyJuYXR1cmFsIiwxXQ==",
              },
            },
          },
        },
      },
      {
        cursor: "WyI2Nzc2ZDM3ZjcwIiwyXQ==",
        node: {
          id: 2,
          headline: "I hate yogurt. It’s just stuff with bits in.",
          headlineTrimmed: "I hate yo…",
          author: {
            id: 1,
            name: "John Smith",
            firstName: "John",
            firstPost: {
              id: 2,
              headline: "I hate yogurt. It’s just stuff with bits in.",
              headlineTrimmed: "I hate yo…",
              author: {
                id: 1,
                name: "John Smith",
                firstName: "John",
              },
            },
            friends: {
              nodes: [
                {
                  id: 2,
                  name: "Sara Smith",
                  firstName: "Sara",
                },
                {
                  id: 3,
                  name: "Budd Deey",
                  firstName: "Budd",
                },
              ],
              totalCount: 2,
              pageInfo: {
                startCursor: "WyJuYXR1cmFsIiwxXQ==",
              },
            },
          },
        },
      },
      {
        cursor: "WyI2Nzc2ZDM3ZjcwIiwzXQ==",
        node: {
          id: 3,
          headline: "Is that a cooking show?",
          headlineTrimmed: "Is that a…",
          author: {
            id: 1,
            name: "John Smith",
            firstName: "John",
            firstPost: {
              id: 2,
              headline: "I hate yogurt. It’s just stuff with bits in.",
              headlineTrimmed: "I hate yo…",
              author: {
                id: 1,
                name: "John Smith",
                firstName: "John",
              },
            },
            friends: {
              nodes: [
                {
                  id: 2,
                  name: "Sara Smith",
                  firstName: "Sara",
                },
                {
                  id: 3,
                  name: "Budd Deey",
                  firstName: "Budd",
                },
              ],
              totalCount: 2,
              pageInfo: {
                startCursor: "WyJuYXR1cmFsIiwxXQ==",
              },
            },
          },
        },
      },
      {
        cursor: "WyI2Nzc2ZDM3ZjcwIiw0XQ==",
        node: {
          id: 4,
          headline: "You hit me with a cricket bat.",
          headlineTrimmed: "You hit m…",
          author: {
            id: 1,
            name: "John Smith",
            firstName: "John",
            firstPost: {
              id: 2,
              headline: "I hate yogurt. It’s just stuff with bits in.",
              headlineTrimmed: "I hate yo…",
              author: {
                id: 1,
                name: "John Smith",
                firstName: "John",
              },
            },
            friends: {
              nodes: [
                {
                  id: 2,
                  name: "Sara Smith",
                  firstName: "Sara",
                },
                {
                  id: 3,
                  name: "Budd Deey",
                  firstName: "Budd",
                },
              ],
              totalCount: 2,
              pageInfo: {
                startCursor: "WyJuYXR1cmFsIiwxXQ==",
              },
            },
          },
        },
      },
      {
        cursor: "WyI2Nzc2ZDM3ZjcwIiw1XQ==",
        node: {
          id: 5,
          headline:
            "Please, Don-Bot… look into your hard drive, and open your mercy file!",
          headlineTrimmed: "Please, D…",
          author: {
            id: 5,
            name: "Joe Tucker",
            firstName: "Joe",
            firstPost: {
              id: 5,
              headline:
                "Please, Don-Bot… look into your hard drive, and open your mercy file!",
              headlineTrimmed: "Please, D…",
              author: {
                id: 5,
                name: "Joe Tucker",
                firstName: "Joe",
              },
            },
            friends: {
              nodes: [
                {
                  id: 6,
                  name: "Twenty Seventwo",
                  firstName: "Twenty",
                },
              ],
              totalCount: 1,
              pageInfo: {
                startCursor: "WyJuYXR1cmFsIiwxXQ==",
              },
            },
          },
        },
      },
      {
        cursor: "WyI2Nzc2ZDM3ZjcwIiw2XQ==",
        node: {
          id: 6,
          headline: "Stop talking, brain thinking. Hush.",
          headlineTrimmed: "Stop talk…",
          author: {
            id: 3,
            name: "Budd Deey",
            firstName: "Budd",
            firstPost: {
              id: 6,
              headline: "Stop talking, brain thinking. Hush.",
              headlineTrimmed: "Stop talk…",
              author: {
                id: 3,
                name: "Budd Deey",
                firstName: "Budd",
              },
            },
            friends: {
              nodes: [
                {
                  id: 4,
                  name: "Kathryn Ramirez",
                  firstName: "Kathryn",
                },
                {
                  id: 5,
                  name: "Joe Tucker",
                  firstName: "Joe",
                },
              ],
              totalCount: 2,
              pageInfo: {
                startCursor: "WyJuYXR1cmFsIiwxXQ==",
              },
            },
          },
        },
      },
      {
        cursor: "WyI2Nzc2ZDM3ZjcwIiw3XQ==",
        node: {
          id: 7,
          headline: "Large bet on myself in round one.",
          headlineTrimmed: "Large bet…",
          author: {
            id: 1,
            name: "John Smith",
            firstName: "John",
            firstPost: {
              id: 2,
              headline: "I hate yogurt. It’s just stuff with bits in.",
              headlineTrimmed: "I hate yo…",
              author: {
                id: 1,
                name: "John Smith",
                firstName: "John",
              },
            },
            friends: {
              nodes: [
                {
                  id: 2,
                  name: "Sara Smith",
                  firstName: "Sara",
                },
                {
                  id: 3,
                  name: "Budd Deey",
                  firstName: "Budd",
                },
              ],
              totalCount: 2,
              pageInfo: {
                startCursor: "WyJuYXR1cmFsIiwxXQ==",
              },
            },
          },
        },
      },
      {
        cursor: "WyI2Nzc2ZDM3ZjcwIiw4XQ==",
        node: {
          id: 8,
          headline: "It’s a fez. I wear a fez now. Fezes are cool.",
          headlineTrimmed: "It’s a fe…",
          author: {
            id: 2,
            name: "Sara Smith",
            firstName: "Sara",
            firstPost: {
              id: 1,
              headline:
                "No… It’s a thing; it’s like a plan, but with more greatness.",
              headlineTrimmed: "No… It’s …",
              author: {
                id: 2,
                name: "Sara Smith",
                firstName: "Sara",
              },
            },
            friends: {
              nodes: [
                {
                  id: 3,
                  name: "Budd Deey",
                  firstName: "Budd",
                },
                {
                  id: 4,
                  name: "Kathryn Ramirez",
                  firstName: "Kathryn",
                },
              ],
              totalCount: 2,
              pageInfo: {
                startCursor: "WyJuYXR1cmFsIiwxXQ==",
              },
            },
          },
        },
      },
      {
        cursor: "WyI2Nzc2ZDM3ZjcwIiw5XQ==",
        node: {
          id: 9,
          headline: "You know how I sometimes have really brilliant ideas?",
          headlineTrimmed: "You know …",
          author: {
            id: 3,
            name: "Budd Deey",
            firstName: "Budd",
            firstPost: {
              id: 6,
              headline: "Stop talking, brain thinking. Hush.",
              headlineTrimmed: "Stop talk…",
              author: {
                id: 3,
                name: "Budd Deey",
                firstName: "Budd",
              },
            },
            friends: {
              nodes: [
                {
                  id: 4,
                  name: "Kathryn Ramirez",
                  firstName: "Kathryn",
                },
                {
                  id: 5,
                  name: "Joe Tucker",
                  firstName: "Joe",
                },
              ],
              totalCount: 2,
              pageInfo: {
                startCursor: "WyJuYXR1cmFsIiwxXQ==",
              },
            },
          },
        },
      },
      {
        cursor: "WyI2Nzc2ZDM3ZjcwIiwxMF0=",
        node: {
          id: 10,
          headline:
            "What’s with you kids? Every other day it’s food, food, food.",
          headlineTrimmed: "What’s wi…",
          author: {
            id: 2,
            name: "Sara Smith",
            firstName: "Sara",
            firstPost: {
              id: 1,
              headline:
                "No… It’s a thing; it’s like a plan, but with more greatness.",
              headlineTrimmed: "No… It’s …",
              author: {
                id: 2,
                name: "Sara Smith",
                firstName: "Sara",
              },
            },
            friends: {
              nodes: [
                {
                  id: 3,
                  name: "Budd Deey",
                  firstName: "Budd",
                },
                {
                  id: 4,
                  name: "Kathryn Ramirez",
                  firstName: "Kathryn",
                },
              ],
              totalCount: 2,
              pageInfo: {
                startCursor: "WyJuYXR1cmFsIiwxXQ==",
              },
            },
          },
        },
      },
      {
        cursor: "WyI2Nzc2ZDM3ZjcwIiwxMV0=",
        node: {
          id: 11,
          headline: "They’re not aliens, they’re Earth…liens!",
          headlineTrimmed: "They’re n…",
          author: {
            id: 3,
            name: "Budd Deey",
            firstName: "Budd",
            firstPost: {
              id: 6,
              headline: "Stop talking, brain thinking. Hush.",
              headlineTrimmed: "Stop talk…",
              author: {
                id: 3,
                name: "Budd Deey",
                firstName: "Budd",
              },
            },
            friends: {
              nodes: [
                {
                  id: 4,
                  name: "Kathryn Ramirez",
                  firstName: "Kathryn",
                },
                {
                  id: 5,
                  name: "Joe Tucker",
                  firstName: "Joe",
                },
              ],
              totalCount: 2,
              pageInfo: {
                startCursor: "WyJuYXR1cmFsIiwxXQ==",
              },
            },
          },
        },
      },
    ],
  },
};

const jsonString = Buffer.from(JSON.stringify(data), "utf8");
const jsonDeflated = deflateSync(jsonString);
const jsonGzipped = gzipSync(jsonString);
const cborString = cbor.encode(data);
const cborDeflated = deflateSync(cborString);
const cborGzipped = gzipSync(cborString);

const sizes = Object.entries({
  jsonString,
  jsonDeflated,
  jsonGzipped,
  cborString,
  cborDeflated,
  cborGzipped,
}).map(([name, buffer]) => ({ name, size: buffer.length }));

console.table(sizes);

Result:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”
β”‚ (index) β”‚      name      β”‚ size β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€
β”‚    0    β”‚  'jsonString'  β”‚ 6235 β”‚
β”‚    1    β”‚ 'jsonDeflated' β”‚ 832  β”‚
β”‚    2    β”‚ 'jsonGzipped'  β”‚ 844  β”‚
β”‚    3    β”‚  'cborString'  β”‚ 5160 β”‚
β”‚    4    β”‚ 'cborDeflated' β”‚ 843  β”‚
β”‚    5    β”‚ 'cborGzipped'  β”‚ 855  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”˜

Note that the data above is very much "testing data" - if someone wants to write a reasonably production-like query against a public GraphQL API, for example the GitHub API, and run the test again you're welcome to - the code is straightforward:

const data = { /* your GraphQL result here */ };

const cbor = require("cbor");
const { deflateSync, gzipSync } = require("zlib");
const jsonString = Buffer.from(JSON.stringify(data), "utf8");
const jsonDeflated = deflateSync(jsonString);
const jsonGzipped = gzipSync(jsonString);
const cborString = cbor.encode(data);
const cborDeflated = deflateSync(cborString);
const cborGzipped = gzipSync(cborString);
const sizes = Object.entries({
  jsonString,
  jsonDeflated,
  jsonGzipped,
  cborString,
  cborDeflated,
  cborGzipped,
}).map(([name, buffer]) => ({ name, size: buffer.length }));
console.table(sizes);

Sandbox: https://codesandbox.io/p/sandbox/determined-shadow-szmxj3

To add some actual evidence to this, I picked a random query result from the Graphile Crystal test suite and encoded it with JSON, JSON+gzip, JSON+deflate, CBOR, CBOR+gzip and CBOR+deflate. The JSON+deflate came out smallest; 11 bytes smaller than the CBOR+deflate result.To add some actual evidence to this, I picked a random query result from the Graphile Crystal test suite and encoded it with JSON, JSON+gzip, JSON+deflate, CBOR, CBOR+gzip and CBOR+deflate. The JSON+deflate came out smallest; 11 bytes smaller than the CBOR+deflate result.

It's definitely not reasonable to base a choice on a single example like this. As per https://arxiv.org/pdf/2201.03051.pdf in practice it really depends on the data you'll want to exchange, but on average CBOR or MsgPack will outperform JSON by a margin - and for small messages (very common in IoT or media arts for instance - 99.9% of the messages I worked with over a ton of different projects are < 100 bytes), compressing will often end up increasing the size

benjie commented

It's definitely not reasonable to base a choice on a single example like this

Absolutely; that wasn't a choice, just a datapoint. Please feel free to use the script I gave and a battery of more realistic GraphQL results to provide more convincing data.

on average CBOR or MsgPack will outperform JSON by a margin

Please provide data that relates specifically to GraphQL responses, since that's what we're discussing here.

for small messages (very common in IoT or media arts for instance - 99.9% of the messages I worked with over a ton of different projects are < 100 bytes), compressing will often end up increasing the size

Correct; but responses less than 100 bytes are exceedingly rare in GraphQL. {"data":{}} is 11 bytes before you've even given any actual data. Please provide data that relates to GraphQL responses.


I don't know of a good repository of example real-world GraphQL queries and responses; but I'm very familiar with PostGraphile's test suite since I wrote it. It's important to not that it is not representative of "regular" GraphQL queries so take this data with a pinch of salt. I adjusted the script above to run across our entire test suite:

Click to expand the test code
const fs = require("fs/promises");
const JSON5 = require("json5");
const cbor = require("cbor");
const { deflateSync, gzipSync } = require("zlib");
const glob = require("glob");

const files = glob.sync([
  "grafast/dataplan-pg/__tests__/**/*.json5",
  "postgraphile/postgraphile/__tests__/**/*.json5",
]);

async function main() {
  const results = (
    await Promise.all(
      files.map(async (file) => {
        console.log(file);
        const dataJSON5 = await fs.readFile(file, "utf8");
        const data = JSON5.parse(dataJSON5);
        const errorsJSON5 = await fs
          .readFile(file.replace(".json5", ".errors.json5"), "utf8")
          // Most tests don't have errors
          .catch(() => null);
        const errors = errorsJSON5 ? JSON5.parse(errorsJSON5) : undefined;
        const payload = { data, errors };
        const jsonString = Buffer.from(JSON.stringify(payload), "utf8");
        const jsonDeflated = deflateSync(jsonString);
        const jsonGzipped = gzipSync(jsonString);
        const cborString = cbor.encode(payload);
        const cborDeflated = deflateSync(cborString);
        const cborGzipped = gzipSync(cborString);

        const sizes = {
          name: file.replace(/^(.*\/)([^/]+)$/, "$2"),
          jsonString: jsonString.length,
          cborString: cborString.length,
          jsonGzipped: jsonGzipped.length,
          cborGzipped: cborGzipped.length,
          jsonDeflated: jsonDeflated.length,
          cborDeflated: cborDeflated.length,
          cborBetter: cborDeflated.length < jsonDeflated.length,
          cborBetterBy10Percent:
            cborDeflated.length <= jsonDeflated.length * 0.9,
          jsonBetter: cborDeflated.length > jsonDeflated.length,
          jsonBetterBy10Percent:
            cborDeflated.length * 0.9 >= jsonDeflated.length,
        };
        return sizes;
      }),
    )
  ).filter(Boolean);
  console.table(results);

  const totals = { count: 0 };
  for (const sizes of results) {
    for (const key in sizes) {
      if (key !== "name") {
        if (!totals[key]) totals[key] = 0;
        totals[key] += sizes[key];
      } else {
        totals.count++;
      }
    }
  }
  console.table(totals);
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});

The result I get is:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚        (index)        β”‚ Values β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚         count         β”‚  229   β”‚
β”‚      jsonString       β”‚ 487079 β”‚
β”‚      cborString       β”‚ 310105 β”‚
β”‚      jsonGzipped      β”‚ 77863  β”‚
β”‚      cborGzipped      β”‚ 69902  β”‚
β”‚     jsonDeflated      β”‚ 75115  β”‚
β”‚     cborDeflated      β”‚ 67154  β”‚
β”‚      cborBetter       β”‚  158   β”‚
β”‚ cborBetterBy10Percent β”‚   19   β”‚
β”‚      jsonBetter       β”‚   61   β”‚
β”‚ jsonBetterBy10Percent β”‚   0    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”˜

I.e. For 61 of the files JSON is better, for 158 CBOR is better, and for 10 they are literally identical. On average CBOR+deflate is ~10.6% better than JSON+deflate for the PostGraphile test suite, but it's only actually better by 10+% on 19 of the test files (out of 229).

For interest I dug into a few of these >= 10% compression files and they're very obviously test fixtures: dominated by nulls, booleans, single digit integers and empty lists - things that presumably CBOR is very effective at compressing. I'm unconvinced that we'd see such savings in real world data sets - perhaps you'd care to provide a battery of real world queries to test against?

xuorig commented

There was a great talk at GraphQLConf this year that dove into some of those nuances: https://www.youtube.com/watch?v=dwf0mh2b5Rc

I don't think anyone denies that there are data sets that could benefit from binary serialization formats, which is why GraphQL does not require a specific serialization format.

I doubt there will be any implementation of an alternative to JSON, unless GraphQL supports(blesses) it. Though this is just for the response side. You are still missing the request side?

It is supported / blessed, nothing wrong with it: https://spec.graphql.org/draft/#sel-DAPJDDAADFBAl_G

JSON is documented because it has been proven for years and is the primary example, but I could see other examples being added if they get sufficient usage and proof in the wild eventually. I don't think we're there, but I also doubt what's written in the spec currently is stopping anyone from trying out binary encodings.

acao commented

Excited to see this is still in motion! Perhaps we can re-open?

Noting that TRPC folks are working on expanding into binary data formats as well as other content types beyond json.

GraphiQL users at least once a year ask us about handling alternative data formats, including binary formats like protobuf and messagepack.

So, we have plans to change Fetcher API back to expect a resolved string, and allow a custom response serializer as a prop that expects a resolved JSON string for cm/monaco.

Then, this could be used alongside the much requested response viewer plugin API when you don't want to present your binary data as json.

With GraphiQL we default to the spec, but make room for innovation beyond the spec via props and soon moreso with plugins, as it is often the tool used when authoring new frameworks.

Let me know if you all would like to see a demo of graphiql w/ CBOR or any other binary data format!

benjie commented

(FWIW, the GraphQL-over-HTTP specification does require JSON support, but it still allows other serialization formats: https://graphql.github.io/graphql-over-http/draft/#sec-Serialization-Format )

We have been using Brotli (e.g. BR) compression for our GraphQL responses for a year now. Our payloads average 500kB of JSON each (mainly consisting of a pretty deep structure with UUID key-value pairs), with compresses nicely to some 50kb (e.g. to 10% of the original size). Brotli is quite widely supported already, and you can even offload that compression to CDNs like CloudFront.

IMHO compression solved the payload size issue already, but I am still eyeing for binary structures for the sake of performance, e.g. faster serialization and deserialization. Do yo know if there are any benchmarks available for that?