Relay Modern: Simpler, faster, more extensible

Today, we're releasing Relay Modern, a new version of Relay designed from the ground up to be easier to use, more extensible and, most of all, able to improve performance on mobile devices. In this post, we'll give a brief overview of Relay and then look at what's new in Relay Modern.

Reintroducing Relay

Relay is our JavaScript framework for building data-driven applications. It combines React for composable user interfaces and GraphQL for composable data fetching. While it is possible to use these technologies together without any framework, we found that this approach didn't always scale to meet our needs. For example, as our applications grew larger, it became more challenging to manage network requests, error handling, and data consistency, among other concerns. Standard approaches can also break the encapsulation of code, requiring developers to make modifications across the codebase to achieve even small changes.

Our goal with Relay is to allow developers to move fast by focusing more on building their applications and less on re-implementing these complex and error-prone details. To do so, Relay introduces two concepts: colocated data and view definitions, and declarative data fetching.

Colocation of data and view

When building UIs in React, we describe an entire application as a set of nested components. News Feed is a collection of stories; a story has a header, body, and list of comments; each comment has an author and text; and so on. Each of these pieces of the UI is described by a React component, allowing developers to reason locally about their application. This approach also enables reuse: The same story component used in News Feed might be used for a story detail page.

Similarly, GraphQL fragments are composable units for describing data dependencies. The GraphQL query for News Feed can be broken down into fragments for stories, which are composed of fragments for the header, body, comments, and so on.

As we built Relay, we realized that these concepts could be aligned to form a powerful unit of abstraction: containers that colocate both the view logic and the data dependencies of each component. Here's an example of a Relay Modern container that renders a user's name and profile photo and specifies its data dependencies accordingly:

Relay containers integrate seamlessly with React and GraphQL: They're regular React components that compose together as developers expect, and the GraphQL fragments compose together using standard GraphQL syntax. Since introducing colocated data and views in Relay, we've applied this approach everywhere that we use GraphQL at Facebook, and we recommend colocation to the GraphQL community as a best practice.

Declarative data fetching

In the same way that React allows developers to express what should be rendered rather than how, Relay allows developers to specify what data is required instead of how to load, cache, and update it. In freeing developers from the need to deal with the network directly, Relay helps reduce application complexity and avoid a potential source of bugs and accidental performance issues.

A further benefit of this approach is that improvements to the framework can benefit multiple applications without requiring per-application updates. Overall, we've found declarative data fetching to be one of Relay's main strengths.

Introducing Relay Modern

These two concepts continue to be an important part of Relay's appeal to product developers. In fact, while we originally designed Relay to help us build apps for desktops, tablets, and other high-end devices, today we use Relay in a wide variety of applications, from rich internal web tools to mobile apps built with React Native. However, as we used Relay on a wider range of devices — especially underpowered mobile hardware — we realized some limitations with the original design. In particular, the expressive and flexible nature of the API made it challenging to achieve the level of performance we wanted on some devices.

At the same time, we listened carefully to feedback from internal developers and the community. They indicated that the API was too “magical,” which sometimes made it hard to learn and predict. We realized that a simpler approach could be easier to understand and also allow for greater performance. So we set out to redesign Relay to accommodate these new constraints, looking to our existing native mobile GraphQL apps for inspiration.

The result is Relay Modern, a forward-looking GraphQL framework that incorporates the learnings and best practices from classic Relay, our native mobile GraphQL clients, and the GraphQL community. Relay Modern retains the best parts of Relay — colocated data and view definitions, declarative data fetching — while also simplifying the API, adding features, improving performance, and reducing the size of the framework. To accomplish this, we embraced two concepts: static queries and ahead-of-time optimization.

Static queries

GraphQL has been used in Facebook's native iOS and Android apps since 2012. As Relay sought to improve performance for lower-powered devices on poor mobile connections, we looked to these engineering teams for inspiration, especially when it came to how they leveraged static analysis and ahead-of-time compilation.

The native app teams discovered that using GraphQL came with the additional overhead of building queries by concatenating a bunch of strings and then uploading those queries over slow connections. These queries could sometimes grow into the tens of thousands of lines of GraphQL. Also, every mobile device running the same app was sending largely the same queries.

The teams realized that if the GraphQL queries instead were statically known — that is, they were not altered by runtime conditions — then they could be constructed once during development time and saved on the Facebook servers, and replaced in the mobile app with a tiny identifier. With this approach, the app sends the identifier along with some GraphQL variables, and the Facebook server knows which query to run. No more overhead, massively reduced network traffic, and much faster mobile apps.

