Snappier UIs with Apollo Client + GraphQL
Dhaivat Pandya
We recently reimplemented parts of the UI for Meteor Galaxy using Apollo Client and GraphQL. This gives us a great platform to experiment with performance improvements within Apollo Client and see immediate impact.
Now that the world considers Javascript a Real Language™ and web applications Real Software™, there is some great performance tooling for Javascript. Armed with the Chrome CPU profiler and the network requests tab, I set out to see how we could improve Apollo Client performance. As the first batch of work, we discovered and eliminated no-op re-renders and implemented a simple scheduling system for polling queries that relies on Apollo’s query batching to reduce server roundtrips.
Reducing React re-renders
The Galaxy UI uses React as the view layer. Here’s what it looks like:
There are many components being rendered on this page but only a few important ones. We have the app list component:
And the user activity feed:
Each of these components have their own GraphQL query attached via react-apollo, and update periodically using the pollInterval option provided by Apollo Client. It turns out that the app list gets new information pretty frequently, whereas the information within the activity feed stays relatively stable.
While looking at the Chrome profiler to investigate performance, we noticed that the user activity feed component was re-rendered on each result from a polling query even if there were no changes in the data. We hypothesized we could eliminate these useless re-renders if we could prevent query observers getting notified unless the query result had actually changed.
It isn’t immediately clear if this helps performance. React first renders components to a virtual DOM, diffs this against the real DOM and then only applies the updates to the real DOM. So, if we were to do a deep comparison of data in order to prevent a React render, we might not end up with a significant improvement in performance since we might replace the virtual DOM diffing with a different type of deep comparison. On the other hand, the DOM representation of a data object is probably significantly more complicated than the data object itself so it is reasonable to expect some performance improvement.
Fortunately, the data spoke for itself. With the Chrome CPU profiler, we measured CPU usage in the Galaxy UI before the deep data comparison:
As you can see, there are multiple React renders for our two primary components and the update for all of the items takes about 80 ms to render. With deep data comparison, we had a different story:
We eliminated the rendering of the user activity feed and also reduced rendering time for the container items to about 40 ms since not all of the container had to re-render. This is a pretty simple page with only a couple of polling queries. A more complicated component tree with more queries would see even more significant performance improvements.
Polling query alignment
The app list and the user activity feed are also involved in another performance feature we added to Apollo Client.
If you switch tabs in Galaxy UI and then switch back to the container list tab, the app list polling query will be stopped and then started again. This means that the user activity feed polling query and the app list polling query can get pretty badly out of sync. This what that looks like in Chrome Dev Tools network tab:
These queries run on the same interval (5 seconds) but still fire separately with a chunk of time in between. This means that they can’t be batched together automatically with the Apollo query batching feature. There are number of complicated things we could do to solve this problem. For example, we could ask app developers to pick polling intervals that are multiples of one another and attempt to align the intervals.
But, there’s a much simpler solution that will improve performance most of the time. Most polling queries that we have any hope of batching together probably run on the same time interval in most applications. Given this hunch, it makes sense to just fire all the queries on the same polling interval at the same time. This means that they’ll be placed in the same tick of the batcher and will be dealt with in a single server roundtrip.
Here’s what that it looks like in the network tab after we implemented polling interval alignment:
You can’t tell from this picture, but each of these requests now consists of two GraphQL queries batched together. So, with this simple change, all the queries on a page that operate on the same interval will fire in a single server roundtrip. We didn’t have to change any of the application code: Apollo Client just made it work.
Next Steps
Both of these features are now available starting with version 0.4.6 of Apollo Client. This means you just need an npm update to eliminate no-op UI re-renders and reduce roundtrips to your server.
We’re now looking at improving GraphQL server side performance with data collected from Apollo Optics and we’ll post about what we find. If this and other GraphQL-related stuff sounds cool, follow us on Medium!