Open-source project
zalando-incubator/graphql-jit avatar
zalando-incubator/graphql-jit

graphql-jit compiles a GraphQL query into JavaScript and lets V8 optimise it

GraphQL execution using a JIT compiler

1,085 stars64 forksTypeScriptNOASSERTION

At a glance

What is it?
graphql-jit takes a parsed GraphQL document and a schema and produces a compiled function that resolves the query directly, so the query runs as optimised JavaScript instead of being walked by an interpreter. The project publishes its own benchmark against graphql-js, and it has one significant limitation, which is that every computed property needs a resolver.
Who is it for?
graphql-jit fits a service whose GraphQL workload is high enough that interpretation shows up in the profile, and whose schema keeps to the resolvers the compiler can handle. It is the wrong choice for a schema that leans on graphql-js behaviour the compiler does not support, because the difference in output is a hard failure rather than a slowdown.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The argument is about V8, not about GraphQL

The Why section of the README is two paragraphs and the reasoning is worth following closely, because it explains what the project is not trying to be.

GraphQL-JS is described as a very well written runtime implementation of the latest GraphQL spec. That is stated without a caveat, and the project is not trying to replace it on correctness or on coverage. The argument is about what happens after the query has been parsed.

By compiling to JS, V8 is able to create optimized code which yields much better performance, and graphql-jit builds on that behaviour of V8 optimization by compiling the queries into functions to improve performance. An interpreted runtime walks the document tree on every request, resolving field by field, and the shape of the work is not something the engine can specialise on. A compiled query is a function with the field selection already baked into it, so the engine sees ordinary JavaScript and can inline and optimise it the way it optimises any other hot code.

That is the entire mechanism, and it is why the project is called a compiler rather than a runtime. The query is compiled once, ahead of the request, and the resulting function is called with the root value, the context and the variables.

The API makes that split explicit. compileQuery(schema, document, operationName, compilerOptions) returns a compiled query, and isCompiledQuery is the check for whether the compilation succeeded.

js
const { compileQuery, isCompiledQuery } = require("graphql-jit");
const compiledQuery = compileQuery(schema, document);

if (!isCompiledQuery(compiledQuery)) {
  console.error(compiledQuery);
  throw new Error("Error compiling query");
}

The failure case is designed rather than accidental, because a query that cannot be compiled returns an object you can print, and the README's own example prints it before throwing.

The published benchmark, and what it actually measures

The README includes the benchmark output, and it is worth reading with the caveat that these are the project's own numbers on its own schema.

The command is yarn benchmark skip-json, run against GraphQL-JS 16 on Node 16.13.0, and it exercises four scenarios.

bash
$ yarn benchmark skip-json
Starting introspection
graphql-js x 1,941 ops/sec ±2.50% (225 runs sampled)
graphql-jit x 6,158 ops/sec ±2.38% (222 runs sampled)

The fewResolvers scenario shows graphql-js at 26,620 ops/sec against graphql-jit at 339,223 ops/sec, and the manyResolvers scenario shows 16,415 against 178,331. The nestedArrays scenario is the smallest of the four, with 127 ops/sec for graphql-js and 1,316 for graphql-jit, and the whole run took 141.25 seconds.

The shape of those numbers is the interesting part. The gain grows with the number of resolvers, which is what you would expect if the win comes from specialising the field selection rather than from removing interpretation altogether. The introspection case, where there is one resolver and a lot of type walking, improves by a factor of three, and the resolver heavy cases improve by more than an order of magnitude.

Two caveats are visible in the output itself. The name of the command says skip-json, so the JSON serialisation path is excluded, and the Node version is 16.13.0, which is a specific engine generation. The numbers are useful as an indication of where the gain comes from, and they are not a prediction for your schema.

The limitation: computed properties need a resolver

The section on differences to graphql-js is short and the main item is a hard requirement.

In order to achieve better performance, the graphql-jit compiler introduces some limitations. The primary limitation is that all computed properties must have a resolver and only these can return a Promise. The README points at a separate document, GraphQL-JS.md, for the details.

That is the price of the approach and it is worth being precise about it. In graphql-js a resolver can return a plain value and the engine can compute a field value on the fly from the parent's fields. A compiled query cannot do that, because the code that computes the value has to exist in the generated function at compile time, and a resolver is the mechanism the compiler has for it. The Promise restriction follows from the same place, since the compiled executor has to know which parts of the work are asynchronous.

So the migration question is concrete. Any field in your schema that is a computed property, a getter on the parent type rather than a stored field, has to be given a resolver before the query will compile, and the Promise rule means the resolvers you write have to be async in the places where they were not before.

This is a schema change rather than a configuration change, which is the practical reason to check your schema before you check your benchmark.

Four compiler options, and the ones that change correctness

The compiler options are documented individually, and two of them are about performance while two are about correctness.

disableLeafSerialization, a boolean that defaults to false, disables the leaf node serializers. The README is careful about the condition, since the serializers validate the content of the field at runtime, so the option should only be set to true if there are strong assurances that the values are valid. In other words, this switch turns off a runtime check, and the check is what stops a malformed value from reaching the rest of your system.

customSerializers, a Map that defaults to empty, replaces serializer functions for specific types, and the README offers it as a safer alternative for overly expensive serializers. That is the option to reach for when the default serialisation of a particular type is slow, since replacing one type's serialiser keeps the validation behaviour of the others.

customJSONSerializer, a boolean defaulting to false, produces also a JSON serializer function using fast-json-stringify, where the default stringifier function is JSON.stringify. This one is separate from the leaf serialisers, and it is the switch that makes compiledQuery.stringify cheaper.

