Slide 1

Slide 1 text

Joseph Savona Software Engineer, Facebook Reintroducing Relay Hi! My name is Joe Savona. I'm a software engineer at Facebook working on the Relay core team.

Slide 2

Slide 2 text

Agenda • Quick Recap • Next Steps for Relay This talk is in two main parts First I'll give a quick overview of what Relay is Some of you may be familiar with Relay, 
 but I want to give a quick overview to make sure we're all on the same page
 The bulk of the talk will be about some new things we're working on. More on that in a second

Slide 3

Slide 3 text

Relay is a framework for building data-driven applications with React and GraphQL First let's recap. ((READ SLIDE OUT LOUD)) Goal is for developers to specify what data is needed for each view And Relay figures out how and when to fetch that data It lets product developers focus on the product, not on data-handling logic

Slide 4

Slide 4 text

Render React Component In Relay, each unit of our application - the component

Slide 5

Slide 5 text

Render Query Can specify its data dependencies via GraphQL Think of this as a more advanced React PropTypes

Slide 6

Slide 6 text

let UserProfile = (props) => (

{props.user.name}

); As a concrete example here's a profile component that shows the user's name and profile picture

Slide 7

Slide 7 text

UserProfile = Relay.createContainer(UserProfile, { fragments: { user: RelayQL` fragment on User { name profilePicture { uri } } `, ... Relay lets us annotate that component with GraphQL fragments Notice that the shape of this fragment matches the data that the component expects

Slide 8

Slide 8 text

Render Props Query Query Relay can aggregate the data dependencies from all our components, send them to the server, cache the responses, and render data to each of our components

Slide 9

Slide 9 text

At Facebook • Groups App • Ads Manager App • Facebook App • Website • Internal Tools • Etc. As you can see, we're using Relay in quite a few places internally - this is just a sampling

Slide 10

Slide 10 text

In Open Source • 6.8k watchers • 560 forks • 675 pull requests • 80+ external contributors We've also seen a lot of interest from the community We're especially excited to see lots of substantial pull requests from the community Adding things like server rendering and React Native compatibility

Slide 11

Slide 11 text

Stepping Back Overall, we're pleased with the progress that we've made But we're just getting started. We've gotten a lot of great feedback about what works well and what could be improved
 We've also begun to explore new use cases where Relay can help improve our apps.

Slide 12

Slide 12 text

Mobile Perf In emerging markets, a lot of users are on 2G or 3G networks and are using older phones. At Facebook we've developed a system for categorizing devices - and many users in these markets use 2011 "year-class" devices. These are devices that would have been considered high-end in 2011.
 
 Our goal is to support developers in building applications that work well on low-end devices connecting on low-end networks. This means doubling-down on performance and better support for working offline.

Slide 13

Slide 13 text

Developer Experience We've gotten a lot of good feedback about the developer experience.
 Relay provides a lot of power, but it could definitely be easier to use.
 Turns out we can improve speed and developer experience by making a few key aspects more explicit.
 
 (notes: A more explicit API means less work for the framework (faster) and, by definitions means that developer is specifying intent more, making the effect of the code clearer (simpler/more predictable). Yes, the developer does a bit more work, but we've found that this is a good tradeoff in order to gain performance and clarity)

Slide 14

Slide 14 text

Big Bets... These are ambitious goals
 Iterative improvements would only get us so far, so fast

Slide 15

Slide 15 text

Relay 2 This brings us to Relay 2
 This is a ground up rewrite of Relay
 Optimized for low-end mobile devices
 Also focused on simpler and more predictable developer experience

Slide 16

Slide 16 text

Relay 2: Best Parts of Relay • Colocated GraphQL with React components • Declarative API We're keeping the best parts of Relay today
 
 Colocating GraphQL fragments with our components makes it easy to reason about our applications
 
 The declarative API lets us focus on the product, not on data-handling.

Slide 17

Slide 17 text

Relay 2: What's New • Static Queries • Arbitrary Mutations • Client State • Garbage Collection a.k.a Cache Eviction • Offline caching • Connection Streaming • Deferred queries (new to OSS) Relay 2 also introduces a lot of new features and improvements

Slide 18

Slide 18 text

Time to Interaction A metric that we care a lot about is Time To Interaction The time from when an app or transition starts, until the first content can be interacted with
 An example is a feed of content. TTI might measure the time until we are able to like or comment on the first item.
 Let's start by looking at TTI in React Native native apps, then web.

Slide 19

Slide 19 text

JS Initialization JS Fetch Process Render React Native & Relay Data Fetching First our code is loaded from disk and is prepared to run
 Then the code runs, figures out the data we need, and fetches it
 When the response is received from the server we process that data - putting it into our cache Then we can render

Slide 20

Slide 20 text

JS Download & Init JS Fetch Process Render Relay Web Data Fetching And this same process plays out on the web too. The only real difference is that we have to download all the javascript first. 
 
 For Relay 2 we considered how we could improve this flow.
 Certainly we could optimize individual steps - and we did.

Slide 21

Slide 21 text

JS Initialization JS Fetch Process Render Relay Data Fetching But let's consider the dependency between initialization and data fetching
 Our code expresses its data dependencies in GraphQL GraphQL is static - it allows us to describe the data we need as text, without JavaScript logic

Slide 22

Slide 22 text

Static Queries If we could fully exploit this property of GraphQL, we could potentially execute the query much earlier In current Relay though, we have some dynamic APIs that prevent us from doing this. The same is true of some other JS GraphQL clients.

Slide 23

Slide 23 text

Relay.createContainer(FriendsList, { initialVariables: { count: 5, }, fragments: { user: RelayQL` fragment on User { friends(first: $count) { ${FriendEdge.getFragment('edge')} } } ` ... Dynamic Queries Here's an example of what I mean in current Relay
 It's a component that shows a list of the user's friends In orange, you can see that the initial number of friends to fetch is specified as a JavaScript object
 In blue, you can see that we're composing a child fragment via string interpolation - also JavaScript.
 We can't just look at the code and know what the query will be

Slide 24

Slide 24 text

Relay2.createContainer(FriendsList, { user: RelayQL` fragment FriendsList on User @argumentDefinitions( count: {type: "Int", defaultValue: 5} ) { friends(first: $count) { ...FriendEdge } } `, }); Static Queries In Relay2, we've embraced the static nature of GraphQL. For the few places where we do need some dynamicism, we've added Relay-specific directives. We're effectively polyfilling GraphQL. To interpret this, we built a GraphQL compiler.

Slide 25

Slide 25 text

Relay2 Compiler GraphQL
 Document GraphQL
 Document Documents Compiler GraphQL GraphQL
 Document GraphQL
 Document Transforms The compiler understands a few Relay-specific directives
 Applies a bunch of transformations to optimize the query
 Such as avoiding redundant data-fetching and adding primary key fields to help identify data in the response The output is plain GraphQL that any server can understand

Slide 26

Slide 26 text

JS Initialization JS Fetch Process Render Relay Data Fetching Going back to our earlier example - we can now break this dependency. 
 We know what screen will be displayed first
 And we can know statically what query will be requested for that screen


Slide 27

Slide 27 text

JS Initialization JS Fetch Process Render JS Initialization Native Fetch Process Render (ready) with Native Prefetching React Native & Relay Data Fetching This allows us to begin fetching the query via native code in parallel with JavaScript initialization

Slide 28

Slide 28 text

JS Download & Init JS Fetch Process Render JS Download & Init Preload Process Render (ready) with Server Preloading Relay Web Data Fetching Or have the server begin preparing and sending our data while the javascript is downloading.
 


Slide 29

Slide 29 text

Static Optimizations So we can see that static queries allows us to skip doing some work at runtime But it also lets us optimize the work that remains.

Slide 30

Slide 30 text

Time To Process a Complex Feed Story Relay 0.9.x ~ 50ms on 2011 Year-Class Device As a comparison, we benchmarked the time to process the results of a complex NewsFeed query. This is a query from a real app, using real data and running on a low- end mobile device.
 
 In current Relay, processing a single story takes about 50ms.

Slide 31

Slide 31 text

Time To Process a Complex Feed Story Relay 0.9.x ~ 50ms Relay 2 ~ 5ms on 2011 Year-Class Device Thanks to the compile-time optimizations, that takes about 5ms in Relay2 on the same device.
 That's a pretty big deal - it means that we can fit processing of complex data in less than a frame, leaving time for animations and rendering.

Slide 32

Slide 32 text

Relay 2: Static Queries • Less work at runtime • Remaining work is faster • Still more optimizations to unlock

Slide 33

Slide 33 text

Client State Next let's look at client state - data that is local to a particular device or session and that we don't want to sync with the server.

Slide 34

Slide 34 text

JS Initialization Prefetch Relay Data Fetching Process Render Let's consider how this type of data can affect TTI
 So far we've optimized things to load server data efficiently

Slide 35

Slide 35 text

JS Initialization Prefetch Relay Data Fetching with Flux/Redux/etc Prep Stores Process Render But if we introduce some local state into a view, we're now back in the same situation as before
 The application has to load more javascript and prepare that data to render
 We've found, though, that using Relay means that there's a lot less need for client state. Since Relay handles loading states, errors, rollback, and more, what's left is just the actual domain data.

Slide 36

Slide 36 text

JS Initialization Prefetch Relay Data Fetching with Flux/Redux/etc Prep Stores Process Render JS Initialization Prefetch Relay Client State Load Process Render If instead we can have Relay manage that state, we can optimize loading that data. The potential performance wins here could vary widely depending on how much client state is fetched and which approach you're currently using. But we think it's simpler to have an integrated approach to client and server state.

Slide 37

Slide 37 text

Relay2.createContainer(StoryHeader, { story: RelayQL` fragment StoryHeader on Story { createdAt hasViewerSeen # client field } `, }); Here's an example of how client State works in Relay2
 We have a story header component that will show a different UI if the user has seen the story on this device.
 So we express that we depend on this field

Slide 38

Slide 38 text

Relay2.createContainer(StoryHeader, { story: RelayQL` fragment StoryHeader on Story { createdAt hasViewerSeen # client field } `, }) // Extend server schema: RelayQL` extend type Story { hasViewerSeen: Boolean } ` This field doesn't exist in the server schema, though. Instead, we define a client extension of the schema. This is standard GraphQL syntax, and allows us to augment the server with client fields
 This field isn't queried from the server, but is cached and loaded by Relay locally.
 
 Now you might be wondering how we set the value of this field.

Slide 39

Slide 39 text

const environment = new Relay2Environment(); environment.write(updater => { const story = updater.get(storyID); story.setValue('hasViewerSeen', true); }); We've also developed a fluent API for writing data to the store.
 Here we provide a function that is given a view of the store, to which we can write to. We load the story by its ID And set the value of the field.

Slide 40

Slide 40 text

Arbitrary Mutations This brings us to mutations. We've just seen an example of how they work in Relay2, but let's look at how that updater actually works.

Slide 41

Slide 41 text

Store Proxy user = updater.get(id); When we call get(), we're actually getting a proxy over the real store

Slide 42

Slide 42 text

Store ChangeSet Proxy user = updater.get(id);
 
 user.setValue( 'clientStatus', 'Hi React Rally!' ); And when we call a set() function, that writes data into a change set. 
 It's a description of the changes that we want to apply - note that the store is not modified directly.

Slide 43

Slide 43 text

Store ChangeSet Proxy Backup In fact, Relay also creates a backup changeset.

Slide 44

Slide 44 text

Store ChangeSet Backup When the mutation finishes the changes is applied to the store

Slide 45

Slide 45 text

Store ChangeSet Backup And views update

Slide 46

Slide 46 text

Store Backup But if we revert the mutation, Relay can apply the backup and bring the store back to its original state.
 
 Note that these change sets are data - so we can keep a queue of them and apply or rollback more than one.

Slide 47

Slide 47 text

Mutable Data, Immutable Architecture In many ways, this gives us the best of both mutability and immutability. In general we get the higher-performance of writing to mutable objects. But at the application level we get the benefits of an immutable architecture such undo/redo changes when necessary.

Slide 48

Slide 48 text

Relay 2: Mutations • Imperative/OO-style API • Creates a description of the changes • Supports rollback & replay (undo/redo)

Slide 49

Slide 49 text

Garbage Collection Next let's look at garbage collection

Slide 50

Slide 50 text

(transition) Fetch Process Relay Data Fetching Render This time let's consider what happens when an application has been open for a while, and the user has loaded a lot of data. 
 If the user transitions to a new view, that causes even more data to be loaded

Slide 51

Slide 51 text

(transition) Fetch Process JS GC Relay Data Fetching Render This can increase memory pressure and cause the JS VM to run garbage collection. Note garbage collection is typically fast - but on a low-end device, that can still impact the perceived performance of our app

Slide 52

Slide 52 text

(transition) Fetch Process JS GC Relay Data Fetching Render (transition) Fetch Process With Relay GC Render Relay GC (note: These two GCs are different The JS GC manages all the data of our application and is implemented in native code. Relay GC managed a subset of that data, releasing references to native objects so that they can be freed. Although Relay's GC can be slower, we can run it at more opportune times (while showing a loading spinner) to help avoid JS GC at an inopportune time).

Slide 53

Slide 53 text

Here's how Relay's GC works
 On the right we have a tree of React components On the left is the graph of data backing those components

Slide 54

Slide 54 text

Now imagine that two media components in red are about to be unmounted The data used by those component is also highlighted in red

Slide 55

Slide 55 text

When the components are unmounted, the data doesn't immediately disappear. It remains in the cache, and can be used to speed up subsequent requests


Slide 56

Slide 56 text

But if we want to free memory we can run Relay2's GC, which sees that this data is not needed anymore and can be released.

Slide 57

Slide 57 text

Relay 2: Garbage Collection • Evict objects not referenced by views • Optional; Product is in control of timing • Zero overhead when not used • Exploring TTL

Slide 58

Slide 58 text

(Offline) Disk Caching Next let's look at disk caching

Slide 59

Slide 59 text

Native Fetch Process Render 2G/3G Networks JS Initialization (idle) On slower networks the query can take a long time to download, even with native prefetching
 
 If we can cache data locally, we can show cached data while fetching fresh data in the background

Slide 60

Slide 60 text

JS Initialization Native Fetch Process Render 2G/3G Networks (idle) with Disk Cache JS Initialization Native Fetch Process Render Render Cached This allows the user to view and interact with cached content quickly, and then see fresh data when its available.

Slide 61

Slide 61 text

environment = Relay2Environment.create({ load: (key, callback) => ..., save: (key, value) => ..., ... }); The API for this is straightforward: the user provides two functions. load() takes a key and a callback, it's your job to load the data for that key from wherever you saved it
 
 save() takes a key and an opaque object, it's your job to save that data somewhere so that you can retrieve it later when Relay calls load()

Slide 62

Slide 62 text

actor { ... } > load(actorID) Let's see how that works. Imagine that we execute a query and the first field is 'actor' Relay will call the load function to get the data for that record

Slide 63

Slide 63 text

actor { hometown friends(...) } > load(townID)
 > load(friendsID) Once the load completes, Relay will continue to the next level of fields And attempt to fetch them in parallel - calling load for both the hometown id and the friends id

Slide 64

Slide 64 text

> save(id) user = updater.get(id);
 
 user.setValue( 'clientStatus', 'Hi React Rally!' ); A similar process works for writes. When new data arrives or a record is changed, Relay calls save() with the new or updated record

Slide 65

Slide 65 text

Relay 2: Disk Caching • Maximum Parallelism • Load only what is required for a view • Orchestrated by Relay • User provides load/save functions

Slide 66

Slide 66 text

Connection Streaming Another cool thing is connection streaming

Slide 67

Slide 67 text

Fetch Paginated Lists Process (2) Render (2) A typical approach to loading a paginated list might look as follows We fetch a few items - say 2 - process them, and render them

Slide 68

Slide 68 text

Fetch Paginated Lists Process (2) Render (2) One problem with this approach is that we don't show the first new item until after we've fetched and processed all the items

Slide 69

Slide 69 text

Fetch Incremental Pagination Lists Paginated Lists Process (2) Render (2) Fetch Process Render Process Render In Relay 2 we've built an API where developers can tell Relay how to execute their query incrementally
 
 This means we can fetch, process, and render data incrementally, showing the first new item sooner.

Slide 70

Slide 70 text

environment = Relay2Environment.create({ fetch: (query, vars) => ..., fetchBatch: (queryBatch) => ..., ... });

Slide 71

Slide 71 text

Relay 2: Overview • Static Queries • Arbitrary Mutations • Client State • Garbage Collection a.k.a Cache Eviction • Offline caching • Connection Streaming • Deferred queries (new to OSS)

Slide 72

Slide 72 text

Simpler Relay 2 is simpler

Slide 73

Slide 73 text

Faster It's faster

Slide 74

Slide 74 text

More Predictable it's more predictable

Slide 75

Slide 75 text

Coming Soon to Open Source We're working on shipping the first production apps using Relay2 We'll bring it to open source once that happens and we've polished a few rough edges I also want to note that while we're calling this Relay2, the product API is very similar to what you're used to today. We're keeping the good parts - components, renderers - and streamlining the rough edges.

Slide 76

Slide 76 text

Questions? twitter: @en_js
 github: @josephsavona Reach out on twitter or github
 
 (note: you can find Relay at https://github.com/facebook/relay)

Slide 77

Slide 77 text

Thanks twitter: @en_js
 github: @josephsavona