Defer and Stream Directives in GraphQL Improve Latency with - - PowerPoint PPT Presentation

defer and stream directives in graphql
SMART_READER_LITE
LIVE PREVIEW

Defer and Stream Directives in GraphQL Improve Latency with - - PowerPoint PPT Presentation

Defer and Stream Directives in GraphQL Improve Latency with Incremental Delivery Liliana Matos, Director, Front-End Engineering @ 1stdibs Rob Richard, Senior Director, Front-End Engineering @ 1stdibs About Online marketplace for luxury


slide-1
SLIDE 1

Liliana Matos, Director, Front-End Engineering @ 1stdibs Rob Richard, Senior Director, Front-End Engineering @ 1stdibs

Defer and Stream Directives in GraphQL

Improve Latency with Incremental Delivery

slide-2
SLIDE 2

About

  • Online marketplace for luxury items

across multiple verticals

  • Front-End stack: Node, GraphQL,

React, and Relay

  • Offices in New York, Vilnius, Lithuania,

and Bangalore, India

slide-3
SLIDE 3

What are @defer and @stream?

  • @defer and @stream are proposed directives to the

GraphQL Specification to support incremental delivery for state-less queries

  • Championing since January 2020
  • In collaboration with GraphQL Working Group
  • In this talk, we will discuss:
  • Motivation
  • Specification proposal overview
  • Code Examples
  • Reference implementation in GraphQL.js
  • Open-source contribution
  • Best practices

query TalksQuery { talks(first: 6) @stream ( label: "talkStream", initialCount: 3 ) { name ...TalkComments @defer(label: "talkCommentsDefer") } } fragment TalkComments on Talk { comments { body } }

slide-4
SLIDE 4

Why @defer and @stream?

  • Large datasets may suffer from latency
  • All requested data may not be of equal importance
  • Current options for applications to prioritize data, such as query splitting and

pre-fetching, come with undesirable trade-offs

  • @defer and @stream would allow GraphQL clients to communicate priority of

requested data to the server without undesirable trade-offs

slide-5
SLIDE 5

Query Splitting

  • Fetch expensive/non-essential fields in a

separate query after initial query

  • Trade-offs:
  • Increased latency for lower priority

fields

  • Client resource contention
  • Increased server cost

/** Original Query */ query SpeakerQuery($speakerId: String!) { speaker(speakerId: $speakerId) { name ...SpeakerPicture } } fragment SpeakerPicture on Speaker { picture { height width
 url } } /** Split Queries */ query SpeakerInitialQuery($speakerId: String!) { speaker(speakerId: $speakerId) { name } } query SpeakerFollowUpQuery($speakerId: String!) { speaker(speakerId: $speakerId) { ...SpeakerPicture } }

slide-6
SLIDE 6

Pre-fetching

  • Optimistically fetching data based on a prediction that a user will execute an

action

  • Trade-offs:
  • Increased server cost due to incorrect predictions
slide-7
SLIDE 7

https://www.apollographql.com/blog/introducing-defer-in-apollo-server-f6797c4e9d6e/

slide-8
SLIDE 8

What about Subscriptions?

  • Intention is for real-time and long connections
  • @defer and @stream, intention is to lower latency for short-lived connections
slide-9
SLIDE 9

Specification Proposal for @defer and @stream

slide-10
SLIDE 10

@defer

  • The @defer directive may be specified on a fragment

spread.

  • if: Boolean
  • When true fragment may be deferred, if omitted

defaults to true.

  • label: String
  • A unique label across all @defer and @stream

directives in an operation.

directive @defer(if: Boolean, label: String) on FRAGMENT_SPREAD

slide-11
SLIDE 11

@defer

Example

query SpeakerQuery($speakerId: String!) { speaker(speakerId: $speakerId) { name ...SpeakerPicture @defer(label: “speakerPictureDefer”) } } fragment SpeakerPicture on Speaker { picture { height width
 url } } // Response Payloads // Payload 1 { "data": { "speaker": { "name": "Jesse Rosenberger" } }, "hasNext": true } // Payload 2 { "label": "speakerPictureDefer", "path": ["speaker"], "data": { "picture": { "height": 200, "width": 200, "url": "jesse-headshot.jpg" } }, "hasNext": false }

slide-12
SLIDE 12

