-
Notifications
You must be signed in to change notification settings - Fork 558
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
RFC: Lazy Context Propagation #118
Conversation
Since you have an implementation now, I wonder if you have some data on how much this helps in real apps? One thing to consider is that the reason context propagation is acceptable fast when optimized (it's actually unnecessary slow now, thanks to code rot, and we need to optimize it again), is because both code an a lot of the data is very local and benefit a lot from caches. Locality can make brute force surprisingly fast. By doing this propagation over and over, we might negate some of those benefits. Especially when only a single context updates. |
I think you're right that we lose some caching benefits so the imagined speedup isn't very prevalent. however I think the qualitative differences in lazy propagation unlock much more powerful bailouts such as with #119 This RFC hasn't had a lot of engagement and I haven't spent a lot of time using it in real world apps. As for whether an optimized context propagation is fast enough, i agree. the salient point of this change isn't that it is intrinsically faster but that if work is scheduled to happen anyway, it has a chance to finish. the focus on perf was it should not be slower, it sometimes is faster, and it allows things like context selectors or whatnot that would be impossible (wasteful) with eager propagation |
like Lazy Eval? |
For internal experimentation only. This implements `unstable_useSelectedContext` behind a feature flag. It's based on [RFC 119](reactjs/rfcs#119) and [RFC 118](reactjs/rfcs#118) by @gnoff. Usage: ```js const selection = useSelectedContext(Context, c => select(c)); ``` The key feature is that if the selected value does not change between renders, the component will bail out of rendering its children, a la `memo`, `PureComponent`, or the `useState` bailout mechanism. (Unless some other state, props, or context was received in the same render.) However, I have not implemented the RFC's proposed optimizations to context propagation. We would like to land those eventually, but doing so will require a refactor that we don't currently have the bandwidth to complete. It will need to wait until after React 18. In the meantime though, we believe there may be value in landing this more naive implementation. It's designed to be API-compatible with the full proposal, so we have the option to make those optimizations in a non-breaking release. However, since it's still behind a flag, this currently has no impact on the stable release channel. We reserve the right to change or remove it, as we conduct internal testing. I also added an optional third argument, `isSelectionEqual`. If defined, it will override the default comparison function used to check if the selected value has changed (`Object.is`).
For internal experimentation only. This implements `unstable_useSelectedContext` behind a feature flag. It's based on [RFC 119](reactjs/rfcs#119) and [RFC 118](reactjs/rfcs#118) by @gnoff. Usage: ```js const selection = useSelectedContext(Context, c => select(c)); ``` The key feature is that if the selected value does not change between renders, the component will bail out of rendering its children, a la `memo`, `PureComponent`, or the `useState` bailout mechanism. (Unless some other state, props, or context was updated in the same render.) However, I have not implemented the RFC's proposed optimizations to context propagation. We would like to land those eventually, but doing so will require a refactor that we don't currently have the bandwidth to complete. It will need to wait until after React 18. In the meantime though, we believe there may be value in landing this more naive implementation. It's designed to be API-compatible with the full proposal, so we have the option to make those optimizations in a non-breaking release. However, since it's still behind a flag, this currently has no impact on the stable release channel. We reserve the right to change or remove it, as we conduct internal testing. I also added an optional third argument, `isSelectionEqual`. If defined, it will override the default comparison function used to check if the selected value has changed (`Object.is`).
For internal experimentation only. This implements `unstable_useSelectedContext` behind a feature flag. It's based on [RFC 119](reactjs/rfcs#119) and [RFC 118](reactjs/rfcs#118) by @gnoff. Usage: ```js const selection = useSelectedContext(Context, c => select(c)); ``` The key feature is that if the selected value does not change between renders, the component will bail out of rendering its children, a la `memo`, `PureComponent`, or the `useState` bailout mechanism. (Unless some other state, props, or context was updated in the same render.) However, I have not implemented the RFC's proposed optimizations to context propagation. We would like to land those eventually, but doing so will require a refactor that we don't currently have the bandwidth to complete. It will need to wait until after React 18. In the meantime though, we believe there may be value in landing this more naive implementation. It's designed to be API-compatible with the full proposal, so we have the option to make those optimizations in a non-breaking release. However, since it's still behind a flag, this currently has no impact on the stable release channel. We reserve the right to change or remove it, as we conduct internal testing. I also added an optional third argument, `isSelectionEqual`. If defined, it will override the default comparison function used to check if the selected value has changed (`Object.is`).
For internal experimentation only. This implements `unstable_useSelectedContext` behind a feature flag. It's based on [RFC 119](reactjs/rfcs#119) and [RFC 118](reactjs/rfcs#118) by @gnoff. Usage: ```js const selection = useSelectedContext(Context, c => select(c)); ``` The key feature is that if the selected value does not change between renders, the component will bail out of rendering its children, a la `memo`, `PureComponent`, or the `useState` bailout mechanism. (Unless some other state, props, or context was updated in the same render.) However, I have not implemented the RFC's proposed optimizations to context propagation. We would like to land those eventually, but doing so will require a refactor that we don't currently have the bandwidth to complete. It will need to wait until after React 18. In the meantime though, we believe there may be value in landing this more naive implementation. It's designed to be API-compatible with the full proposal, so we have the option to make those optimizations in a non-breaking release. However, since it's still behind a flag, this currently has no impact on the stable release channel. We reserve the right to change or remove it, as we conduct internal testing. I also added an optional third argument, `isSelectionEqual`. If defined, it will override the default comparison function used to check if the selected value has changed (`Object.is`).
When a context provider changes, we scan the tree for matching consumers and mark them as dirty so that we know they have pending work. This prevents us from bailing out if, say, an intermediate wrapper is memoized. Currently, we propagate these changes eagerly, at the provider. However, in many cases, we would have ended up visiting the consumer nodes anyway, as part of the normal render traversal, because there's no memoized node in between that bails out. We can save CPU cycles by propagating changes only when we hit a memoized component — so, instead of propagating eagerly at the provider, we propagate lazily if or when something bails out. Another neat optimization is that if multiple context providers change simultaneously, we don't need to propagate all of them; we can stop propagating as soon as one of them matches a deep consumer. This works even though the providers have consumers in different parts of the tree, because we'll pick up the propagation algorithm again during the next nested bailout. Most of our bailout logic is centralized in `bailoutOnAlreadyFinishedWork`, so this ended up being not that difficult to implement correctly. There are some exceptions: Suspense and Offscreen. Those are special because they sometimes defer the rendering of their children to a completely separate render cycle. In those cases, we must take extra care to propagate *all* the context changes, not just the first one. I'm pleasantly surprised at how little I needed to change in this initial implementation. I was worried I'd have to use the reconciler fork, but I ended up being able to wrap all my changes in a regular feature flag. So, we could run an experiment in parallel to our other ones. I do consider this a risky rollout overall because of the potential for subtle semantic deviations. However, the model is simple enough that I don't expect us to have trouble fixing regressions if or when they arise during internal dogfooding. --- This is largely based on [RFC facebook#118](reactjs/rfcs#118), by @gnoff. I did deviate in some of the implementation details, though. The main one is how I chose to track context changes. Instead of storing a dirty flag on the stack, I added a `memoizedValue` field to the context dependency object. Then, to check if something has changed, the consumer compares the new context value to the old (memoized) one. This is necessary because of Suspense and Offscreen — those components defer work from one render into a later one. When the subtree continues rendering, the stack from the previous render is no longer available. But the memoized values on the dependencies list are. (Refer to the previous commit where I implemented this as its own atomic change.) This requires a bit more work when a consumer bails out, but nothing considerable, and there are ways we could optimize it even further. Concpeutally, this model is really appealing, since it matches how our other features "reactively" detect changes — `useMemo`, `useEffect`, `getDerivedStateFromProps`, the built-in cache, and so on. I also intentionally dropped support for `unstable_calculateChangedBits`. We're planning to remove this API anyway before the next major release, in favor of context selectors. It's an unstable feature that we never advertised; I don't think it's seen much adoption. Co-Authored-By: Josh Story <[email protected]>
When a context provider changes, we scan the tree for matching consumers and mark them as dirty so that we know they have pending work. This prevents us from bailing out if, say, an intermediate wrapper is memoized. Currently, we propagate these changes eagerly, at the provider. However, in many cases, we would have ended up visiting the consumer nodes anyway, as part of the normal render traversal, because there's no memoized node in between that bails out. We can save CPU cycles by propagating changes only when we hit a memoized component — so, instead of propagating eagerly at the provider, we propagate lazily if or when something bails out. Another neat optimization is that if multiple context providers change simultaneously, we don't need to propagate all of them; we can stop propagating as soon as one of them matches a deep consumer. This works even though the providers have consumers in different parts of the tree, because we'll pick up the propagation algorithm again during the next nested bailout. Most of our bailout logic is centralized in `bailoutOnAlreadyFinishedWork`, so this ended up being not that difficult to implement correctly. There are some exceptions: Suspense and Offscreen. Those are special because they sometimes defer the rendering of their children to a completely separate render cycle. In those cases, we must take extra care to propagate *all* the context changes, not just the first one. I'm pleasantly surprised at how little I needed to change in this initial implementation. I was worried I'd have to use the reconciler fork, but I ended up being able to wrap all my changes in a regular feature flag. So, we could run an experiment in parallel to our other ones. I do consider this a risky rollout overall because of the potential for subtle semantic deviations. However, the model is simple enough that I don't expect us to have trouble fixing regressions if or when they arise during internal dogfooding. --- This is largely based on [RFC facebook#118](reactjs/rfcs#118), by @gnoff. I did deviate in some of the implementation details, though. The main one is how I chose to track context changes. Instead of storing a dirty flag on the stack, I added a `memoizedValue` field to the context dependency object. Then, to check if something has changed, the consumer compares the new context value to the old (memoized) one. This is necessary because of Suspense and Offscreen — those components defer work from one render into a later one. When the subtree continues rendering, the stack from the previous render is no longer available. But the memoized values on the dependencies list are. (Refer to the previous commit where I implemented this as its own atomic change.) This requires a bit more work when a consumer bails out, but nothing considerable, and there are ways we could optimize it even further. Concpeutally, this model is really appealing, since it matches how our other features "reactively" detect changes — `useMemo`, `useEffect`, `getDerivedStateFromProps`, the built-in cache, and so on. I also intentionally dropped support for `unstable_calculateChangedBits`. We're planning to remove this API anyway before the next major release, in favor of context selectors. It's an unstable feature that we never advertised; I don't think it's seen much adoption. Co-Authored-By: Josh Story <[email protected]>
When a context provider changes, we scan the tree for matching consumers and mark them as dirty so that we know they have pending work. This prevents us from bailing out if, say, an intermediate wrapper is memoized. Currently, we propagate these changes eagerly, at the provider. However, in many cases, we would have ended up visiting the consumer nodes anyway, as part of the normal render traversal, because there's no memoized node in between that bails out. We can save CPU cycles by propagating changes only when we hit a memoized component — so, instead of propagating eagerly at the provider, we propagate lazily if or when something bails out. Another neat optimization is that if multiple context providers change simultaneously, we don't need to propagate all of them; we can stop propagating as soon as one of them matches a deep consumer. This works even though the providers have consumers in different parts of the tree, because we'll pick up the propagation algorithm again during the next nested bailout. Most of our bailout logic is centralized in `bailoutOnAlreadyFinishedWork`, so this ended up being not that difficult to implement correctly. There are some exceptions: Suspense and Offscreen. Those are special because they sometimes defer the rendering of their children to a completely separate render cycle. In those cases, we must take extra care to propagate *all* the context changes, not just the first one. I'm pleasantly surprised at how little I needed to change in this initial implementation. I was worried I'd have to use the reconciler fork, but I ended up being able to wrap all my changes in a regular feature flag. So, we could run an experiment in parallel to our other ones. I do consider this a risky rollout overall because of the potential for subtle semantic deviations. However, the model is simple enough that I don't expect us to have trouble fixing regressions if or when they arise during internal dogfooding. --- This is largely based on [RFC facebook#118](reactjs/rfcs#118), by @gnoff. I did deviate in some of the implementation details, though. The main one is how I chose to track context changes. Instead of storing a dirty flag on the stack, I added a `memoizedValue` field to the context dependency object. Then, to check if something has changed, the consumer compares the new context value to the old (memoized) one. This is necessary because of Suspense and Offscreen — those components defer work from one render into a later one. When the subtree continues rendering, the stack from the previous render is no longer available. But the memoized values on the dependencies list are. (Refer to the previous commit where I implemented this as its own atomic change.) This requires a bit more work when a consumer bails out, but nothing considerable, and there are ways we could optimize it even further. Concpeutally, this model is really appealing, since it matches how our other features "reactively" detect changes — `useMemo`, `useEffect`, `getDerivedStateFromProps`, the built-in cache, and so on. I also intentionally dropped support for `unstable_calculateChangedBits`. We're planning to remove this API anyway before the next major release, in favor of context selectors. It's an unstable feature that we never advertised; I don't think it's seen much adoption. Co-Authored-By: Josh Story <[email protected]>
When a context provider changes, we scan the tree for matching consumers and mark them as dirty so that we know they have pending work. This prevents us from bailing out if, say, an intermediate wrapper is memoized. Currently, we propagate these changes eagerly, at the provider. However, in many cases, we would have ended up visiting the consumer nodes anyway, as part of the normal render traversal, because there's no memoized node in between that bails out. We can save CPU cycles by propagating changes only when we hit a memoized component — so, instead of propagating eagerly at the provider, we propagate lazily if or when something bails out. Another neat optimization is that if multiple context providers change simultaneously, we don't need to propagate all of them; we can stop propagating as soon as one of them matches a deep consumer. This works even though the providers have consumers in different parts of the tree, because we'll pick up the propagation algorithm again during the next nested bailout. Most of our bailout logic is centralized in `bailoutOnAlreadyFinishedWork`, so this ended up being not that difficult to implement correctly. There are some exceptions: Suspense and Offscreen. Those are special because they sometimes defer the rendering of their children to a completely separate render cycle. In those cases, we must take extra care to propagate *all* the context changes, not just the first one. I'm pleasantly surprised at how little I needed to change in this initial implementation. I was worried I'd have to use the reconciler fork, but I ended up being able to wrap all my changes in a regular feature flag. So, we could run an experiment in parallel to our other ones. I do consider this a risky rollout overall because of the potential for subtle semantic deviations. However, the model is simple enough that I don't expect us to have trouble fixing regressions if or when they arise during internal dogfooding. --- This is largely based on [RFC facebook#118](reactjs/rfcs#118), by @gnoff. I did deviate in some of the implementation details, though. The main one is how I chose to track context changes. Instead of storing a dirty flag on the stack, I added a `memoizedValue` field to the context dependency object. Then, to check if something has changed, the consumer compares the new context value to the old (memoized) one. This is necessary because of Suspense and Offscreen — those components defer work from one render into a later one. When the subtree continues rendering, the stack from the previous render is no longer available. But the memoized values on the dependencies list are. (Refer to the previous commit where I implemented this as its own atomic change.) This requires a bit more work when a consumer bails out, but nothing considerable, and there are ways we could optimize it even further. Concpeutally, this model is really appealing, since it matches how our other features "reactively" detect changes — `useMemo`, `useEffect`, `getDerivedStateFromProps`, the built-in cache, and so on. I also intentionally dropped support for `unstable_calculateChangedBits`. We're planning to remove this API anyway before the next major release, in favor of context selectors. It's an unstable feature that we never advertised; I don't think it's seen much adoption. Co-Authored-By: Josh Story <[email protected]>
When a context provider changes, we scan the tree for matching consumers and mark them as dirty so that we know they have pending work. This prevents us from bailing out if, say, an intermediate wrapper is memoized. Currently, we propagate these changes eagerly, at the provider. However, in many cases, we would have ended up visiting the consumer nodes anyway, as part of the normal render traversal, because there's no memoized node in between that bails out. We can save CPU cycles by propagating changes only when we hit a memoized component — so, instead of propagating eagerly at the provider, we propagate lazily if or when something bails out. Another neat optimization is that if multiple context providers change simultaneously, we don't need to propagate all of them; we can stop propagating as soon as one of them matches a deep consumer. This works even though the providers have consumers in different parts of the tree, because we'll pick up the propagation algorithm again during the next nested bailout. Most of our bailout logic is centralized in `bailoutOnAlreadyFinishedWork`, so this ended up being not that difficult to implement correctly. There are some exceptions: Suspense and Offscreen. Those are special because they sometimes defer the rendering of their children to a completely separate render cycle. In those cases, we must take extra care to propagate *all* the context changes, not just the first one. I'm pleasantly surprised at how little I needed to change in this initial implementation. I was worried I'd have to use the reconciler fork, but I ended up being able to wrap all my changes in a regular feature flag. So, we could run an experiment in parallel to our other ones. I do consider this a risky rollout overall because of the potential for subtle semantic deviations. However, the model is simple enough that I don't expect us to have trouble fixing regressions if or when they arise during internal dogfooding. --- This is largely based on [RFC facebook#118](reactjs/rfcs#118), by @gnoff. I did deviate in some of the implementation details, though. The main one is how I chose to track context changes. Instead of storing a dirty flag on the stack, I added a `memoizedValue` field to the context dependency object. Then, to check if something has changed, the consumer compares the new context value to the old (memoized) one. This is necessary because of Suspense and Offscreen — those components defer work from one render into a later one. When the subtree continues rendering, the stack from the previous render is no longer available. But the memoized values on the dependencies list are. (Refer to the previous commit where I implemented this as its own atomic change.) This requires a bit more work when a consumer bails out, but nothing considerable, and there are ways we could optimize it even further. Concpeutally, this model is really appealing, since it matches how our other features "reactively" detect changes — `useMemo`, `useEffect`, `getDerivedStateFromProps`, the built-in cache, and so on. I also intentionally dropped support for `unstable_calculateChangedBits`. We're planning to remove this API anyway before the next major release, in favor of context selectors. It's an unstable feature that we never advertised; I don't think it's seen much adoption. Co-Authored-By: Josh Story <[email protected]>
When a context provider changes, we scan the tree for matching consumers and mark them as dirty so that we know they have pending work. This prevents us from bailing out if, say, an intermediate wrapper is memoized. Currently, we propagate these changes eagerly, at the provider. However, in many cases, we would have ended up visiting the consumer nodes anyway, as part of the normal render traversal, because there's no memoized node in between that bails out. We can save CPU cycles by propagating changes only when we hit a memoized component — so, instead of propagating eagerly at the provider, we propagate lazily if or when something bails out. Another neat optimization is that if multiple context providers change simultaneously, we don't need to propagate all of them; we can stop propagating as soon as one of them matches a deep consumer. This works even though the providers have consumers in different parts of the tree, because we'll pick up the propagation algorithm again during the next nested bailout. Most of our bailout logic is centralized in `bailoutOnAlreadyFinishedWork`, so this ended up being not that difficult to implement correctly. There are some exceptions: Suspense and Offscreen. Those are special because they sometimes defer the rendering of their children to a completely separate render cycle. In those cases, we must take extra care to propagate *all* the context changes, not just the first one. I'm pleasantly surprised at how little I needed to change in this initial implementation. I was worried I'd have to use the reconciler fork, but I ended up being able to wrap all my changes in a regular feature flag. So, we could run an experiment in parallel to our other ones. I do consider this a risky rollout overall because of the potential for subtle semantic deviations. However, the model is simple enough that I don't expect us to have trouble fixing regressions if or when they arise during internal dogfooding. --- This is largely based on [RFC facebook#118](reactjs/rfcs#118), by @gnoff. I did deviate in some of the implementation details, though. The main one is how I chose to track context changes. Instead of storing a dirty flag on the stack, I added a `memoizedValue` field to the context dependency object. Then, to check if something has changed, the consumer compares the new context value to the old (memoized) one. This is necessary because of Suspense and Offscreen — those components defer work from one render into a later one. When the subtree continues rendering, the stack from the previous render is no longer available. But the memoized values on the dependencies list are. (Refer to the previous commit where I implemented this as its own atomic change.) This requires a bit more work when a consumer bails out, but nothing considerable, and there are ways we could optimize it even further. Concpeutally, this model is really appealing, since it matches how our other features "reactively" detect changes — `useMemo`, `useEffect`, `getDerivedStateFromProps`, the built-in cache, and so on. I also intentionally dropped support for `unstable_calculateChangedBits`. We're planning to remove this API anyway before the next major release, in favor of context selectors. It's an unstable feature that we never advertised; I don't think it's seen much adoption. Co-Authored-By: Josh Story <[email protected]>
When a context provider changes, we scan the tree for matching consumers and mark them as dirty so that we know they have pending work. This prevents us from bailing out if, say, an intermediate wrapper is memoized. Currently, we propagate these changes eagerly, at the provider. However, in many cases, we would have ended up visiting the consumer nodes anyway, as part of the normal render traversal, because there's no memoized node in between that bails out. We can save CPU cycles by propagating changes only when we hit a memoized component — so, instead of propagating eagerly at the provider, we propagate lazily if or when something bails out. Another neat optimization is that if multiple context providers change simultaneously, we don't need to propagate all of them; we can stop propagating as soon as one of them matches a deep consumer. This works even though the providers have consumers in different parts of the tree, because we'll pick up the propagation algorithm again during the next nested bailout. Most of our bailout logic is centralized in `bailoutOnAlreadyFinishedWork`, so this ended up being not that difficult to implement correctly. There are some exceptions: Suspense and Offscreen. Those are special because they sometimes defer the rendering of their children to a completely separate render cycle. In those cases, we must take extra care to propagate *all* the context changes, not just the first one. I'm pleasantly surprised at how little I needed to change in this initial implementation. I was worried I'd have to use the reconciler fork, but I ended up being able to wrap all my changes in a regular feature flag. So, we could run an experiment in parallel to our other ones. I do consider this a risky rollout overall because of the potential for subtle semantic deviations. However, the model is simple enough that I don't expect us to have trouble fixing regressions if or when they arise during internal dogfooding. --- This is largely based on [RFC facebook#118](reactjs/rfcs#118), by @gnoff. I did deviate in some of the implementation details, though. The main one is how I chose to track context changes. Instead of storing a dirty flag on the stack, I added a `memoizedValue` field to the context dependency object. Then, to check if something has changed, the consumer compares the new context value to the old (memoized) one. This is necessary because of Suspense and Offscreen — those components defer work from one render into a later one. When the subtree continues rendering, the stack from the previous render is no longer available. But the memoized values on the dependencies list are. (Refer to the previous commit where I implemented this as its own atomic change.) This requires a bit more work when a consumer bails out, but nothing considerable, and there are ways we could optimize it even further. Concpeutally, this model is really appealing, since it matches how our other features "reactively" detect changes — `useMemo`, `useEffect`, `getDerivedStateFromProps`, the built-in cache, and so on. I also intentionally dropped support for `unstable_calculateChangedBits`. We're planning to remove this API anyway before the next major release, in favor of context selectors. It's an unstable feature that we never advertised; I don't think it's seen much adoption. Co-Authored-By: Josh Story <[email protected]>
When a context provider changes, we scan the tree for matching consumers and mark them as dirty so that we know they have pending work. This prevents us from bailing out if, say, an intermediate wrapper is memoized. Currently, we propagate these changes eagerly, at the provider. However, in many cases, we would have ended up visiting the consumer nodes anyway, as part of the normal render traversal, because there's no memoized node in between that bails out. We can save CPU cycles by propagating changes only when we hit a memoized component — so, instead of propagating eagerly at the provider, we propagate lazily if or when something bails out. Another neat optimization is that if multiple context providers change simultaneously, we don't need to propagate all of them; we can stop propagating as soon as one of them matches a deep consumer. This works even though the providers have consumers in different parts of the tree, because we'll pick up the propagation algorithm again during the next nested bailout. Most of our bailout logic is centralized in `bailoutOnAlreadyFinishedWork`, so this ended up being not that difficult to implement correctly. There are some exceptions: Suspense and Offscreen. Those are special because they sometimes defer the rendering of their children to a completely separate render cycle. In those cases, we must take extra care to propagate *all* the context changes, not just the first one. I'm pleasantly surprised at how little I needed to change in this initial implementation. I was worried I'd have to use the reconciler fork, but I ended up being able to wrap all my changes in a regular feature flag. So, we could run an experiment in parallel to our other ones. I do consider this a risky rollout overall because of the potential for subtle semantic deviations. However, the model is simple enough that I don't expect us to have trouble fixing regressions if or when they arise during internal dogfooding. --- This is largely based on [RFC facebook#118](reactjs/rfcs#118), by @gnoff. I did deviate in some of the implementation details, though. The main one is how I chose to track context changes. Instead of storing a dirty flag on the stack, I added a `memoizedValue` field to the context dependency object. Then, to check if something has changed, the consumer compares the new context value to the old (memoized) one. This is necessary because of Suspense and Offscreen — those components defer work from one render into a later one. When the subtree continues rendering, the stack from the previous render is no longer available. But the memoized values on the dependencies list are. (Refer to the previous commit where I implemented this as its own atomic change.) This requires a bit more work when a consumer bails out, but nothing considerable, and there are ways we could optimize it even further. Concpeutally, this model is really appealing, since it matches how our other features "reactively" detect changes — `useMemo`, `useEffect`, `getDerivedStateFromProps`, the built-in cache, and so on. I also intentionally dropped support for `unstable_calculateChangedBits`. We're planning to remove this API anyway before the next major release, in favor of context selectors. It's an unstable feature that we never advertised; I don't think it's seen much adoption. Co-Authored-By: Josh Story <[email protected]>
When a context provider changes, we scan the tree for matching consumers and mark them as dirty so that we know they have pending work. This prevents us from bailing out if, say, an intermediate wrapper is memoized. Currently, we propagate these changes eagerly, at the provider. However, in many cases, we would have ended up visiting the consumer nodes anyway, as part of the normal render traversal, because there's no memoized node in between that bails out. We can save CPU cycles by propagating changes only when we hit a memoized component — so, instead of propagating eagerly at the provider, we propagate lazily if or when something bails out. Another neat optimization is that if multiple context providers change simultaneously, we don't need to propagate all of them; we can stop propagating as soon as one of them matches a deep consumer. This works even though the providers have consumers in different parts of the tree, because we'll pick up the propagation algorithm again during the next nested bailout. Most of our bailout logic is centralized in `bailoutOnAlreadyFinishedWork`, so this ended up being not that difficult to implement correctly. There are some exceptions: Suspense and Offscreen. Those are special because they sometimes defer the rendering of their children to a completely separate render cycle. In those cases, we must take extra care to propagate *all* the context changes, not just the first one. I'm pleasantly surprised at how little I needed to change in this initial implementation. I was worried I'd have to use the reconciler fork, but I ended up being able to wrap all my changes in a regular feature flag. So, we could run an experiment in parallel to our other ones. I do consider this a risky rollout overall because of the potential for subtle semantic deviations. However, the model is simple enough that I don't expect us to have trouble fixing regressions if or when they arise during internal dogfooding. --- This is largely based on [RFC facebook#118](reactjs/rfcs#118), by @gnoff. I did deviate in some of the implementation details, though. The main one is how I chose to track context changes. Instead of storing a dirty flag on the stack, I added a `memoizedValue` field to the context dependency object. Then, to check if something has changed, the consumer compares the new context value to the old (memoized) one. This is necessary because of Suspense and Offscreen — those components defer work from one render into a later one. When the subtree continues rendering, the stack from the previous render is no longer available. But the memoized values on the dependencies list are. (Refer to the previous commit where I implemented this as its own atomic change.) This requires a bit more work when a consumer bails out, but nothing considerable, and there are ways we could optimize it even further. Concpeutally, this model is really appealing, since it matches how our other features "reactively" detect changes — `useMemo`, `useEffect`, `getDerivedStateFromProps`, the built-in cache, and so on. I also intentionally dropped support for `unstable_calculateChangedBits`. We're planning to remove this API anyway before the next major release, in favor of context selectors. It's an unstable feature that we never advertised; I don't think it's seen much adoption. Co-Authored-By: Josh Story <[email protected]>
When a context provider changes, we scan the tree for matching consumers and mark them as dirty so that we know they have pending work. This prevents us from bailing out if, say, an intermediate wrapper is memoized. Currently, we propagate these changes eagerly, at the provider. However, in many cases, we would have ended up visiting the consumer nodes anyway, as part of the normal render traversal, because there's no memoized node in between that bails out. We can save CPU cycles by propagating changes only when we hit a memoized component — so, instead of propagating eagerly at the provider, we propagate lazily if or when something bails out. Another neat optimization is that if multiple context providers change simultaneously, we don't need to propagate all of them; we can stop propagating as soon as one of them matches a deep consumer. This works even though the providers have consumers in different parts of the tree, because we'll pick up the propagation algorithm again during the next nested bailout. Most of our bailout logic is centralized in `bailoutOnAlreadyFinishedWork`, so this ended up being not that difficult to implement correctly. There are some exceptions: Suspense and Offscreen. Those are special because they sometimes defer the rendering of their children to a completely separate render cycle. In those cases, we must take extra care to propagate *all* the context changes, not just the first one. I'm pleasantly surprised at how little I needed to change in this initial implementation. I was worried I'd have to use the reconciler fork, but I ended up being able to wrap all my changes in a regular feature flag. So, we could run an experiment in parallel to our other ones. I do consider this a risky rollout overall because of the potential for subtle semantic deviations. However, the model is simple enough that I don't expect us to have trouble fixing regressions if or when they arise during internal dogfooding. --- This is largely based on [RFC facebook#118](reactjs/rfcs#118), by @gnoff. I did deviate in some of the implementation details, though. The main one is how I chose to track context changes. Instead of storing a dirty flag on the stack, I added a `memoizedValue` field to the context dependency object. Then, to check if something has changed, the consumer compares the new context value to the old (memoized) one. This is necessary because of Suspense and Offscreen — those components defer work from one render into a later one. When the subtree continues rendering, the stack from the previous render is no longer available. But the memoized values on the dependencies list are. (Refer to the previous commit where I implemented this as its own atomic change.) This requires a bit more work when a consumer bails out, but nothing considerable, and there are ways we could optimize it even further. Concpeutally, this model is really appealing, since it matches how our other features "reactively" detect changes — `useMemo`, `useEffect`, `getDerivedStateFromProps`, the built-in cache, and so on. I also intentionally dropped support for `unstable_calculateChangedBits`. We're planning to remove this API anyway before the next major release, in favor of context selectors. It's an unstable feature that we never advertised; I don't think it's seen much adoption. Co-Authored-By: Josh Story <[email protected]>
When a context provider changes, we scan the tree for matching consumers and mark them as dirty so that we know they have pending work. This prevents us from bailing out if, say, an intermediate wrapper is memoized. Currently, we propagate these changes eagerly, at the provider. However, in many cases, we would have ended up visiting the consumer nodes anyway, as part of the normal render traversal, because there's no memoized node in between that bails out. We can save CPU cycles by propagating changes only when we hit a memoized component — so, instead of propagating eagerly at the provider, we propagate lazily if or when something bails out. Another neat optimization is that if multiple context providers change simultaneously, we don't need to propagate all of them; we can stop propagating as soon as one of them matches a deep consumer. This works even though the providers have consumers in different parts of the tree, because we'll pick up the propagation algorithm again during the next nested bailout. Most of our bailout logic is centralized in `bailoutOnAlreadyFinishedWork`, so this ended up being not that difficult to implement correctly. There are some exceptions: Suspense and Offscreen. Those are special because they sometimes defer the rendering of their children to a completely separate render cycle. In those cases, we must take extra care to propagate *all* the context changes, not just the first one. I'm pleasantly surprised at how little I needed to change in this initial implementation. I was worried I'd have to use the reconciler fork, but I ended up being able to wrap all my changes in a regular feature flag. So, we could run an experiment in parallel to our other ones. I do consider this a risky rollout overall because of the potential for subtle semantic deviations. However, the model is simple enough that I don't expect us to have trouble fixing regressions if or when they arise during internal dogfooding. --- This is largely based on [RFC facebook#118](reactjs/rfcs#118), by @gnoff. I did deviate in some of the implementation details, though. The main one is how I chose to track context changes. Instead of storing a dirty flag on the stack, I added a `memoizedValue` field to the context dependency object. Then, to check if something has changed, the consumer compares the new context value to the old (memoized) one. This is necessary because of Suspense and Offscreen — those components defer work from one render into a later one. When the subtree continues rendering, the stack from the previous render is no longer available. But the memoized values on the dependencies list are. (Refer to the previous commit where I implemented this as its own atomic change.) This requires a bit more work when a consumer bails out, but nothing considerable, and there are ways we could optimize it even further. Concpeutally, this model is really appealing, since it matches how our other features "reactively" detect changes — `useMemo`, `useEffect`, `getDerivedStateFromProps`, the built-in cache, and so on. I also intentionally dropped support for `unstable_calculateChangedBits`. We're planning to remove this API anyway before the next major release, in favor of context selectors. It's an unstable feature that we never advertised; I don't think it's seen much adoption. Co-Authored-By: Josh Story <[email protected]>
For internal experimentation only. This implements `unstable_useContextSelector` behind a feature flag. It's based on [RFC 119](reactjs/rfcs#119) and [RFC 118](reactjs/rfcs#118) by @gnoff. Usage: ```js const context = useSelectedContext(Context, c => select(c)); ``` The key feature is that if the selected value does not change between renders, the component will bail out of rendering its children, a la `memo`, `PureComponent`, or the `useState` bailout mechanism. (Unless some other state, props, or context was updated in the same render.) One difference from the RFC is that it does not return the selected value. It returns the full context object. This serves a few purposes: it discourages you from creating any new objects or derived values inside the selector, because it'll get thrown out regardless. Instead, all the selector will do is return a subfield. Then you can compute the derived value inside the component, and if needed, you memoize that derived value with `useMemo`. If all the selectors do is access a subfield, they're (theoretically) fast enough that we can call them during the propagation scan and bail out really early, without having to visit the component during the render phase. Another benefit is that it's API compatible with `useContext`. So we can put it behind a flag that falls back to regular `useContext`. The longer term vision is that these optimizations (in addition to other memoization checks, like `useMemo` and `useCallback`) are inserted automatically by a compiler. So you would write code like this: ```js const {a, b} = useContext(Context); const derived = computeDerived(a, b); ``` and it would get converted to something like this: ```js const {a} = useContextSelector(Context, context => context.a); const {b} = useContextSelector(Context, context => context.b); const derived = useMemo(() => computeDerived(a, b), [a, b]); ``` (Though not this exactly. Some lower level compiler output target.)
For internal experimentation only. This implements `unstable_useContextSelector` behind a feature flag. It's based on [RFC 119](reactjs/rfcs#119) and [RFC 118](reactjs/rfcs#118) by @gnoff. Usage: ```js const context = useContextSelector(Context, c => c.selectedField); ``` The key feature is that if the selected value does not change between renders, the component will bail out of rendering its children, a la `memo`, `PureComponent`, or the `useState` bailout mechanism. (Unless some other state, props, or context was updated in the same render.) One difference from the RFC is that it does not return the selected value. It returns the full context object. This serves a few purposes: it discourages you from creating any new objects or derived values inside the selector, because it'll get thrown out regardless. Instead, all the selector will do is return a subfield. Then you can compute the derived value inside the component, and if needed, you memoize that derived value with `useMemo`. If all the selectors do is access a subfield, they're (theoretically) fast enough that we can call them during the propagation scan and bail out really early, without having to visit the component during the render phase. Another benefit is that it's API compatible with `useContext`. So we can put it behind a flag that falls back to regular `useContext`. The longer term vision is that these optimizations (in addition to other memoization checks, like `useMemo` and `useCallback`) are inserted automatically by a compiler. So you would write code like this: ```js const {a, b} = useContext(Context); const derived = computeDerived(a, b); ``` and it would get converted to something like this: ```js const {a} = useContextSelector(Context, context => context.a); const {b} = useContextSelector(Context, context => context.b); const derived = useMemo(() => computeDerived(a, b), [a, b]); ``` (Though not this exactly. Some lower level compiler output target.)
When a context provider changes, we scan the tree for matching consumers and mark them as dirty so that we know they have pending work. This prevents us from bailing out if, say, an intermediate wrapper is memoized. Currently, we propagate these changes eagerly, at the provider. However, in many cases, we would have ended up visiting the consumer nodes anyway, as part of the normal render traversal, because there's no memoized node in between that bails out. We can save CPU cycles by propagating changes only when we hit a memoized component — so, instead of propagating eagerly at the provider, we propagate lazily if or when something bails out. Most of our bailout logic is centralized in `bailoutOnAlreadyFinishedWork`, so this ended up being not that difficult to implement correctly. There are some exceptions: Suspense and Offscreen. Those are special because they sometimes defer the rendering of their children to a completely separate render cycle. In those cases, we must take extra care to propagate *all* the context changes, not just the first one. I'm pleasantly surprised at how little I needed to change in this initial implementation. I was worried I'd have to use the reconciler fork, but I ended up being able to wrap all my changes in a regular feature flag. So, we could run an experiment in parallel to our other ones. I do consider this a risky rollout overall because of the potential for subtle semantic deviations. However, the model is simple enough that I don't expect us to have trouble fixing regressions if or when they arise during internal dogfooding. --- This is largely based on [RFC#118](reactjs/rfcs#118), by @gnoff. I did deviate in some of the implementation details, though. The main one is how I chose to track context changes. Instead of storing a dirty flag on the stack, I added a `memoizedValue` field to the context dependency object. Then, to check if something has changed, the consumer compares the new context value to the old (memoized) one. This is necessary because of Suspense and Offscreen — those components defer work from one render into a later one. When the subtree continues rendering, the stack from the previous render is no longer available. But the memoized values on the dependencies list are. This requires a bit more work when a consumer bails out, but nothing considerable, and there are ways we could optimize it even further. Conceptually, this model is really appealing, since it matches how our other features "reactively" detect changes — `useMemo`, `useEffect`, `getDerivedStateFromProps`, the built-in cache, and so on. I also intentionally dropped support for `unstable_calculateChangedBits`. We're planning to remove this API anyway before the next major release, in favor of context selectors. It's an unstable feature that we never advertised; I don't think it's seen much adoption. Co-Authored-By: Josh Story <[email protected]>
For internal experimentation only. This implements `unstable_useContextSelector` behind a feature flag. It's based on [RFC 119](reactjs/rfcs#119) and [RFC 118](reactjs/rfcs#118) by @gnoff. Usage: ```js const context = useContextSelector(Context, c => c.selectedField); ``` The key feature is that if the selected value does not change between renders, the component will bail out of rendering its children, a la `memo`, `PureComponent`, or the `useState` bailout mechanism. (Unless some other state, props, or context was updated in the same render.) One difference from the RFC is that it does not return the selected value. It returns the full context object. This serves a few purposes: it discourages you from creating any new objects or derived values inside the selector, because it'll get thrown out regardless. Instead, all the selector will do is return a subfield. Then you can compute the derived value inside the component, and if needed, you memoize that derived value with `useMemo`. If all the selectors do is access a subfield, they're (theoretically) fast enough that we can call them during the propagation scan and bail out really early, without having to visit the component during the render phase. Another benefit is that it's API compatible with `useContext`. So we can put it behind a flag that falls back to regular `useContext`. The longer term vision is that these optimizations (in addition to other memoization checks, like `useMemo` and `useCallback`) are inserted automatically by a compiler. So you would write code like this: ```js const {a, b} = useContext(Context); const derived = computeDerived(a, b); ``` and it would get converted to something like this: ```js const {a} = useContextSelector(Context, context => context.a); const {b} = useContextSelector(Context, context => context.b); const derived = useMemo(() => computeDerived(a, b), [a, b]); ``` (Though not this exactly. Some lower level compiler output target.)
When a context provider changes, we scan the tree for matching consumers and mark them as dirty so that we know they have pending work. This prevents us from bailing out if, say, an intermediate wrapper is memoized. Currently, we propagate these changes eagerly, at the provider. However, in many cases, we would have ended up visiting the consumer nodes anyway, as part of the normal render traversal, because there's no memoized node in between that bails out. We can save CPU cycles by propagating changes only when we hit a memoized component — so, instead of propagating eagerly at the provider, we propagate lazily if or when something bails out. Most of our bailout logic is centralized in `bailoutOnAlreadyFinishedWork`, so this ended up being not that difficult to implement correctly. There are some exceptions: Suspense and Offscreen. Those are special because they sometimes defer the rendering of their children to a completely separate render cycle. In those cases, we must take extra care to propagate *all* the context changes, not just the first one. I'm pleasantly surprised at how little I needed to change in this initial implementation. I was worried I'd have to use the reconciler fork, but I ended up being able to wrap all my changes in a regular feature flag. So, we could run an experiment in parallel to our other ones. I do consider this a risky rollout overall because of the potential for subtle semantic deviations. However, the model is simple enough that I don't expect us to have trouble fixing regressions if or when they arise during internal dogfooding. --- This is largely based on [RFC#118](reactjs/rfcs#118), by @gnoff. I did deviate in some of the implementation details, though. The main one is how I chose to track context changes. Instead of storing a dirty flag on the stack, I added a `memoizedValue` field to the context dependency object. Then, to check if something has changed, the consumer compares the new context value to the old (memoized) one. This is necessary because of Suspense and Offscreen — those components defer work from one render into a later one. When the subtree continues rendering, the stack from the previous render is no longer available. But the memoized values on the dependencies list are. This requires a bit more work when a consumer bails out, but nothing considerable, and there are ways we could optimize it even further. Conceptually, this model is really appealing, since it matches how our other features "reactively" detect changes — `useMemo`, `useEffect`, `getDerivedStateFromProps`, the built-in cache, and so on. I also intentionally dropped support for `unstable_calculateChangedBits`. We're planning to remove this API anyway before the next major release, in favor of context selectors. It's an unstable feature that we never advertised; I don't think it's seen much adoption. Co-Authored-By: Josh Story <[email protected]>
When a context provider changes, we scan the tree for matching consumers and mark them as dirty so that we know they have pending work. This prevents us from bailing out if, say, an intermediate wrapper is memoized. Currently, we propagate these changes eagerly, at the provider. However, in many cases, we would have ended up visiting the consumer nodes anyway, as part of the normal render traversal, because there's no memoized node in between that bails out. We can save CPU cycles by propagating changes only when we hit a memoized component — so, instead of propagating eagerly at the provider, we propagate lazily if or when something bails out. Most of our bailout logic is centralized in `bailoutOnAlreadyFinishedWork`, so this ended up being not that difficult to implement correctly. There are some exceptions: Suspense and Offscreen. Those are special because they sometimes defer the rendering of their children to a completely separate render cycle. In those cases, we must take extra care to propagate *all* the context changes, not just the first one. I'm pleasantly surprised at how little I needed to change in this initial implementation. I was worried I'd have to use the reconciler fork, but I ended up being able to wrap all my changes in a regular feature flag. So, we could run an experiment in parallel to our other ones. I do consider this a risky rollout overall because of the potential for subtle semantic deviations. However, the model is simple enough that I don't expect us to have trouble fixing regressions if or when they arise during internal dogfooding. --- This is largely based on [RFC#118](reactjs/rfcs#118), by @gnoff. I did deviate in some of the implementation details, though. The main one is how I chose to track context changes. Instead of storing a dirty flag on the stack, I added a `memoizedValue` field to the context dependency object. Then, to check if something has changed, the consumer compares the new context value to the old (memoized) one. This is necessary because of Suspense and Offscreen — those components defer work from one render into a later one. When the subtree continues rendering, the stack from the previous render is no longer available. But the memoized values on the dependencies list are. This requires a bit more work when a consumer bails out, but nothing considerable, and there are ways we could optimize it even further. Conceptually, this model is really appealing, since it matches how our other features "reactively" detect changes — `useMemo`, `useEffect`, `getDerivedStateFromProps`, the built-in cache, and so on. I also intentionally dropped support for `unstable_calculateChangedBits`. We're planning to remove this API anyway before the next major release, in favor of context selectors. It's an unstable feature that we never advertised; I don't think it's seen much adoption. Co-Authored-By: Josh Story <[email protected]>
When a context provider changes, we scan the tree for matching consumers and mark them as dirty so that we know they have pending work. This prevents us from bailing out if, say, an intermediate wrapper is memoized. Currently, we propagate these changes eagerly, at the provider. However, in many cases, we would have ended up visiting the consumer nodes anyway, as part of the normal render traversal, because there's no memoized node in between that bails out. We can save CPU cycles by propagating changes only when we hit a memoized component — so, instead of propagating eagerly at the provider, we propagate lazily if or when something bails out. Most of our bailout logic is centralized in `bailoutOnAlreadyFinishedWork`, so this ended up being not that difficult to implement correctly. There are some exceptions: Suspense and Offscreen. Those are special because they sometimes defer the rendering of their children to a completely separate render cycle. In those cases, we must take extra care to propagate *all* the context changes, not just the first one. I'm pleasantly surprised at how little I needed to change in this initial implementation. I was worried I'd have to use the reconciler fork, but I ended up being able to wrap all my changes in a regular feature flag. So, we could run an experiment in parallel to our other ones. I do consider this a risky rollout overall because of the potential for subtle semantic deviations. However, the model is simple enough that I don't expect us to have trouble fixing regressions if or when they arise during internal dogfooding. --- This is largely based on [RFC#118](reactjs/rfcs#118), by @gnoff. I did deviate in some of the implementation details, though. The main one is how I chose to track context changes. Instead of storing a dirty flag on the stack, I added a `memoizedValue` field to the context dependency object. Then, to check if something has changed, the consumer compares the new context value to the old (memoized) one. This is necessary because of Suspense and Offscreen — those components defer work from one render into a later one. When the subtree continues rendering, the stack from the previous render is no longer available. But the memoized values on the dependencies list are. This requires a bit more work when a consumer bails out, but nothing considerable, and there are ways we could optimize it even further. Conceptually, this model is really appealing, since it matches how our other features "reactively" detect changes — `useMemo`, `useEffect`, `getDerivedStateFromProps`, the built-in cache, and so on. I also intentionally dropped support for `unstable_calculateChangedBits`. We're planning to remove this API anyway before the next major release, in favor of context selectors. It's an unstable feature that we never advertised; I don't think it's seen much adoption. Co-Authored-By: Josh Story <[email protected]>
* Move context comparison to consumer In the lazy context implementation, not all context changes are propagated from the provider, so we can't rely on the propagation alone to mark the consumer as dirty. The consumer needs to compare to the previous value, like we do for state and context. I added a `memoizedValue` field to the context dependency type. Then in the consumer, we iterate over the current dependencies to see if something changed. We only do this iteration after props and state has already bailed out, so it's a relatively uncommon path, except at the root of a changed subtree. Alternatively, we could move these comparisons into `readContext`, but that's a much hotter path, so I think this is an appropriate trade off. * [Experiment] Lazily propagate context changes When a context provider changes, we scan the tree for matching consumers and mark them as dirty so that we know they have pending work. This prevents us from bailing out if, say, an intermediate wrapper is memoized. Currently, we propagate these changes eagerly, at the provider. However, in many cases, we would have ended up visiting the consumer nodes anyway, as part of the normal render traversal, because there's no memoized node in between that bails out. We can save CPU cycles by propagating changes only when we hit a memoized component — so, instead of propagating eagerly at the provider, we propagate lazily if or when something bails out. Most of our bailout logic is centralized in `bailoutOnAlreadyFinishedWork`, so this ended up being not that difficult to implement correctly. There are some exceptions: Suspense and Offscreen. Those are special because they sometimes defer the rendering of their children to a completely separate render cycle. In those cases, we must take extra care to propagate *all* the context changes, not just the first one. I'm pleasantly surprised at how little I needed to change in this initial implementation. I was worried I'd have to use the reconciler fork, but I ended up being able to wrap all my changes in a regular feature flag. So, we could run an experiment in parallel to our other ones. I do consider this a risky rollout overall because of the potential for subtle semantic deviations. However, the model is simple enough that I don't expect us to have trouble fixing regressions if or when they arise during internal dogfooding. --- This is largely based on [RFC#118](reactjs/rfcs#118), by @gnoff. I did deviate in some of the implementation details, though. The main one is how I chose to track context changes. Instead of storing a dirty flag on the stack, I added a `memoizedValue` field to the context dependency object. Then, to check if something has changed, the consumer compares the new context value to the old (memoized) one. This is necessary because of Suspense and Offscreen — those components defer work from one render into a later one. When the subtree continues rendering, the stack from the previous render is no longer available. But the memoized values on the dependencies list are. This requires a bit more work when a consumer bails out, but nothing considerable, and there are ways we could optimize it even further. Conceptually, this model is really appealing, since it matches how our other features "reactively" detect changes — `useMemo`, `useEffect`, `getDerivedStateFromProps`, the built-in cache, and so on. I also intentionally dropped support for `unstable_calculateChangedBits`. We're planning to remove this API anyway before the next major release, in favor of context selectors. It's an unstable feature that we never advertised; I don't think it's seen much adoption. Co-Authored-By: Josh Story <[email protected]> * Propagate all contexts in single pass Instead of propagating the tree once per changed context, we can check all the contexts in a single propagation. This inverts the two loops so that the faster loop (O(numberOfContexts)) is inside the more expensive loop (O(numberOfFibers * avgContextDepsPerFiber)). This adds a bit of overhead to the case where only a single context changes because you have to unwrap the context from the array. I'm also unsure if this will hurt cache locality. Co-Authored-By: Josh Story <[email protected]> * Stop propagating at nearest dependency match Because we now propagate all context providers in a single traversal, we can defer context propagation to a subtree without losing information about which context providers we're deferring — it's all of them. Theoretically, this is a big optimization because it means we'll never propagate to any tree that has work scheduled on it, nor will we ever propagate the same tree twice. There's an awkward case related to bailing out of the siblings of a context consumer. Because those siblings don't bail out until after they've already entered the begin phase, we have to do extra work to make sure they don't unecessarily propagate context again. We could avoid this by adding an earlier bailout for sibling nodes, something we've discussed in the past. We should consider this during the next refactor of the fiber tree structure. Co-Authored-By: Josh Story <[email protected]> * Mark trees that need propagation in readContext Instead of storing matched context consumers in a Set, we can mark when a consumer receives an update inside `readContext`. I hesistated to put anything in this function because it's such a hot path, but so are bail outs. Fortunately, we only need to set this flag once, the first time a context is read. So I think it's a reasonable trade off. In exchange, propagation is faster because we no longer need to accumulate a Set of matched consumers, and fiber bailouts are faster because we don't need to consult that Set. And the code is simpler. Co-authored-by: Josh Story <[email protected]>
* Move context comparison to consumer In the lazy context implementation, not all context changes are propagated from the provider, so we can't rely on the propagation alone to mark the consumer as dirty. The consumer needs to compare to the previous value, like we do for state and context. I added a `memoizedValue` field to the context dependency type. Then in the consumer, we iterate over the current dependencies to see if something changed. We only do this iteration after props and state has already bailed out, so it's a relatively uncommon path, except at the root of a changed subtree. Alternatively, we could move these comparisons into `readContext`, but that's a much hotter path, so I think this is an appropriate trade off. * [Experiment] Lazily propagate context changes When a context provider changes, we scan the tree for matching consumers and mark them as dirty so that we know they have pending work. This prevents us from bailing out if, say, an intermediate wrapper is memoized. Currently, we propagate these changes eagerly, at the provider. However, in many cases, we would have ended up visiting the consumer nodes anyway, as part of the normal render traversal, because there's no memoized node in between that bails out. We can save CPU cycles by propagating changes only when we hit a memoized component — so, instead of propagating eagerly at the provider, we propagate lazily if or when something bails out. Most of our bailout logic is centralized in `bailoutOnAlreadyFinishedWork`, so this ended up being not that difficult to implement correctly. There are some exceptions: Suspense and Offscreen. Those are special because they sometimes defer the rendering of their children to a completely separate render cycle. In those cases, we must take extra care to propagate *all* the context changes, not just the first one. I'm pleasantly surprised at how little I needed to change in this initial implementation. I was worried I'd have to use the reconciler fork, but I ended up being able to wrap all my changes in a regular feature flag. So, we could run an experiment in parallel to our other ones. I do consider this a risky rollout overall because of the potential for subtle semantic deviations. However, the model is simple enough that I don't expect us to have trouble fixing regressions if or when they arise during internal dogfooding. --- This is largely based on [RFC#118](reactjs/rfcs#118), by @gnoff. I did deviate in some of the implementation details, though. The main one is how I chose to track context changes. Instead of storing a dirty flag on the stack, I added a `memoizedValue` field to the context dependency object. Then, to check if something has changed, the consumer compares the new context value to the old (memoized) one. This is necessary because of Suspense and Offscreen — those components defer work from one render into a later one. When the subtree continues rendering, the stack from the previous render is no longer available. But the memoized values on the dependencies list are. This requires a bit more work when a consumer bails out, but nothing considerable, and there are ways we could optimize it even further. Conceptually, this model is really appealing, since it matches how our other features "reactively" detect changes — `useMemo`, `useEffect`, `getDerivedStateFromProps`, the built-in cache, and so on. I also intentionally dropped support for `unstable_calculateChangedBits`. We're planning to remove this API anyway before the next major release, in favor of context selectors. It's an unstable feature that we never advertised; I don't think it's seen much adoption. Co-Authored-By: Josh Story <[email protected]> * Propagate all contexts in single pass Instead of propagating the tree once per changed context, we can check all the contexts in a single propagation. This inverts the two loops so that the faster loop (O(numberOfContexts)) is inside the more expensive loop (O(numberOfFibers * avgContextDepsPerFiber)). This adds a bit of overhead to the case where only a single context changes because you have to unwrap the context from the array. I'm also unsure if this will hurt cache locality. Co-Authored-By: Josh Story <[email protected]> * Stop propagating at nearest dependency match Because we now propagate all context providers in a single traversal, we can defer context propagation to a subtree without losing information about which context providers we're deferring — it's all of them. Theoretically, this is a big optimization because it means we'll never propagate to any tree that has work scheduled on it, nor will we ever propagate the same tree twice. There's an awkward case related to bailing out of the siblings of a context consumer. Because those siblings don't bail out until after they've already entered the begin phase, we have to do extra work to make sure they don't unecessarily propagate context again. We could avoid this by adding an earlier bailout for sibling nodes, something we've discussed in the past. We should consider this during the next refactor of the fiber tree structure. Co-Authored-By: Josh Story <[email protected]> * Mark trees that need propagation in readContext Instead of storing matched context consumers in a Set, we can mark when a consumer receives an update inside `readContext`. I hesistated to put anything in this function because it's such a hot path, but so are bail outs. Fortunately, we only need to set this flag once, the first time a context is read. So I think it's a reasonable trade off. In exchange, propagation is faster because we no longer need to accumulate a Set of matched consumers, and fiber bailouts are faster because we don't need to consult that Set. And the code is simpler. Co-authored-by: Josh Story <[email protected]>
For internal experimentation only. This implements `unstable_useContextSelector` behind a feature flag. It's based on [RFC 119](reactjs/rfcs#119) and [RFC 118](reactjs/rfcs#118) by @gnoff. Usage: ```js const context = useContextSelector(Context, c => c.selectedField); ``` The key feature is that if the selected value does not change between renders, the component will bail out of rendering its children, a la `memo`, `PureComponent`, or the `useState` bailout mechanism. (Unless some other state, props, or context was updated in the same render.) One difference from the RFC is that it does not return the selected value. It returns the full context object. This serves a few purposes: it discourages you from creating any new objects or derived values inside the selector, because it'll get thrown out regardless. Instead, all the selector will do is return a subfield. Then you can compute the derived value inside the component, and if needed, you memoize that derived value with `useMemo`. If all the selectors do is access a subfield, they're (theoretically) fast enough that we can call them during the propagation scan and bail out really early, without having to visit the component during the render phase. Another benefit is that it's API compatible with `useContext`. So we can put it behind a flag that falls back to regular `useContext`. The longer term vision is that these optimizations (in addition to other memoization checks, like `useMemo` and `useCallback`) are inserted automatically by a compiler. So you would write code like this: ```js const {a, b} = useContext(Context); const derived = computeDerived(a, b); ``` and it would get converted to something like this: ```js const {a} = useContextSelector(Context, context => context.a); const {b} = useContextSelector(Context, context => context.b); const derived = useMemo(() => computeDerived(a, b), [a, b]); ``` (Though not this exactly. Some lower level compiler output target.)
For internal experimentation only. This implements `unstable_useContextSelector` behind a feature flag. It's based on [RFC 119](reactjs/rfcs#119) and [RFC 118](reactjs/rfcs#118) by @gnoff. Usage: ```js const context = useContextSelector(Context, c => c.selectedField); ``` The key feature is that if the selected value does not change between renders, the component will bail out of rendering its children, a la `memo`, `PureComponent`, or the `useState` bailout mechanism. (Unless some other state, props, or context was updated in the same render.) One difference from the RFC is that it does not return the selected value. It returns the full context object. This serves a few purposes: it discourages you from creating any new objects or derived values inside the selector, because it'll get thrown out regardless. Instead, all the selector will do is return a subfield. Then you can compute the derived value inside the component, and if needed, you memoize that derived value with `useMemo`. If all the selectors do is access a subfield, they're (theoretically) fast enough that we can call them during the propagation scan and bail out really early, without having to visit the component during the render phase. Another benefit is that it's API compatible with `useContext`. So we can put it behind a flag that falls back to regular `useContext`. The longer term vision is that these optimizations (in addition to other memoization checks, like `useMemo` and `useCallback`) are inserted automatically by a compiler. So you would write code like this: ```js const {a, b} = useContext(Context); const derived = computeDerived(a, b); ``` and it would get converted to something like this: ```js const {a} = useContextSelector(Context, context => context.a); const {b} = useContextSelector(Context, context => context.b); const derived = useMemo(() => computeDerived(a, b), [a, b]); ``` (Though not this exactly. Some lower level compiler output target.)
For internal experimentation only. This implements `unstable_useContextSelector` behind a feature flag. It's based on [RFC 119](reactjs/rfcs#119) and [RFC 118](reactjs/rfcs#118) by @gnoff. Usage: ```js const context = useContextSelector(Context, c => c.selectedField); ``` The key feature is that if the selected value does not change between renders, the component will bail out of rendering its children, a la `memo`, `PureComponent`, or the `useState` bailout mechanism. (Unless some other state, props, or context was updated in the same render.) One difference from the RFC is that it does not return the selected value. It returns the full context object. This serves a few purposes: it discourages you from creating any new objects or derived values inside the selector, because it'll get thrown out regardless. Instead, all the selector will do is return a subfield. Then you can compute the derived value inside the component, and if needed, you memoize that derived value with `useMemo`. If all the selectors do is access a subfield, they're (theoretically) fast enough that we can call them during the propagation scan and bail out really early, without having to visit the component during the render phase. Another benefit is that it's API compatible with `useContext`. So we can put it behind a flag that falls back to regular `useContext`. The longer term vision is that these optimizations (in addition to other memoization checks, like `useMemo` and `useCallback`) are inserted automatically by a compiler. So you would write code like this: ```js const {a, b} = useContext(Context); const derived = computeDerived(a, b); ``` and it would get converted to something like this: ```js const {a} = useContextSelector(Context, context => context.a); const {b} = useContextSelector(Context, context => context.b); const derived = useMemo(() => computeDerived(a, b), [a, b]); ``` (Though not this exactly. Some lower level compiler output target.)
For internal experimentation only. This implements `unstable_useContextSelector` behind a feature flag. It's based on [RFC 119](reactjs/rfcs#119) and [RFC 118](reactjs/rfcs#118) by @gnoff. Usage: ```js const context = useContextSelector(Context, c => c.selectedField); ``` The key feature is that if the selected value does not change between renders, the component will bail out of rendering its children, a la `memo`, `PureComponent`, or the `useState` bailout mechanism. (Unless some other state, props, or context was updated in the same render.) One difference from the RFC is that it does not return the selected value. It returns the full context object. This serves a few purposes: it discourages you from creating any new objects or derived values inside the selector, because it'll get thrown out regardless. Instead, all the selector will do is return a subfield. Then you can compute the derived value inside the component, and if needed, you memoize that derived value with `useMemo`. If all the selectors do is access a subfield, they're (theoretically) fast enough that we can call them during the propagation scan and bail out really early, without having to visit the component during the render phase. Another benefit is that it's API compatible with `useContext`. So we can put it behind a flag that falls back to regular `useContext`. The longer term vision is that these optimizations (in addition to other memoization checks, like `useMemo` and `useCallback`) are inserted automatically by a compiler. So you would write code like this: ```js const {a, b} = useContext(Context); const derived = computeDerived(a, b); ``` and it would get converted to something like this: ```js const {a} = useContextSelector(Context, context => context.a); const {b} = useContextSelector(Context, context => context.b); const derived = useMemo(() => computeDerived(a, b), [a, b]); ``` (Though not this exactly. Some lower level compiler output target.)
For internal experimentation only. This implements `unstable_useContextSelector` behind a feature flag. It's based on [RFC 119](reactjs/rfcs#119) and [RFC 118](reactjs/rfcs#118) by @gnoff. Usage: ```js const context = useContextSelector(Context, c => c.selectedField); ``` The key feature is that if the selected value does not change between renders, the component will bail out of rendering its children, a la `memo`, `PureComponent`, or the `useState` bailout mechanism. (Unless some other state, props, or context was updated in the same render.) One difference from the RFC is that it does not return the selected value. It returns the full context object. This serves a few purposes: it discourages you from creating any new objects or derived values inside the selector, because it'll get thrown out regardless. Instead, all the selector will do is return a subfield. Then you can compute the derived value inside the component, and if needed, you memoize that derived value with `useMemo`. If all the selectors do is access a subfield, they're (theoretically) fast enough that we can call them during the propagation scan and bail out really early, without having to visit the component during the render phase. Another benefit is that it's API compatible with `useContext`. So we can put it behind a flag that falls back to regular `useContext`. The longer term vision is that these optimizations (in addition to other memoization checks, like `useMemo` and `useCallback`) are inserted automatically by a compiler. So you would write code like this: ```js const {a, b} = useContext(Context); const derived = computeDerived(a, b); ``` and it would get converted to something like this: ```js const {a} = useContextSelector(Context, context => context.a); const {b} = useContextSelector(Context, context => context.b); const derived = useMemo(() => computeDerived(a, b), [a, b]); ``` (Though not this exactly. Some lower level compiler output target.)
For internal experimentation only. This implements `unstable_useContextSelector` behind a feature flag. It's based on [RFC 119](reactjs/rfcs#119) and [RFC 118](reactjs/rfcs#118) by @gnoff. Usage: ```js const context = useContextSelector(Context, c => c.selectedField); ``` The key feature is that if the selected value does not change between renders, the component will bail out of rendering its children, a la `memo`, `PureComponent`, or the `useState` bailout mechanism. (Unless some other state, props, or context was updated in the same render.) One difference from the RFC is that it does not return the selected value. It returns the full context object. This serves a few purposes: it discourages you from creating any new objects or derived values inside the selector, because it'll get thrown out regardless. Instead, all the selector will do is return a subfield. Then you can compute the derived value inside the component, and if needed, you memoize that derived value with `useMemo`. If all the selectors do is access a subfield, they're (theoretically) fast enough that we can call them during the propagation scan and bail out really early, without having to visit the component during the render phase. Another benefit is that it's API compatible with `useContext`. So we can put it behind a flag that falls back to regular `useContext`. The longer term vision is that these optimizations (in addition to other memoization checks, like `useMemo` and `useCallback`) are inserted automatically by a compiler. So you would write code like this: ```js const {a, b} = useContext(Context); const derived = computeDerived(a, b); ``` and it would get converted to something like this: ```js const {a} = useContextSelector(Context, context => context.a); const {b} = useContextSelector(Context, context => context.b); const derived = useMemo(() => computeDerived(a, b), [a, b]); ``` (Though not this exactly. Some lower level compiler output target.)
Small update. We've implemented a version of this, and it seems to have worked well. It's currently behind a flag but we'll need to do more testing and rollout before we can switch it on by default. I'm not sure if we have any noticeable perf data since the initial experiment was small. |
We’ve essentially implemented a version of this so it’s fair to say that this has been approved. Thank you! |
hey @gaearon will this be available in React 18? Could we use with the flag? |
We'll probably enable it in some 18.x minor release. Until then you can build from main with the flag on if you'd like to try it. |
Hopefully this will be available in React version 18 |
View Rendered Text
This RFC describes a new Context Propagation implementation which has improved performance characteristics in certain cases by using the work React is already going to do before resorting to explicit context dependencies searches. This lazy implementation also has qualitative differences from the current eager approach which may allow new kinds of context update bailouts to be implemented
It does not modify the public API of React in any way.
Motivation
Addendum
Example: https://codesandbox.io/s/react-lazy-context-propagation-67e5j
Implementation: https://github.com/gnoff/react/pull/4/files