-
Notifications
You must be signed in to change notification settings - Fork 787
Best practices for heavy subscriptions react (native) app performance #1535
Comments
Anyone who comes across the same problem? |
Just before christmas I wrote a GraphQL server + react client for monitoring live casino tables .. it handled ~50 events/second. In the beginning I had issues with the browser becoming unresponsive and crashing after a few minutes. As you alude to, the main issue is the number of DOM updates. The JS thread can actually handle quite many socket events without problem. My recommendation is that you debounce the UI updates. import React from 'react';
import debounceRender from 'react-debounce-render';
import Table from '../../components/Table';
const Tables = ({ tables, loading, error }) => {
if (loading) {
return <div>Loading tables</div>;
}
if (error) {
return <div>{error.message}</div>;
}
if (!tables) {
return <div>no tables</div>;
}
let players = 0;
tables.forEach(table => (players = players + table.players));
return (
<div>
<p className="App-intro">
{players} players online, with {tables.filter(table => table.open).length} tables
</p>
<ul className="table-grid">
{tables &&
tables
.filter(table => table.open)
.sort((a, b) => {
if (a.name < b.name) return -1;
return 1;
})
.sort((a, b) => a.players - b.players)
.reverse()
.map(table => (
<li key={table.tableId}>
<Table table={table} />
</li>
))}
</ul>
</div>
);
};
// .filter(table => table.gameType === 'Roulette')
export default debounceRender(Tables, 100, { leading: false, maxWait: 500, trailing: true }); |
This won't help however in cases with the graphql subscriptions that apollo clients provides, since even if you dont render anything, it still has the same impact. This is duo the fact that the subscriptions are somehow stored/diffed against the current state of the apollo store. On mobile this causes a long ui freezes. However I managed to solve this issue by creating own redux store and use the link interface (so no apollo client in between) to subscribe to my subscriptions. I also use a debounce lodash function to batch the incoming subscriptions to a larger array which is dispatched all at once. |
I think that part of this is related to a complete lack of query invalidation at the moment in |
I have read your issue but in my case the problem is not because the views get re-rendered on new subscription. In my case, the store gets really slow receiving and processing new data when it gets really big and since all the subs are stored in the store (why?) the store can explode in matter of seconds (eg chat app). At the end I just ended up using a normalized redux store with just using the ws link interface to subscribe and process the data. |
@Rusfighter i'm having the same problem. i think it has to do with caching. i tried both can you expand more about your solution using redux & link? |
The redux part is very different per application, just remember to use normalized structure which is best suited for performance |
Not only for native app. Even if subscription doesn't render anything, sometimes, the interface freezes because a lot of data is pushed at the same time. |
I think the only solution for now is using own data store and use wsLink interface to subscribe to graphql subscriptions. Perhaps use some debounce/throttle functions to avoid performance drops |
Sorry to drag this up. But @Rusfighter have you had any difficulties in keeping the subscription connection alive ? KeepAlive config option seems to ping but fails to hault the disconnect. I’m using Apollo subscriptions and the client disconnects when idle after 6 or so minutes. It automatically reconnects when the app comes to the foreground. Client also disconnects immediately once app goes to the background |
@farzd I didn't used apollo subscriptions for a long time so I cannot help you but i remember there were some connection problems |
This is a very real performance issue for any type of high frequency realtime applications. A decent solution for this is to throttle and batch request to updateCurrentData query components, grouped by context. import React from 'react';
import { Query } from 'react-apollo';
let contextMap = new Map();
let throttleTimeout
/**
* Throttles all updates for a unique context, drops intermediate requests for the same context
* Batches updates for all unique contexts together
*/
const contextThrottle = function (context, callback) {
if (!throttleTimeout) {
throttleTimeout = setTimeout(() => {
requestAnimationFrame(() => {
for (let [context, callback] of contextMap) {
callback.call(context);
}
throttleTimeout = undefined;
contextMap.clear();
})
}, 200)
}
contextMap.set(context, callback);
}
export default class ThrottledQuery extends Query {
constructor(props, context) {
super(props, context)
let _updateCurrentData = this.updateCurrentData;
this.updateCurrentData = () => {
contextThrottle(this, _updateCurrentData)
}
}
} It is possible there is a race condition if the component is unmounted before the throttle executes It does not solve micro updating the store either |
This "crashes" an Electron app as well. Right now we have a Query that uses Is there no built in throttling / debounce? Before we implemented subscription we used |
Solved it by debouncing |
I did something similar to @Rusfighter's buffer solution and it works fine. But I think Apollo should provide a native way of throttling / debouncing subscription handlers. Because with the buffer solution, I don't see how we can debounce updateQuery as it's the return value that is important. How did you debounce |
Also having these problems with React Native + Apollo GraphQL Subscriptions. I am also building a chat application.
I will give @Rusfighter solution of separate Redux store a try. |
@booboothefool so apparently the phones disconnect your subscription connection when idle/locked to conserve battery. Theres no way around this. I have resorted to using polling. You could potentially reconnect when app comes to foreground. Another alternative would be background tasks - this is available in expo, however its quite hacky and i had to resolve to background location and using that to fire my task [used this for heartbeat type prototcol / not subscriptions] |
@farzd Thank you for the insight. Yes, after some research phones disconnecting connections to save power seems to be the expected behavior (makes sense I guess). I am not a fan of polling due to the extra resource usage and sometimes less predictable behavior, so I was able to get a solution working for my use case by using the following: This will restore the websocket connection after idle/lock/inactivity for new messages to come in:
Then this will refresh the queries that were missed during inactivity:
Now the websocket connection seems reliable, so this solves issue 2) for me. 👍 But I am still having the issue 1) when a few new messages are received from a subscription that the app lags out and freezes, so I will continue to try out the separate store method. |
I was just able to solve my issue 1) with the app grinding to a halt. Turns out in my case, I did not need a separate store. I was doing (as seen in the old official documentation but they have since updated it with the proper way using wrapper and componentDidMount...):
I changed to:
Lag and freezing = gone. 👍 |
For what it's worth: Ideally the rate throttling & filtering would happen at the server level. For now, I just used lodash/debounce on the updateQuery to apply rate throttling on the client to prevent even the CPU on the tab from just going crazy. |
We're moving towards only using GH issues in this repo for bugs. There doesn't really seem to be a bug here, so if anyone is interested in opening a feature request for anything in this issue thread, you can do that over in https://github.com/apollographql/apollo-feature-requests. Thanks! |
I think this is still an issue and would consider this a bug as it has serious performance implications. I receiving just a couple of messages e.g. 6 in 2-3 seconds each with a high payload (95613 length) and it causes the UI and whole browser to lag and the CPU to go crazy. This should not happen, I tried both documented ways from Apollo, but neither worked. I think this should be addressed. If it is considered a feature, then I would consider a flag which allows to disable normalization or any cache interactions. |
Can someone clarify how many subscriptions you are talking about, we have a working solution that can handle 100 subscriptions per second as the display will refresh every N seconds. |
Intended outcome:
Non laggy UI with many subscriptions aka apollo cache writes, lets say several incoming subscriptions (5+) per second. Quick summary of my proposal: create graphql HOC that only fetches data but not subscribes to it (like react-graphql) and then have another hoc which is similar to react-redux connect HOC, which subscribes only to the store thus giving more control to connect deeper components to data.
Actual outcome:
On react native, the performance is terrible. And because everything runs on one thread (UI + logic), the app stucks and becomes unusable. One of the problems with apollo client (and may be any deep nested data structure) is that a lot of re-renders need to happen when there is a data structure that higher than one level, so if there is a graphql hoc which has several levels, for example in my chat application:
Now assume there is a subscription per channel (which is fine i guess in a chat application), The component that executes the
channelTypeQuery
has a list of channels with each channel item in the list the most recent channelEvent.Assume some people are typing in several channels which triggers many subscriptions, thus the
channelTypeQuery
query will be updated and the graphql hoc will trigger re-render on the whole tree. Now you can do some shouldUpdateComponent checks or use pure components but still on a react native app this will cause large performance spikes. My idea: create graphql HOC that only fetches data but not subscribes (aka prefetching and filling the store) and then have another hoc which is similar to react-redux connect HOC, which is listening only to the apollo store thus giving more control to connect deeper components to data thus avoid many unnecessary re-rerenders and checks. And if data is not available just returns an error or null. The idea is similar to readFragment but I cannot find out how to have a sort of 'watchableFragment'.I don't know if this is currently possible or someone has any good examples/use cases for this scenario.
I hope someone can make sense of this and give some feedback.
Ilija
Version
The text was updated successfully, but these errors were encountered: