Deferred queries

Optimize data loading with the @defer directive

Setting up

Note: @defer support is an experimental feature that is only available in the alpha preview of Apollo Server and Apollo Client.

  • On the server:

      npm install apollo-server@alpha
    
  • On the client, if you are using Apollo Boost:

      npm install apollo-boost@alpha react-apollo@alpha
    

    Or if you are using Apollo Client:

      npm install apollo-client@alpha apollo-cache-inmemory@alpha apollo-link-http@alpha apollo-link-error apollo-link
    

The @defer Directive

Many applications that use Apollo fetch data from a variety of microservices, which may each have varying latencies and cache characteristics. Apollo comes with a built-in directive for deferring parts of your GraphQL query in a declarative way, so that fields that take a long time to resolve do not need to slow down your entire query.

There are 3 main reasons why you may want to defer a field:

  1. Field is expensive to load. This includes private data that is not cached (like user progress), or information that requires more computation on the backend (like calculating price quotes on Airbnb).
  2. Field is not on the critical path for interactivity. This includes the comments section of a story, or the number of claps received.
  3. Field is expensive to send. Even if the field may resolve quickly (ready to send back), users might still choose to defer it if the cost of transport is too expensive.

As an example, take a look at the following query that populates a NewsFeed page:

query NewsFeed {
  newsFeed {
    stories {
      text
      comments {
        text
      }
    }
    recommendedForYou {
      story {
        text
        comments {
          text
        }
      }
      matchScore
    }
  }
}

It is likely that the time needed for different fields in a query to resolve are significantly different. stories is highly public data that we can cache in CDNs (fast), while recommendedForYou is personalized and may need to be computed for every user (slooow). Also, we might not need comments to be displayed immediately, so slowing down our query to wait for them to be fetched is not the best idea.

How to use @defer

We can optimize the above query with @defer:

query NewsFeed {
  newsFeed {
    stories {
      text
      comments @defer {
        text
      }
    }
    recommendedForYou @defer {
      story {
        text
        comments @defer {
          text
        }
      }
      matchScore
    }
  }
}

Once you have added @defer, Apollo Server will return an initial response without waiting for deferred fields to resolve, using null as placeholders for them. Then, it streams patches for each deferred field asynchronously as they resolve.

// Initial response
{
  "data": {
    "newsFeed": {
      "stories": [{ "text": "...", "comments": null }],
      "recommendedForYou": null
    }
  }
}
// Patch for "recommendedForYou"
{
  "path": ["newsFeed", "recommendedForYou"],
  "data": [
    {
      "story": {
        "text": "..."
      },
      "matchScore": 99
    }
  ]
}
// Patch for "comments", sent for each story
{
  "path": ["newsFeed", "stories", 1, "comments"],
  "data": [
    {
      "text": "..."
    }
  ]
}

If an error is thrown within a resolver, the error gets sent along with its closest deferred parent, and is merged with the graphQLErrors array on the client.

// Patch for "comments" if there is an error
{
  "path": ["newsFeed", "stories", 1, "comments"],
  "data": null,
  "errors": [
    {
      "message": "Failed to fetch comments"
    }
  ]
}

Distinguishing between “pending” and “null”

You may have noticed that deferred fields are returned as null in the initial response. So how can we know which fields are pending so that we can show some loading indicator? To deal with that, Apollo Client now exposes field-level loading information in a new property called loadingState that you can check for in your UI components. The shape of loadingState mirrors that of your data. For example, if data.newsFeed.stories is ready, loadingState.newsFeed.stories will be true.

You can use it in a React component like this:

<Query query={query}>
  {({ loading, error, data, loadingState }) => {
    if (loading) return 'loading...';
    return loadingState.newsFeed.recommendedForYou
      ? data.newsFeed.recommendedForYou
        ? data /* render component here */
        : 'No recommended content'
      : 'Loading recommended content';
  }}
</Query>

Where is @defer allowed?

  • @defer can be applied on any FIELD of a Query operation. It also takes an optional argument if, that is a boolean controlling whether it is active, similar to @include.

  • @include and @skip take precedence over @defer.

  • Mutations: Not supported.

  • Non-Nullable Types: Not allowed and will throw a validation error. This is because deferred fields are returned as null in the initial response. Deferring non-nullable types may also lead to unexpected behavior when errors occur, since errors will propagate up to the nearest nullable parent as per the GraphQL spec. We want to avoid letting errors on deferred fields clobber the initial data that was loaded already.

  • Nesting: @defer can be nested arbitrarily. For example, we can defer a list type, and defer a field on an object in the list. During execution, we ensure that the patch for a parent field will be sent before its children, even if the child object resolves first. This will simplify the logic for merging patches.

  • GraphQL fragments: Supported. If there are multiple declarations of a field within the query, all of them have to contain @defer for the field to be deferred. This could happen if we have use a fragment like this:

      fragment StoryDetail on Story {
        id
        text
      }
      query {
        newsFeed {
          stories {
            text @defer
            ...StoryDetail
          }
        }
      }
    

    In this case, text will not be deferred since @defer was not applied in the fragment definition.

    A common pattern around fragments is to bind it to a component and reuse them across different parts of your UI. This is why it would be ideal to make sure that the @defer behavior of fields in a fragment is not overridden.

Transport

There is no additional setup for the transport required to use @defer. By default, deferred responses are transmitted using Multipart HTTP. For browsers that do not support the ReadableStream API used to read streaming responses, we will just fallback to normal query execution ignoring @defer.

Performance Considerations

@defer is one of those features that work best if used in moderation. If it is used too granularly (on many nested fields), the overhead of performing patching and re-rendering could be worse than just waiting for the full query to resolve. Try to limit @defer to fields that take a significantly longer time to load. This is super easy to figure out if you have Apollo Engine set up!

Use with other GraphQL servers

If you are sending queries to a GraphQL server that does not support @defer, it is likely that the @defer directive is simply ignored, or a GraphQL validation error is thrown.

If you would want to implement a GraphQL server that is able to interoperate with Apollo Client, please look at the documentation here.

Edit on GitHub
// search box