Skip to content
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

Merged
merged 1 commit into from Aug 19, 2021
Merged

Conversation

@gnoff
Copy link
Contributor

@gnoff gnoff commented Jun 21, 2019

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

These two changes allow for propagation tree traversal to be somewhere between 0 and 1...

...unmounts will now happen before context propagation begins.

...will allow for some interesting new APIs.

Addendum

Example: https://codesandbox.io/s/react-lazy-context-propagation-67e5j
Implementation: https://github.com/gnoff/react/pull/4/files

@sebmarkbage
Copy link
Collaborator

@sebmarkbage sebmarkbage commented Jan 6, 2020

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.

Loading

@gnoff
Copy link
Contributor Author

@gnoff gnoff commented Jan 7, 2020

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

Loading

@zzz6519003
Copy link

@zzz6519003 zzz6519003 commented Jan 30, 2020

like Lazy Eval?

Loading

@gnoff gnoff mentioned this pull request Feb 28, 2020
acdlite added a commit to acdlite/react that referenced this issue Jan 23, 2021
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`).
acdlite added a commit to acdlite/react that referenced this issue Jan 23, 2021
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`).
acdlite added a commit to acdlite/react that referenced this issue Feb 6, 2021
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`).
acdlite added a commit to acdlite/react that referenced this issue Feb 11, 2021
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`).
acdlite added a commit to acdlite/react that referenced this issue Feb 26, 2021
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 <jcs.gnoff@gmail.com>
acdlite added a commit to acdlite/react that referenced this issue Feb 26, 2021
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 <jcs.gnoff@gmail.com>
acdlite added a commit to acdlite/react that referenced this issue Feb 26, 2021
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 <jcs.gnoff@gmail.com>
acdlite added a commit to acdlite/react that referenced this issue Feb 26, 2021
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 <jcs.gnoff@gmail.com>
acdlite added a commit to acdlite/react that referenced this issue Feb 26, 2021
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 <jcs.gnoff@gmail.com>
acdlite added a commit to acdlite/react that referenced this issue Feb 26, 2021
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 <jcs.gnoff@gmail.com>
acdlite added a commit to acdlite/react that referenced this issue Feb 26, 2021
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 <jcs.gnoff@gmail.com>
acdlite added a commit to acdlite/react that referenced this issue Feb 26, 2021
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 <jcs.gnoff@gmail.com>
acdlite added a commit to acdlite/react that referenced this issue Feb 26, 2021
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 <jcs.gnoff@gmail.com>
acdlite added a commit to acdlite/react that referenced this issue Feb 26, 2021
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 <jcs.gnoff@gmail.com>
acdlite added a commit to acdlite/react that referenced this issue Feb 26, 2021
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 <jcs.gnoff@gmail.com>
acdlite added a commit to acdlite/react that referenced this issue Feb 26, 2021
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.)
acdlite added a commit to acdlite/react that referenced this issue Feb 26, 2021
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.)
acdlite added a commit to acdlite/react that referenced this issue Feb 27, 2021
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 <jcs.gnoff@gmail.com>
acdlite added a commit to acdlite/react that referenced this issue Feb 27, 2021
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.)
acdlite added a commit to acdlite/react that referenced this issue Mar 1, 2021
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 <jcs.gnoff@gmail.com>
acdlite added a commit to acdlite/react that referenced this issue Mar 2, 2021
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 <jcs.gnoff@gmail.com>
acdlite added a commit to acdlite/react that referenced this issue Mar 7, 2021
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 <jcs.gnoff@gmail.com>
acdlite added a commit to facebook/react that referenced this issue Mar 7, 2021
* 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 <jcs.gnoff@gmail.com>

* 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 <jcs.gnoff@gmail.com>

* 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 <jcs.gnoff@gmail.com>

* 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 <jcs.gnoff@gmail.com>
koto added a commit to koto/react that referenced this issue Jun 15, 2021
* 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 <jcs.gnoff@gmail.com>

* 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 <jcs.gnoff@gmail.com>

* 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 <jcs.gnoff@gmail.com>

* 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 <jcs.gnoff@gmail.com>
acdlite added a commit to acdlite/react that referenced this issue Jul 7, 2021
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.)
acdlite added a commit to acdlite/react that referenced this issue Jul 7, 2021
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.)
acdlite added a commit to acdlite/react that referenced this issue Jul 7, 2021
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.)
acdlite added a commit to acdlite/react that referenced this issue Jul 8, 2021
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.)
acdlite added a commit to acdlite/react that referenced this issue Jul 8, 2021
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.)
acdlite added a commit to acdlite/react that referenced this issue Jul 10, 2021
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.)
@gaearon
Copy link
Member

@gaearon gaearon commented Aug 18, 2021

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.

Loading

@gaearon gaearon merged commit babeebc into reactjs:master Aug 19, 2021
1 check passed
Loading
@gaearon
Copy link
Member

@gaearon gaearon commented Aug 19, 2021

We’ve essentially implemented a version of this so it’s fair to say that this has been approved. Thank you!

Loading

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

5 participants