The fourth option is debug, and it is the largest of the four. It publishes formatted generated executors as virtual JavaScript sources, and the README says this is intended for local development. Inside it there are enabled, querySourceName, variablesSourceName and formatSourceCode, and the two name options take a stable name for the generated query source and for the variable coercion source, with the example format graphql-jit://graphql-jit/my-service/GetProduct.query.js. formatSourceCode is a synchronous formatter that runs before compilation, and the example given is @prettier/sync.

The distinction between the query source and the variables source is a small thing that makes the debugging story work, and it is covered next.

Setting breakpoints in generated code

There is a documented way to debug the compiled output, and it works by making the generated code visible to a debugger.

Start Node with --inspect and enable debug for the operation being investigated.

js
const { format } = require("@prettier/sync");

const compiledQuery = compileQuery(schema, document, "GetProduct", {
  debug: {
    enabled: true,
    querySourceName: "graphql-jit://graphql-jit/shop/GetProduct.query.js",
    variablesSourceName:
      "graphql-jit://graphql-jit/shop/GetProduct.variables.js",
    formatSourceCode: (source) => format(source, { parser: "babel" })
  }
});

The debugger exposes separate query and variable-coercion scripts, and the README says to set breakpoints directly in those generated JavaScript sources. The two script kinds are separated because they fail differently, and a variable coercion problem and a resolver problem are not the same problem.

The naming convention is the part that makes this usable across a session. The names are URIs, and they carry the service and the operation name, so a breakpoint in one operation's generated source is distinguishable from another's, and the same convention is used for the variables source. The formatSourceCode hook with a formatter such as @prettier/sync exists because generated code is unreadable otherwise.

For the rest of the API, the compiled query exposes query(root, context, variables) for execution, subscribe(root, context, variables) for subscriptions, which produces either an AsyncIterator if successful or an ExecutionResult on error, and stringify(value) for producing a JSON string, which uses JSON.stringify unless customJSONSerializer is on.

Dual builds, a mutation testing setup and a busy release line

The package metadata shows a library that is careful about how it is consumed. The name is graphql-jit at version 0.9.3, the description is GraphQL JIT Compiler to JS, and the author is Rui Araujo. There is a main entry of dist/cjs/index.js, a module entry of dist/esm/index.js, and separate type declarations for each, with typesVersions mapping so that a TypeScript consumer on either module system resolves the right types. The exports map is written out in full with require and import conditions, subpath exports for./* and for dist/*, and an explicit entry for./package.json.

The build scripts explain the layout. There is a prepare-build.js step, then build:cjs and build:esm, each running tsc against its own tsconfig, tsconfig.cjs.json and tsconfig.esm.json, with a third tsconfig.json for the type check. There is a check-exports script and a check-tsc script, which is how a library with a conditional export map keeps the two halves from drifting apart.

The repository has a stryker.conf.json, which configures mutation testing. The examples directory holds three runnable projects, a blog application, a blog with an Apollo server, and a simple-debug example that is presumably the smallest end to end case of the debugging workflow described above.

The release line is busy. The listed releases are v0.8.10 and v0.7.5 on 2026-10-13 and v0.9.3 on 2026-10-12, and the last push was on 2026-10-13, so several lines are being published at once. The README's own licence section says MIT, and the repository holds a LICENSE file, a SECURITY.md, a MAINTAINERS file and a CODE_OF_CONDUCT.md, and the homepage is a live playground at try-graphql-jit.boopathi.in/.

Editorial conclusion

graphql-jit fits a service whose GraphQL workload is high enough that interpretation shows up in the profile, and whose schema keeps to the resolvers the compiler can handle. It is the wrong choice for a schema that leans on graphql-js behaviour the compiler does not support, because the difference in output is a hard failure rather than a slowdown. Verify first which of your fields are computed properties without resolvers, and benchmark your own schema with yarn benchmark skip-json rather than relying on the numbers in the README.

Frequently asked questions

How does graphql-jit differ from graphql-js?

graphql-jit compiles the parsed query into a JavaScript function so V8 can optimise it, while graphql-js interprets the document at runtime. The cost is a set of limitations, and the primary one is that all computed properties must have a resolver and only these can return a Promise, with more detail in GraphQL-JS.md.

How do I install graphql-jit?

With yarn add graphql-jit. The package is published as graphql-jit, currently at version 0.9.3, and it ships CommonJS and ES module builds with separate type declarations for each.

How do I compile and run a query with graphql-jit?

Parse the query with parse from graphql, call compileQuery(schema, document), check the result with isCompiledQuery, and then call the compiled query with compiledQuery.query(root, context, variables). Subscriptions use compiledQuery.subscribe instead.

What compiler options can I pass to compileQuery?

There are four. disableLeafSerialization turns off the runtime validation of leaf values and should only be set with strong assurances the values are valid, customSerializers replaces the serialiser for specific types, customJSONSerializer produces a fast-json-stringify based serialiser, and the debug object publishes the generated executors as virtual JavaScript sources for local development.

Can I debug the generated GraphQL code?

Yes. Start Node with --inspect and enable the debug option, giving a querySourceName and a variablesSourceName, and the debugger exposes separate query and variable-coercion scripts where you can set breakpoints directly in the generated JavaScript.

Official sources

  1. Issues
  2. Project website
  3. README
  4. Releases
  5. zalando-incubator/graphql-jit on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/zalando-incubator-graphql-jit.svg)](https://hysenlabs.com/projects/zalando-incubator-graphql-jit)

Was this useful?