Skip to content

feat(execution): experimental batch field resolution#4760

Draft
yaacovCR wants to merge 3 commits into
graphql:17.x.xfrom
yaacovCR:batch-resolution
Draft

feat(execution): experimental batch field resolution#4760
yaacovCR wants to merge 3 commits into
graphql:17.x.xfrom
yaacovCR:batch-resolution

Conversation

@yaacovCR
Copy link
Copy Markdown
Contributor

@yaacovCR yaacovCR commented May 19, 2026

#4627

Some numbers...

UPDATED: 2026-05-20
UPDATED: 2026-05-21
UPDATED: 2026-05-24
UPDATED: 2026-05-26

Batch Resolution List-Field Benchmark

baseline=17.x.x modes=graphql-js-17,graphql-js-17-dataloader,graphql-js-local-batch,graphql-breadth-js,grafast,graphql-jit sizes=1,10,100,1000,10000 treeListDepths=1,5 treeListBreadths=10,100,1000 treeDepths=1,5,10,18 warmup=2000ms run=10000ms timingMinRounds=30 memoryIterations=500 memoryWarmup=100

Flat list

query { widgets(first: N) { id } }

Speed

size 17.x.x regular async 17.x.x dataloader working tree batch graphql-breadth-js grafast graphql-jit
1 66.9k ops/sec (1.3x) 66.5k ops/sec (1.29x) 58.7k ops/sec (1.14x) 114k ops/sec (2.22x) 51.4k ops/sec (1x) 929k ops/sec (18.1x)
10 53.9k ops/sec (1.48x) 55.5k ops/sec (1.52x) 49.9k ops/sec (1.37x) 83.2k ops/sec (2.28x) 36.5k ops/sec (1x) 759k ops/sec (20.8x)
100 20.9k ops/sec (2.12x) 20.8k ops/sec (2.11x) 17.8k ops/sec (1.8x) 19.7k ops/sec (2x) 9.86k ops/sec (1x) 249k ops/sec (25.3x)
1000 3.2k ops/sec (2.37x) 3.24k ops/sec (2.39x) 2.85k ops/sec (2.1x) 2.74k ops/sec (2.03x) 1.35k ops/sec (1x) 39.1k ops/sec (28.9x)
10000 353 ops/sec (2.87x) 365 ops/sec (2.96x) 274 ops/sec (2.22x) 263 ops/sec (2.13x) 123 ops/sec (1x) 3.38k ops/sec (27.4x)

GC pressure

size 17.x.x regular async 17.x.x dataloader working tree batch graphql-breadth-js grafast graphql-jit
100 1.74 us (1.35x) 2.35 us (1x) 2.18 us (1.08x) - 1.66 us (1.42x) -
1000 10.7 us (1.14x) 12.2 us (1x) 10.4 us (1.18x) 1.31 us (9.32x) 7.67 us (1.59x) -
10000 251 us (1.05x) 262 us (1.01x) 264 us (1x) 29.4 us (8.98x) 222 us (1.19x) 5.69 us (46.4x)

Tree within list

# inner tree depth D
query { widgets(first: N) { widget { widget { id } id } id } }

Speed

D x N 17.x.x regular async 17.x.x dataloader working tree batch graphql-breadth-js grafast graphql-jit
1 x 10 23.3k ops/sec (1.15x) 23.9k ops/sec (1.18x) 33.3k ops/sec (1.65x) 39.8k ops/sec (1.97x) 20.2k ops/sec (1x) 209k ops/sec (10.4x)
1 x 100 4.22k ops/sec (1.01x) 4.16k ops/sec (1x) 9.24k ops/sec (2.22x) 10.1k ops/sec (2.42x) 4.73k ops/sec (1.14x) 35.3k ops/sec (8.48x)
1 x 1000 402 ops/sec (1x) 405 ops/sec (1.01x) 1.22k ops/sec (3.04x) 1.31k ops/sec (3.25x) 574 ops/sec (1.43x) 4.27k ops/sec (10.6x)
5 x 10 7.34k ops/sec (1x) 7.33k ops/sec (1x) 14.2k ops/sec (1.94x) 13.6k ops/sec (1.85x) 7.69k ops/sec (1.05x) 67k ops/sec (9.14x)
5 x 100 828 ops/sec (1x) 834 ops/sec (1.01x) 2.59k ops/sec (3.13x) 2.71k ops/sec (3.27x) 1.38k ops/sec (1.66x) 7.87k ops/sec (9.5x)
5 x 1000 77.6 ops/sec (1.02x) 76.2 ops/sec (1x) 278 ops/sec (3.65x) 303 ops/sec (3.97x) 155 ops/sec (2.03x) 912 ops/sec (12x)

