question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Investigate use of context + observedBits for performance optimization

See original GitHub issue

I’ve written about this in several places recently, so I might as well consolidate the information as an issue. I’ll copy and paste from the other stuff I’ve written as appropriate.

Prior discussions and materials for reference:

Summary and Use Case

In React-Redux v5 (the current version), every instance of a connected component is a separate subscriber to the store. If you have a connected list with 10K connected list items, that’s 10,001 separate subscriber callbacks.

Every time an action is dispatched, every subscriber callback is run. Each connected component checks to see if the root state has changed. If it has, that component re-runs the mapState function it was given, and checks to see if the values it returned have changed. If any of them have, then it re-renders your real component.

In v6, we’re changing it so that there’s only one subscriber to the Redux store: the <Provider>. When an action is dispatched, it runs this.setState({storeState : store.getState()}), and then the store state is passed down to connected components using the new React.createContext API. When React sees the value given to the Context.Provider has changed, it will force all of the associated Context.Consumer instances to update, and that’s when the connected components will now re-run their mapState functions.

So, in v5, there’s 10,001 subscriber callbacks and 10,001 mapState functions executed. In v6, there’s only 1 subscriber callback executed, but still 10,001 mapState functions, plus the overhead of React re-rendering the component tree to update the context consumers. Based on what we’ve seen so far, the overhead of React updating is a bit more expensive than it was to run all those subscriber callbacks, but it’s close. Also, there’s other benefits to letting React handle this part of the work (including hopefully better compatibility with the upcoming async timeslicing stuff).

However… as many people have pointed out, in most apps, for any given action and state update, most of the components don’t actually care about the changes. Let me give a different example. Let’s say that our root state looks like {a, b, c, d}. Doesn’t matter what’s inside those, but for sake of the argument let’s say that each top-level slice holds the data for 2500 items, and a separate connected component for each item.

Now, imagine we dispatch an action that updates, say, state.a[1234].value. state, state.a, and state.a[1234] will be updated to new references by our reducers, but state.b, state.c, and state.d are all the same.

That means that only 2500 out of 10K components would have any interest in the changes at all - the ones whose data is in state.a. The other 7500 could really just skip re-running their mapState functions completely, because the top-level slices their data is in haven’t changed.

So, what I imagine is a way for a connected component to say “hey, I only want to re-run my mapState function if state.b or state.c have changed”. (Technically, you could do something sorta like this now with some of the lesser-known options to connect, I think, but lemme keep explaining.) If we did some magic to turn those state slice names into a bitmask pattern, and <Provider> ran a check to calculate a bitmask pattern based on which state slices did change, then we could potentially use that to skip the update process entirely for any components that didn’t care about these changes, and things would be a lot faster as a result.

Where the proxy stuff comes in would be some real “magic” , where we could maybe “automagically” see which state fields your mapState function tries to read. It’s theoretically possible, but very very complex with lots of edge cases. If that doesn’t work, we’d maybe let you pass an option to connect that says subscribeToStateSlices : ["a", "c"] or something like that.

Potential Implementation Details

Calculating Changed Bits from State

In the RFC, I wrote this hand-waved function:

function calculateChangedBitsForPlainObject(currentValue, nextValue) {
    // magic logic to diff objects and generate a bitmask
}

Here’s how I imagine an un-hand-waved implementation would work.

Let’s say we’ve got a Redux store whose top-level state looks like {a, b, c, d}. The contents of those state slices is irrelevant - we can assume that the values are being updated immutably, as you’re supposed to do with Redux.

A standard shallow equality check loops over the keys for two objects , and for each key, checks to see if the corresponding values in the two objects have changed. If any have changed, it returns false (“these objects are not equal”). I would want to implement a similar function that returns an array of all keys whose values have changed in the newer object (such as ["a", "c"]).

From there, I would run each key name through a hash function of some sort, which hashes the key string into a single bit. I briefly experimented with using a “minimal perfect hashing” library a while back as an early proof of concept.

So, every time the Redux store updated, the React-Redux <Provider> component, as the sole subscriber to the store, would determine which keys had their values changed and generate the changedBits bitmask accordingly. It wouldn’t be a “deep” comparison of the state trees, but rather a quick and simple “top level” list of changes, at least as a default implementation.

