Enabling Defer and Stream
@defer, @stream, and experimentalExecuteIncrementally() are available
in GraphQL.js v17 and newer. Incremental delivery is pending GraphQL
specification work.
@defer and @stream allow a GraphQL operation to produce an initial result
and later incremental payloads. This is useful when part of a response is slow,
large, or naturally delivered over time.
GraphQL.js keeps this feature explicit. You must add the directives to the schema and use the experimental executor.
Add the directives to your schema
If the directives option is passed to GraphQLSchema, the default directive
list is replaced. Include specifiedDirectives when adding experimental
directives.
import {
GraphQLDeferDirective,
GraphQLSchema,
GraphQLStreamDirective,
specifiedDirectives,
} from 'graphql';
const schema = new GraphQLSchema({
query,
directives: [
...specifiedDirectives,
GraphQLDeferDirective,
GraphQLStreamDirective,
],
});Once a schema includes @defer or @stream, execute operations against that
schema with experimentalExecuteIncrementally(). execute() is the
single-result executor and will reject schemas that contain the experimental
incremental directives.
Execute incrementally
import { experimentalExecuteIncrementally, parse } from 'graphql';
const document = parse(`
query ProductPage {
product(id: "abc") {
id
name
...Reviews @defer(label: "reviews")
}
}
fragment Reviews on Product {
reviews {
body
rating
}
}
`);
const result = await experimentalExecuteIncrementally({
schema,
document,
});The result is either a normal ExecutionResult or an incremental result object.
if ('initialResult' in result) {
sendInitialPayload(result.initialResult);
for await (const subsequentResult of result.subsequentResults) {
sendIncrementalPayload(subsequentResult);
}
} else {
sendSinglePayload(result);
}GraphQL.js produces the execution results; your server transport is responsible for serializing and delivering them to the client.
Transport framing guidance
experimentalExecuteIncrementally() gives you result objects, not a wire
protocol. Pick a transport framing format that your clients already support and
test it end to end.
- HTTP multipart responses for clients that support incremental patches.
- Server-sent events when your stack already uses event streams.
- WebSocket message streams for subscription-like transports.
Keep transport concerns separate from execution concerns: validate operation behavior first, then validate framing and client reassembly separately.
@defer
@defer can be applied to fragment spreads and inline fragments. It defers the
fragment when if is true or omitted.
query ProductPage($includeReviews: Boolean! = true) {
product(id: "abc") {
id
name
...Reviews @defer(if: $includeReviews, label: "reviews")
}
}The label argument is optional, but labels must be unique for active
@defer and @stream usages in the operation.
@stream
@stream can be applied to list fields. It sends initialCount items in the
initial result and streams later items in subsequent payloads.
query Feed {
feed(first: 100) @stream(initialCount: 10, label: "feed") {
id
title
}
}initialCount is non-null and defaults to 0.
Resolvers may return normal iterables, promises, or async iterables for list
fields. Async iterables are especially useful with @stream because the
executor can complete list items as they become available.
Early execution
enableEarlyExecution allows deferred work to begin before all non-deferred
work has completed.
const result = await experimentalExecuteIncrementally({
schema,
document,
enableEarlyExecution: true,
});This can reduce total latency for expensive deferred sections, but it can also increase concurrent work. Measure before enabling it broadly.
Validation limits
GraphQL.js validates the current incremental delivery rules:
@deferand@streamare not supported on subscription operations.@streammust be used on list fields.@stream(initialCount:)must be non-null.- Active
@deferand@streamlabels must be unique. - Root field usage must follow the current proposal rules.
- Multiple active
@streaminstances cannot target the same field instance.
If a fragment is shared between query and subscription operations, use the
directive if argument to disable incremental behavior in the subscription.
subscription Events($incremental: Boolean! = false) {
event {
...EventFields @defer(if: $incremental)
}
}Cancellation
Incremental execution accepts abortSignal. Aborting stops new payload
production and attempts to close async iterators.
const controller = new AbortController();
const result = await experimentalExecuteIncrementally({
schema,
document,
abortSignal: controller.signal,
});