Relay Modern adopts a similar approach. The Relay compiler extracts colocated GraphQL snippets from across an app, constructs the necessary queries, saves them on the server ahead of time, and outputs artifacts that the Relay runtime uses to fetch those queries and process their results at runtime.

Ahead-of-time optimization

The Relay compiler uses its static knowledge of the query structure to optimize both of these outputs, helping to improve application performance. Optimizing the query saved to the server means the server may be able to execute it and return a result sooner. Similarly, the optimized artifacts can allow the runtime to process those query results faster as well.

Examples of such optimizations include flattening, which reduces duplicate fields to help avoid redundant data processing at runtime, and constant inlining, in which static knowledge of conditionals may allow some branches of a query to be pruned at compile time.

Combined, these optimizations help to reduce both the time spent fetching and downloading query results as well as time spent processing them.

New features

In addition to these performance improvements, Relay Modern improves existing features and adds some new ones.

Simpler mutations

Even before setting out to make queries statically known, we knew we wanted to simplify how mutations work in Relay. Improvements to simplify the mutations API is a common request we hear from the community.

Classic Relay does have some powerful features when it comes to mutations, if you understand what's happening under the hood. Relay's mutations API allows developers to specify a "fat query" — all the things that might change in response to applying a mutation. When a mutation is executed, classic Relay intersects the fat query with the queries that have actually been executed to determine the minimal set of data to refetch. This approach was convenient in some cases, but it could also be unpredictable and confusing. It was sometimes difficult to tell what mutation would be generated. A further difficulty is that required runtime dynamic queries, limiting the ability to perform static query optimizations.

Relay Modern instead provides an explicit mutation API: Developers specify exactly which fields to fetch after a mutation and exactly how the cache should be updated after the mutation occurs. This helps to both improve performance and make the system more predictable. The API also allows for arbitrary logic for handling mutations, making it possible to handle edge cases (such as client-side sorting of a list of items) that were not supported in the classic API.

Garbage collection

Being a good (application) citizen on mobile devices means managing not only CPU usage but also memory. Accordingly, Relay Modern is designed from the start to support garbage collection — that is, cache eviction — in which GraphQL data that is no longer used by any views can be removed from the cache. This is especially useful for longer-lived applications that might otherwise cause the cache to grow without bound.

Garbage collection is enabled in the core runtime and also carefully integrated into the public API so that developers do not have to explicitly manage cache memory usage.

Extensible and reusable

Relay Modern is capable of much more than bringing React and GraphQL together. With this release, what used to be one library is now three: the compiler, runtime, and React/Relay integration layer. The Relay compiler (npm: relay-compiler) is a generic utility for parsing and optimizing GraphQL and generating code artifacts. The Relay runtime (npm: relay-runtime) is a view library-agnostic generic utility for writing, sending, and caching GraphQL data. Finally, React Relay (npm: react-relay) integrates Relay with React, providing the container API demonstrated above.

This modularity may allow Relay to be used with other view libraries in the future or as a stand-alone library. Relay's compiler is also designed to be extended to add more capabilities based on GraphQL's type system or to be used for other tools beyond app development. We're excited to see where the GraphQL community helps us take these new parts of the Relay suite.

Innovate in place

We're excited about what we've built. But since we're introducing some major changes to how Relay works, it's important to provide an iterative path for existing Relay apps to adopt Relay Modern and provide value along the way. We've learned from React's philosophy of innovating in place that introducing significant changes to software is possible when it's done in an iterative way that doesn't leave anyone behind.

To accomplish this, the latest version of Relay offers a compatibility API. This allows you to begin to use the new Relay Modern APIs in the context of an existing classic Relay application. While using the compatibility API, you won't yet benefit from statically known queries or the smaller core, but will benefit from a simpler and more predictable API. Once all components in a portion of your app have been converted to the modern API, the Relay Modern runtime can be included as well for improved performance and lower overhead.

As an example, we incrementally converted the Marketplace tab of the Facebook app from Relay to Relay Modern, while the team was iterating on features at the same time. When the conversion was complete and we enabled Relay Modern runtime, the product's time to interaction (TTI) on Android improved by an average of 900 ms. Going forward, we're continuing with this process of bringing Relay Modern to our existing applications, and we hope to share some of the tools we build for making this process easier.

Where we go from here

We're excited to share the new Relay Modern APIs, runtime, and compiler with the community on GitHub and npm and to hear your feedback. We're even more excited about sharing this philosophy of how to use GraphQL in sophisticated clients, and we look forward to seeing more tools in the GraphQL community adopt them as well.