GC pressure

D x N 17.x.x regular async 17.x.x dataloader working tree batch graphql-breadth-js grafast graphql-jit
1 x 100 21.5 us (1x) 21.4 us (1x) 5.13 us (4.18x) - 2.84 us (7.55x) -
1 x 1000 274 us (1x) 268 us (1.02x) 47.4 us (5.78x) 5.41 us (50.6x) 28.2 us (9.71x) 6.83 us (40.1x)
5 x 10 9.68 us (1x) 9.2 us (1.05x) 3.53 us (2.74x) - 2.01 us (4.83x) -
5 x 100 77.7 us (1.06x) 82.1 us (1x) 16.7 us (4.93x) 2.54 us (32.3x) 7.08 us (11.6x) 2.95 us (27.8x)
5 x 1000 1,307 us (1.03x) 1,347 us (1x) 133 us (10.1x) 38.5 us (34.9x) 84.9 us (15.9x) 49.4 us (27.3x)

List with async widget field

query { widgets(first: N) { id widget { id } } }  # N async widget resolutions

Speed

size 17.x.x regular async 17.x.x dataloader working tree batch graphql-breadth-js grafast graphql-jit
1 52.9k ops/sec (1.56x) 50.8k ops/sec (1.5x) 52.2k ops/sec (1.54x) 60.6k ops/sec (1.78x) 34k ops/sec (1x) 452k ops/sec (13.3x)
10 22.8k ops/sec (1.14x) 22.5k ops/sec (1.12x) 33.5k ops/sec (1.67x) 37k ops/sec (1.85x) 20k ops/sec (1x) 230k ops/sec (11.5x)
100 3.56k ops/sec (1x) 3.55k ops/sec (1x) 7.41k ops/sec (2.09x) 7.89k ops/sec (2.22x) 4.2k ops/sec (1.18x) 35.2k ops/sec (9.93x)
1000 338 ops/sec (1x) 341 ops/sec (1.01x) 900 ops/sec (2.66x) 982 ops/sec (2.9x) 492 ops/sec (1.46x) 3.79k ops/sec (11.2x)
10000 28.8 ops/sec (1x) 29.2 ops/sec (1.01x) 89.5 ops/sec (3.11x) 92.3 ops/sec (3.21x) 45.1 ops/sec (1.57x) 306 ops/sec (10.6x)

GC pressure

size 17.x.x regular async 17.x.x dataloader working tree batch graphql-breadth-js grafast graphql-jit
100 22.9 us (1.04x) 23.8 us (1x) 6.59 us (3.61x) - 2.49 us (9.53x) -
1000 327 us (1x) 286 us (1.14x) 42.7 us (7.65x) 5.68 us (57.5x) 30 us (10.9x) 5.3 us (61.6x)
10000 3,102 us (1x) 3,101 us (1x) 853 us (3.64x) 184 us (16.9x) 936 us (3.31x) 137 us (22.6x)

Deep flat tree

query { widget { widget { widget { id } id } id } }  # depth D

Speed

depth 17.x.x regular async 17.x.x dataloader working tree batch graphql-breadth-js grafast graphql-jit
1 71.7k ops/sec (1.2x) 69k ops/sec (1.15x) 68.2k ops/sec (1.14x) 166k ops/sec (2.78x) 59.8k ops/sec (1x) 1,080k ops/sec (18.1x)
5 36.4k ops/sec (1.84x) 35.2k ops/sec (1.78x) 38.6k ops/sec (1.95x) 31.3k ops/sec (1.58x) 19.8k ops/sec (1x) 328k ops/sec (16.6x)
10 24k ops/sec (2.15x) 21.5k ops/sec (1.93x) 24.6k ops/sec (2.21x) 16.6k ops/sec (1.49x) 11.2k ops/sec (1x) 166k ops/sec (14.9x)
18 15.3k ops/sec (2.4x) 13.7k ops/sec (2.14x) 15.6k ops/sec (2.45x) 9.58k ops/sec (1.5x) 6.38k ops/sec (1x) 82.3k ops/sec (12.9x)

GC pressure