@stream

  • The @stream directive may be provided for a field of List
  • if: Boolean
  • When true fragment may be deferred, if omitted defaults to true.
  • label: String
  • A unique label across all @defer and @stream directives in an
  • peration.
  • initialCount: Int
  • The number of list items the server should return as part of the

initial response.

directive @stream(if: Boolean, label: String, initialCount: Int) on FIELD

slide-13
SLIDE 13

@stream

Example

query SpeakersQuery { speakers(first: 3) @stream(label: “speakerStream", initialCount: 1) { name } } // Response Payloads // Payload 1 { "data": { "speakers": [ { "name": "Jesse Rosenberger" } ] }, "hasNext": true } // Payload 2 { "label": "speakerStream", “path": ["speakers", 1], "data": { "name": "Liliana Matos" }, "hasNext": true } // Payload 3 { "label": "speakerStream", "path": ["speakers", 2], "data": { "name": "Rob Richard" }, "hasNext": false }

slide-14
SLIDE 14

Response Format

Overview

  • When an operation contains @defer or @stream directives, the GraphQL

execution will return multiple payloads

  • The first payload is the same shape as a standard GraphQL response
  • Any fields that were only requested on a fragment that is deferred will not be

present in this payload

  • Any list fields that are streamed will only contain the initial list items
slide-15
SLIDE 15

Response Format

Details

  • label — The string that was passed to the label argument of the @defer or @stream

directive that corresponds to this results

  • path — A list of keys from the root of the response to the insertion point
  • hasNext — A boolean that is present and true when there are more payloads that will be

sent for this operation.

  • data — The data that is being delivered incrementally.
  • errors — An array of errors that occurred while executing deferred or streamed selection

set

  • extensions — For implementors to extend the protocol
slide-16
SLIDE 16

Response Format

Example

query SpeakersQuery { speakers(first: 2) @stream(label: "speakerStream", initialCount: 1) { name ...SpeakerPicture @defer(label: "speakerPictureDefer") } } fragment SpeakerPicture on Speaker { picture { url } }

// Response Payloads // Payload 1 { "data": { "speakers": [ { "name": "Jesse Rosenberger" } ] }, "hasNext": true } // Payload 2 { "label": "SpeakerPictureDefer", "path": ["speakers", 0, "picture"], "data": { "url": "jesse-headshot.jpg" }, "hasNext": true } // Payload 3 { "label": "SpeakerStream", "path": ["speakers", 1], "data": { "name": "Liliana Matos" }, "hasNext": true } // Payload 4 { "label": "SpeakerPictureDefer", "path": ["speakers", 1, "picture"], "data": { "url": "liliana-headshot.jpg" }, "hasNext": false }

slide-17
SLIDE 17

How to use @defer and @stream in your GraphQL Server

  • @defer - existing resolvers will work effectively
  • @stream - need to consider how resolvers return data
slide-18
SLIDE 18

Execution

fragment SpeakerPicture on Speaker { picture { height width
 url } } { "data": { "speaker": { "name": "Jesse Rosenberger" } }, "hasNext": true } query SpeakerQuery($speakerId: String!) { speaker(speakerId: $speakerId) { name ...SpeakerPicture @defer(label: “speakerPictureDefer”) } } { "label": "speakerPictureDefer", "path": ["speaker"], "data": { "picture": { "height": 200, "width": 200, "url": "jesse-headshot.jpg" } }, "hasNext": false }

Fork execution to dispatcher Initial Payload Subsequent Payload

slide-19
SLIDE 19

How to use @stream in your GraphQL Server

  • Any List field can use the @stream directive
  • What you return from your resolver matters
slide-20
SLIDE 20

List return types in GraphQL-JS

  • GraphQL-JS supports returning several different data types in List resolvers.
  • Array<T>, any Iterable, Promise<Iterable<T>>
  • GraphQL engine will get all results at once
  • Initial payload will be held up by this resolver
  • Subsequent payloads will be sent immediately after

const resolvers = { Query: { items: async function (_, { filters }): Promise<Array<Item>> { const items = await api.getFilteredItems({ filters }); return items; }, }, };

slide-21
SLIDE 21

List return types in GraphQL-JS

  • Array<Promise<T>>
  • GraphQL engine will start waiting for all results
  • Initial payload will be sent as soon as the "initialCount" values are ready
  • Subsequent payloads will be sent as each promise resolves
  • Requires knowing how many results there will be before the resolver returns
slide-22
SLIDE 22

Returning Array<Promise<T>>

const resolvers = { Query: { items: async function (_, { filters }): Array<Promise<<Item>> { const itemIds = await api.filterItems({ filters }); return itemIds.map(async itemId => await api.getItemById(itemId)); }, }, };

slide-23
SLIDE 23

List return types in GraphQL-JS

  • AsyncIterable, Async Generator function
  • GraphQL engine will yield each result from the iterable
  • Initial payload will be sent as soon as the "initialCount" values are ready
  • Subsequent payloads will be sent as each new value is yielded
  • Can determine asynchronously if the list is completed
slide-24
SLIDE 24

Async Generator Function Resolver

const resolvers = { Query: { users: async function* (): AsyncIterable<User> { const db = new Database(); while (true) { // select one document const result = await db.getNext(); // end iteration if there are no documents returned if (!result) { break; } yield result; } return; }, }, };

slide-25
SLIDE 25

Server - Client Communication

  • No websockets or any other stateful connection mechanism
  • Works with common infrastructure and old browsers
  • Spec is transport-agnostic, so you could use websockets
slide-26
SLIDE 26

"Chunked transfer encoding (CTE) is a mechanism in which the encoder sends data to the player in a series of chunks. The player doesn’t have to wait until the complete segment is available"

transfer-encoding: chunked

slide-27
SLIDE 27

Multipart HTTP

  • Standard for encoding multiple payloads in a single HTTP request response
  • Used for File Uploads to attach binary file data to form requests
  • Used for Emails to add attachments to email body
  • GraphQL Response can be encoded as multipart data of multiple JSON payloads
slide-28
SLIDE 28

Multipart HTTP

  • Content-Type: application/json

Content-Length: 590 { "data": { "talks": [{ "title": "Opening Keynote", "time": "10:30-11:00", "speaker": { "name": "Jesse Rosenberger" }, }, ...] } }

  • Content-Type: application/json

Content-Length: 140 { "path": ["talks", 0, "comments"], "data": [{ "body": "Loved this!" }] }

query ConferenceQuery { talks { title time speaker { name } ...commentsFragment @defer } } fragment commentsFragment on Talk { body }

slide-29
SLIDE 29
slide-30
SLIDE 30

GraphQL over HTTP

  • GraphQL over HTTP is a proposed specification to define how GraphQL should

be served over HTTP

  • We have an RFC to add incremental delivery to this spec using chunked

encoding and multipart responses

  • https://github.com/graphql/graphql-over-http/pull/124
slide-31
SLIDE 31

Server Code

function sendPartialResponse( response: $Response, result: AsyncExecutionResult, ): void { const json = JSON.stringify(result, null, 2); const chunk = Buffer.from(json, 'utf8'); const data = [ '', '---', 'Content-Type: application/json; charset=utf-8', 'Content-Length: ' + String(chunk.length), '', chunk, '', ].join('\r\n'); response.write(data); } // Close connection response.end();

slide-32
SLIDE 32

Client Code - Fetch

const response = await fetch(url, { method, headers, body }) // Don't call response.json()! // That waits for the whole response to finish loading. // const json = await response.json() const reader = response.body.getReader(); while (true) { const { value, done } = await reader.read(); if (done) return; handleChunk(value); }

slide-33
SLIDE 33

Client Code - XMLHttpRequest

// This works in Internet Explorer 7! const xhr = new XMLHttpRequest(); let index = 0; xhr.open(method, url); xhr.addEventListener("readystatechange", function onReadyStateChange() { const chunk = xhr.response.substr(index); handleChunk(chunk); index = xhr.responseText.length; }); xhr.send(body);

slide-34
SLIDE 34

Open Source Implementation

fetch-multipart-graphql

  • https://github.com/relay-tools/fetch-multipart-graphql
slide-35
SLIDE 35

References

  • Spec RFC: https://github.com/graphql/graphql-spec/blob/master/rfcs/

DeferStream.md

  • Spec Proposal: https://github.com/graphql/graphql-spec/pull/742
  • GraphQL-JS: https://github.com/graphql/graphql-js/pull/2319
  • express-graphql: https://github.com/graphql/express-graphql/pull/583
  • fetch-multipart-graphql: https://github.com/relay-tools/fetch-multipart-graphql
  • GraphQL over HTTP RFC: https://github.com/graphql/graphql-over-http/pull/124
slide-36
SLIDE 36