Connected components would by default still subscribe to all updates, but we would provide a way for the end user to indicate that a connected component only cares about updates to certain state slices (hypothetical example: connect(mapState, null, null, {subscribeToStateSlices: ["a", "d"]})(MyComponent).

Calculating Component Usage of State Slices

This is the tricky part. In an ideal world, a connected component could wrap up the store state in an ES6 Proxy, call its mapState function with that wrapped state, and track which fields had been accessed. Unfortunately, there’s tons of edge cases involved here. @theKashey has been doing a lot of research on this as part of the Remixx project he’s working on. Quoting his comment:

So, let me explain what I’ve got today:

Proxyequal - as a “base layer”. Wraps state and records all the operations on it. After being used it will produce a list and a trie with all values used, and how exactly they were used. Ie then you are doing state.a.b - you need only b, but could early-deside(memoize in this case) if a still the same.

This library is all you need to make a first step. But not the second step.

The problems are

  • selectors are not constant. In the different conditions, they may access different keys - state.items[props.key] - so their work should be reestimated every run.
  • there is no “every run” with memoization. On the second run memoized selector will do nothing.
  • this situation is easy to detect, proxyequal ships shouldBePure helper, which could test, that some function is doing the same every run.
  • but memoization and memoization cascades is something good to have.

So the plan is:

  • create a function, which could wrap any other memoization library, and “bubble” “access trie” to the top, in case of memoization
  • amend connect to throw in debug mode, if mapStateToProps is not absolutely pure
  • collect all “access tries” on the top level, and do the job.
  • but it is still a bit fragile.
  • probably call all mapStateToProps in dev mode (or in tests), and throw throw errors, if something got unpredicted updated, ie was not “reading” some keys before.

Another option:

  • add another option to connect, with information about “what it will do, which parts of the state it gonna access”. In case of breach - console.log settings to be updated.

Ie be more declarative and let js help you write down these declarations.

All this stuff is hard to write, but easy to test. But this behavior doesn’t sound like something production ready.

Use of calculateChangedBits

I had originally assumed that each Context.Provider could accepts a calculateChangedBits function as a prop. However, I was wrong - you actually pass one function as an argument to createContext, like React.createContext(defaultValue, calculateChangedBits). This is a problem, because we’re likely to use a singleton-style ReactReduxContext.Provider/Consumer pair for all of React-Redux, and that would be created when the module is imported. It would be really helpful if we had the ability to pass it as a prop to each Context.Provider instead. That would let us be more dynamic with our change calculations, such as handling an arrayor an Immutable.js Map as the root state instead of a plain object.

At Dan Abramov’s suggestion, I filed React RFC #60: Accept calculateChangedBits as a prop on Context.Providers to discuss making that change. Sebastian Markbage liked the writeup itself, but was hesitant about the actual usage of the bitmask functionality on the grounds that it might change in the future, and whether it was appropriate for our use case. The discussion hasn’t advanced further since then.

Personally, I think the bitmask API is exactly what React-Redux needs, and that we’re the kind of use case the API was created for.

One potential alternative might be a “double context” approach. In this approach, we might create a unique context provider/consumer pair whenever a React-Redux <Provider> is instantiated, use the singleton ReactReduxContext instance to pass that unique pair downwards to the connected components, and put the data into that unique context instance. That way, we could pass in a calculateChangedBits function to the createContext call . It seems hacky, but that was actually something Sebastian suggested as well.

Potential Improvements

@cellog did some quick changes to our existing stockticker benchmarks, which uses an array of entries. I think it was basically bitIndex = arrayIndex % 31, or something close to that - something simple and hardcoded as a proof of concept. He then tweaked his v6 branch (#1000 ) to calculate observedBits the same way, and compared it to both v5 and the prior results for that branch. I don’t have the specific numbers on hand, but I remember he reported it was noticeably faster than the v5 implementation, whereas the prior results were a bit slower than v5. (Greg, feel free to comment with the actual numbers.)

So, conceptually it makes sense this could be an improvement, and our first test indicates it can indeed be an improvement.

Timeline

At this point, I don’t think this would be part of a 6.0 release. Instead, we’d probably make sure that 6.0 works correctly and is hopefully equivalent in performance to v5, and release it. Then we could research the bitmasking stuff and put it out in a 6.x release, similar to how React laid the foundation with a rewrite in 16.0 and then actually released createContext in 16.3.

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Reactions:5
  • Comments:22 (16 by maintainers)

github_iconTop GitHub Comments

1reaction
markeriksoncommented, Nov 15, 2018
1reaction
markeriksoncommented, Sep 12, 2018

If we’re only concerned with top-level state slices (at least as an initial approach), then 31 bits is way more than enough. I’d say the majority of Redux stores I’ve seen have <15 top-level state slices, and even if there’s more than 31, then having more than one key map to the same bit index certainly isn’t a problem.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Performance Optimization in React Context - Disenchanted
A Bad Example. Consider the following code, it may be the worst practice of React context. const Ctx = React.createContext(); const SideMenu ...
Read more >
React Context: a Hidden Power - DEV Community ‍ ‍
React Context allows its consumers to observe certain bits of a bitmask produced by the calculateChangedBits function that can be passed as the ......
Read more >
Idiomatic Redux: The History and Implementation of React ...
Here's an example with a vanilla JS "counter" app, where the "state" is ... #1018: Investigate use of context + observedBits for performance...
Read more >
TIL React Context has a secret observedBits feature ... - Reddit
A community for learning and developing web applications using React ... TIL React Context has a secret observedBits feature for performance.
Read more >
an overlooked factor for performance optimization in React
In the following example, every time Parent component gets re-rendered, the context consumer, i.e. Child , gets re-rendered too. function App() ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found