depth 17.x.x regular async 17.x.x dataloader working tree batch graphql-breadth-js grafast graphql-jit
10 - - - - 1.23 us (1x) -
18 6.09 us (1x) 5.74 us (1.06x) 4.83 us (1.26x) 1.03 us (5.91x) 1.22 us (4.99x) -

@yaacovCR yaacovCR added the PR: feature 🚀 requires increase of "minor" version number label May 19, 2026
@vercel
Copy link
Copy Markdown

vercel Bot commented May 19, 2026

@yaacovCR is attempting to deploy a commit to the The GraphQL Foundation Team on Vercel.

A member of the Team first needs to authorize it.

@gmac
Copy link
Copy Markdown

gmac commented May 20, 2026

FWIW, released today https://github.com/gmac/graphql-breadth-js

@yaacovCR
Copy link
Copy Markdown
Contributor Author

@gmac

From what I can tell graphql-breadth-js is pretty impressive!

I'm surprised by my numbers above that grafast is not more competitive, I may be doing something wrong in terms of the measuring.

There are hopefully a few things we could do to improve the batching implementation in graphql-js, but I think it's impressive that we still beat dataloaders without introducing any planning whatsoever, batching resolver shapes almost identical to usual:

export type GraphQLFieldExperimentalBatchResolver<
  TSource,
  TContext,
  TArgs = any,
  TResult = unknown,
> = (
  sources: ReadonlyArray<TSource>,
  args: TArgs,
  context: TContext,
  info: GraphQLBatchedResolveInfo,
) => PromiseOrValue<ReadonlyArray<TResult>>;

export interface GraphQLBatchedResolveInfo extends Omit<
  GraphQLResolveInfo,
  'path'
> {
  /** Response paths for each source value in the batch. */
  readonly paths: ReadonlyArray<Path>;
}

Hopefully this PR gets the discussion going, looking for Golden Path fans input. @benjie

@benjie
Copy link
Copy Markdown
Member

benjie commented May 20, 2026

Re: your surprise: Grafast is designed to help you reduce the amount of work that your application layer needs to do - fetching less data from your datasources, combining related work, removing redundant work, etc - and this benchmark doesn't have any such work (it's almost entirely synchronous, there isn't really an application layer to optimize) thus you're not going to see the benefits of Grafast execution, only the costs necessary to support them. Once you add network latency, bandwidth limits, and start leveraging things like attribute tracking to not even fetch attributes you don't need from your underlying datasources (which doesn't make sense when the datasources are just maps in memory as here) then the benefits of Grafast start to significantly outweigh the costs you see in synthetic benchmarks like this.

For the synthetic benchmarks, simply eliminating the number of promises drastically reduces the amount of memory allocation and garbage collection needed, saving quite significant Node CPU time. You'd see even greater benefits if the base execution had all the resolvers wrapped with observability or similar - those costs quickly mount, so turning 10,000 resolver calls into a single batch resolver call does a lot to reduce the execution cost!

@yaacovCR yaacovCR force-pushed the batch-resolution branch from 5f14bc3 to b8fbe1f Compare May 20, 2026 20:10
@gmac
Copy link
Copy Markdown

gmac commented May 20, 2026

Looks like 1 and 10 should be tossed out of the memory lineup. The faster engines produced zero events.

Also after seeing Benjie’s talk today, it really drove home the difference in problems we’re trying to solve. Grafast is trying to give you a standardized pattern for sound operational efficiencies. We build from the standpoint that the application is carefully structured for maximum efficiency through the resolver model, and we just want to squeeze as much out of scaling costs a possible.

@yaacovCR yaacovCR force-pushed the batch-resolution branch from 79d3127 to 10f2698 Compare May 24, 2026 14:13
yaacovCR added 2 commits May 24, 2026 18:00
Introduce the experimental field batch resolver execution mode behind enableBatchResolvers: 'field'. The option is typed by BatchResolverLevel, and regular field execution remains the path for fields that do not define experimentalBatchResolve.

Example option:

  execute({ schema, document, enableBatchResolvers: 'field' });

Example field resolver:

  const name = {

    type: GraphQLString,

    experimentalBatchResolve: (sources) =>

      sources.map((source) => source.name),

  };
@yaacovCR yaacovCR force-pushed the batch-resolution branch from 8d09aa1 to d841932 Compare May 26, 2026 07:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

PR: feature 🚀 requires increase of "minor" version number

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants