-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Performance downgrade after update to v.6.0.0 #1164
Comments
We have a benchmark suite we have run against any major release to ensure we don't have severe performance regressions. 6.0 is slightly slower than 5.1. If you're running into performance issues with 6.0, it's likely you were close to the edge of acceptable performance before. For instance, you absolutely should not be hooking up an input to the store. That is too high bandwidth and isn't state that affects the rest of the app. If you're doing typeahead-style search, you should instead batch dispatches with a debouncer. That's good for everyone, not just React Redux. But in your example, that Test component you added isn't rendering when the store changes. We use much of the same methods for avoiding renders as before. There is an attempt to render, but it is blocked when there is no change detected. |
@timdorr firstly thx for answering this. Component isn't rendering, like you said, but react has a lot to be done (to build virtual dom and compare) and it takes huge part of lag time as Chrome Profiler says. And batch dispatches, you mentioned, seems like a crutch, not a fix. Anyway we have only one fix for this - downgrade (as we have no problems with previous versions, even if we multiply *5 number of connects on the page). |
@timdorr we got a total performance screw on v6 upgrade.
That is exactly what redux-form does. They do connect every field to the store, and that causes mass updates for all connected components with new react-redux. I afraid that the tests are synthetic and does not show the real impact of the change. Most of them have just few connect calls. For our prod app v6 just killed the performance. There are no re-renders, but there are mass updates on connected components and that somehow slows everything down. It seems that each extra connect slows down the entire page, so list and forms page are just slow. Extra connects were free in v5. Do you have any ideas on how to track what exactly is slow, as there are no re-renders, just connect-level updates? |
@PashaPash , @m1lk1way : there's not much we can do unless you can provide projects that specifically demonstrate the performance issues. If you can provide repos that I can just clone and run, I might be able to look into things. |
I'm going to go ahead and reopen this, since overall perf is something I'd like to look into further. |
One idea I just had is to memoize the children of In practice, I'm not sure how much of a difference this would make, because it's likely there's connected components high up in the tree acting as "blockers" for the cascade of re-rendering from |
Just did a bit of experimenting, and I'm not sure the "memoize provider children" idea will actually do anything: https://codesandbox.io/s/jln1wjor25 What I'm seeing is that when the parent calls |
Yeah, that will work as long as there is nothing else in between |
Here's the current code: react-redux/src/components/Provider.js Lines 63 to 71 in 6e0a106
|
@markerikson thanks for reopening. not sure about prod code access, will build a sample based on it. |
@markerikson @Jessidhia here is a code sample https://github.com/PashaPash/connect-sample Here what on the page:
Measured timings on dev machine, i7-7700k, On 5.1.1: Only text box gets updated. Timeline: On 6.0.0: All connected rows gets updated (but not re-rendered) There is 6x slow down for the interactive update part. 10ms difference may not be an issue, please note that I'm measuring on dev pc, and components tree is quite simple. Setting up 4x slowdown in chrome performance tools results in 80-100ms total event time (and 60-80 ms of Tree Reconciliation caused by connected components update). We have a bit more components connected in the live app. So we are getting ~80-100ms lags on everything, including server-side pushes and API responses. The root cause seems to be state passed via store provider in v.6 that leads to full tree traversal on store change in React 16 Any ideas how to fix that? Right now the only workaround is to stay on 5.1.1. Another drawback of v6 behavior is mass unrelated updates highlight in React dev tools, as it breaks the common way of finding unwanted components updates, but that is not as critical as performance degradation for me. |
@PashaPash : Thanks for putting that together. I'm trying to juggle several different priorities atm, so I may not get to this one right away, but it's on my list. If anyone else would like to help out by doing some analysis on this, I'd appreciate it. |
Hey :) We are seeing similar results :( when updating an isolated redux value all components are updated causing large slowdowns. Isn't this what We will stay on 5.1.1 until this is resolved |
Not necessarily. That only gives us a 32 bit mask to work with, which wouldn't be enough for sufficiently large apps. We could use a bloom filter, but then you're getting near complexity levels that start to smell wrong. Since the whole point of using state as the context value was to avoid tearing, did we ever come up with an example of tearing under 5.0 that was fixed in 6.0? I'd like to think through that problem in more detail and use a real example, rather than theory. Maybe there is some middle ground solution or something we can offer as an option? (e.g., |
@timdorr : a runnable example? no. conceptually, sort of. The React team have talked about the concept several times, both in person and in various discussions online. Concurrent Mode and Suspense can both lead to situations where tearing may occur. I'll admit I'm still a bit hazy on some of the exact details. That's part of why I've kept pestering the React team for specific examples we can poke at. I've kind of gotten pushed off with "well, go look at the conference demos we've done, you can't do that right with Redux". Then again, based on some of Sebastian's comments, it seems like they're needing to do some rethinking on how external state management interacts with React as a whole overall. |
Yeah, I think we need to see about making async safety an opt-in thing. My assumption is in async mode these perf issues stop being perceptible, whereas they block in sync mode and cause visible slowdowns. So, it's not that we made things faster or slower, we just moved work into the rendering path in a blocking manner. |
I think it's fair to say we made things slower in v6. Specifically, in v5, connected components re-ran In v6, every store update calls Part of the issue here is that the state propagation model we settle on is pretty much an "all-in" approach. Either we propagate the state through context, which means having |
Bundle size actually is something that can be traded for performance. |
Is this a hard rule? For the Rust Playground, 90% of the app is an input field connected directly to the store. One of the things that I've enjoyed about Redux (and React Redux by extension) is that this was a very natural way of structuring the code. I'll be sad to learn that I need to reorganize my code to be able to upgrade without inflicting performance issues on my users. |
@shepmaster : it's not a hard rule, but it's something you should take into consideration. Redux is a global state value with an event emitter. By definition, that means that if an action is dispatched that affects one tiny area of the state, all the other components in the app get notified as well, and need to check if the data they're interested in has changed. There's a lot of back and forth discussion about what's appropriate to put into Redux. The Redux FAQ has an entry with some useful rules of thumb on what should go into the store. Specifically for forms, the main questions are:
Even in v5, our general advice was "most forms probably aren't worth putting in the store". I specifically added a Redux FAQ entry discussing whether to keep form state in the store as well. My own take is that form state that's kept in Redux should be buffered at the local component level, with a debounced action dispatching to update the store. That way, if the user types 10 keystrokes, only 1 action gets dispatched. I showed an example of this in my post Practical Redux, Part 7: Form Change Handling. There's also some related FAQ info on cutting down store notification events. |
@markerikson @timdorr having a single way to propagate changes down the tree sound like a right decision for me. The problem is that current React context API is SLOW, and they are not urging to fix it. It is not possible to get a measurable slowdown on pure React just because it is not possible to build a large app on pure React . Most people will encounter performance issues with Is there any chance that facebook/react#13739 will be fixed in nearest feature? If no, that will be a blocker for migration for most of v5 users. |
The semi-official stance (from Seb) is that the context API is just the wrong primitive for a value that changes very often. There's nothing better for this specific use case than passing subscriptions through yet (the r-r@5 model). It also seems to be his opinion that a single centralized store that's updated with everything is not itself a good model. I think the "right" solution for this problem just hasn't been found yet. The traditional model (even if implemented with new context) will still be the most performant but will cause tearing and likely behave wrong in Maybe there is a middle ground here -- expose a The problem is that this essentially codifies cascading updates into the API. My hunch is that this would be even worse for performance than the v6 model, unless the potential The list of subscribers doesn't have to be in It is possible to cancel deferred updates (so we can just continuously queue/unqueue a setState until the store stops changing faster than react can consume callbacks), but that depends on whether we do deferred updates or sync updates. The ability to cancel it prevents the user from being able to do ((I did not check if |
Putting the above in annotated code probably makes it easier to understand: const ctx = createContext()
const Provider = memo(function Provider({ store, children }) {
// const subscriptions as a mutable Set
const [subscriptions] = useState(() => new Set())
// this goes in the context and, as subscriptions can't be reassigned, is immutable
const subscribe = useCallback(
cb => {
subscriptions.add(cb)
// do we call the cb() synchronously to replicate redux's own subscribe API?
// do we schedule an independent update?
// right now do nothing
return () => subscriptions.delete(cb)
},
[subscriptions]
)
// perhaps unnecessary but explicitly empties subscription list on unmount
useEffect(
() => () => {
subscriptions.clear()
},
[]
)
// where we copy the store's state
const [state, setState] = useState(() => store.getState())
// used to make getState immutable; getState goes in the context
const stateRef = useRef(state)
stateRef.current = state
const getState = useCallback(() => stateRef.current, [stateRef])
// the currently scheduled callback
const currentUpdate = useRef(undefined)
useEffect(() => {
const unsub = store.subscribe(() => {
const state = store.getState()
if (state !== stateRef.current) {
// this effectively debounces updates until the store stops
// updating faster than React
if (currentUpdate.current !== undefined) {
cancelCallback(currentUpdate.current)
}
// this inherits the priority level of the store.dispatch()
// call that triggered the store update (Normal by default)
currentUpdate.current = scheduleCallback(() => {
currentUpdate.current = undefined
setState(state)
})
}
})
return () => {
if (currentUpdate.current !== undefined) {
cancelCallback(currentUpdate.current)
}
unsub()
}
// everything here is either the 'store' prop or is immutable
}, [store, stateRef, subscriptions, currentUpdate])
// notifies subscriptions if the local state changes
useEffect(() => {
// maybe unneeded bailout?
if (subscriptions.size === 0) {
return
}
batchedUpdates(() => {
for (const cb of subscriptions) {
// the callbacks presumably come from a consumer that has
// access to the provider; however, we could pass
// stateRef.current as an argument here instead of requiring
// them to access getState() from the context.
cb()
}
})
}, [state, subscriptions])
// context should be immutable unless the 'store' prop is replaced
const [context, replaceContext] = useState({
subscribe,
getState,
store
})
// handle the 'store' prop update as a synchronous cascading update
// (like getDerivedStateFromProps)
if (context.store !== store) {
// this is the only dangerous update in this component AFAICT,
// and should only happen if the store instance is itself replaced,
// but never on first mount
stateRef.current = store.getState()
replaceContext({
subscribe,
getState,
store
})
}
// everything here immutable unless props are changed!
return useMemo(
() => <ctx.Provider value={context}>{children}</ctx.Provider>,
[context, children]
)
}) The updates to the store will always be asynchronous (unless
|
We had a lengthy chat with @markerikson yesterday. Our current recommendation is to revert to subscriptions, but the v4 kind — not v5. To enforce top down flow, use batchedUpdates. To fix tearing we could add some band-aid, like Relay does. IIRC it reads a “version” and if it detects a different store version between renders, it forces a sync update. In longer term we’ll want something different but I think this will check most boxes now and will be fast. |
If there is no negotiation at the connect / useRedux / etc level to ensure subscriptions are well-ordered, this would require a store enhancer to wrap all dispatches in |
We need to be clear that there are several “levels” of compatibility with new features of React. They’re not formalized anywhere yet but a rough sketch for a library or technique X could be:
This is not a strict progression and there’s a spectrum of tradeoffs. (For example, maybe there is some temporary visual inconsistencies such as different like counts, but no crashes are guaranteed.) But we need to be more precise about where React Redux is, and where it aims to be. |
I think that’s probably the best way. And maybe opt in reexport of batched updates for more manual control. |
Yeah, it's gonna take me some time to go back through everything we discussed, absorb it all, and come up with a roadmap. I know we discussed the |
I have issues with this in general because I'm guessing that many people doing this are doing it in an incomplete fashion. By "many people", I mean that my team has implemented it poorly, and, luckily for me, your example also experiences the same problem. I'm happy to discuss concrete solutions to it on that issue, but I believe that it's an example of how what we think is "local" state really isn't. My own mantra has been to place everything into Redux and to rely on tools like React and Reselect to reduce spurious updates. My usages of React's |
Just adding our experience here: we have a pretty big app that has a slow rate of update for our redux store. The problem is that when it gets updated, it does affect almost every single part of the app (we use |
@quentez : if you can provide an example repo that demonstrates those particular perf issues, that would definitely help. |
This issue has been partially superseded by the roadmap I just laid out in #1177 . Key point: we're going to switch I'll leave this open for now, but basically this will get resolved when we actually figure out what the right internal re-mplementation of subscription logic is. |
Thanks @markerikson. For the sake of completeness, I entered a (now closed) issue on |
Do you want to request a feature or report a bug?
Bug
What is the current behavior?
In ver.6.0.0 current state passing into context provider, it leads to update on every consumer (every connected component (on context.consumers)). Wrapped component isn't updates but React has work to be done and its influence on performance is greatly negative.
Our case when there is connected input to store and there are a lot of connected components on page and user starts typing fast (like long keypress), in this case we see lags (like input freezes and throws updates by portions).
What is the expected behavior?
As it was in v5.1.1.
Which versions of React, ReactDOM/React Native, Redux, and React Redux are you using? Which browser and OS are affected by this issue? Did this work in previous versions of React Redux?
As an example, you can check this fiddle
https://x3yqx3ov1q.codesandbox.io/ - (upper component with greetings mustn't be updated when new ToDoItem added)
Source code of fiddle
https://codesandbox.io/s/x3yqx3ov1q
UPD it can be related to this issue in react repo facebook/react#13739
If so, what is the workaround (only one we came up with - downgrade of react-redux to v5.1.1)?
The text was updated successfully, but these errors were encountered: