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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Derive computed states only when required for delayed Reactions #3819

Closed
wants to merge 15 commits into from

Conversation

taj-p
Copy link

@taj-p taj-p commented Jan 24, 2024

Derive computed states only when required for delayed Reactions

Fixes #3724

Context

We use MobX with React. Due to the above issue, we are triggering unnecessary derivations for computed values (and contexts) for autoruns with delay or custom schedulers. This means that, at times, the user presses a button on screen and a render is scheduled, but, before the browser paints, it has to compute some derivation that is not required for the render.

Sometimes our derivations are expensive, so we really rely on deferring the (re)calculation of computed values until a delayed autorun needs to read them. Moreover, without these changes, we trigger unnescessary (re)calculations prior to when a delayed autorun runs.

image

Changes

The core change of this PR is to lift the "scheduler" from the autorun and reaction into Reaction. The rationale for this change is (I haven't contributed to this library before, so please scrutinise these reasons closely 馃檹 ):

It is within Reaction that shouldCompute runs. I.e., the Reaction itself determines whether its been invalidated and needs to run its onInvalidate, so, I reasoned that it should also be responsible for scheduling that invalidation.

if (shouldCompute(this)) {

Benefits

Again, I am a MobX noob, so these are the perceived benefits from a novice's POV:

  1. We only derive computed values when they're read when they're needed, both reducing the number of times some state is derived (during delay) and only deriving them when needed.
  2. The autorun and reaction APIs appear a bit simpler since we've consolidated the scheduling logic to Reaction.

Disadvantages

Are we moving logic to Reaction that shouldn't be moved there? I think it makes sense for a Reaction to schedule its own onInvalidation, but I don't have the full context of the library.

Code change checklist

  • Added/updated unit tests
  • [ ] Updated /docs. For new functionality, at least API.md should be updated
  • Verified that there is no significant performance drop (yarn mobx test:performance)

cc @urugator

Copy link

changeset-bot bot commented Jan 24, 2024

馃 Changeset detected

Latest commit: 3e56277

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
mobx Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Comment on lines 97 to 106
if (!this.scheduledRunReaction_) {
this.scheduledRunReaction_ = true
this.scheduler(() => {
this.scheduledRunReaction_ = false
this.runReactionImpl()
})
}
}

private runReactionImpl() {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are the core changes - Reaction dictates when it should runReactionImpl based on the passed in scheduler.

Comment on lines +88 to +133
test("autorun re-calculates computed states only when required", function (done) {
let autoruns = 0
let computedCalcs = 0

const o = mobx.observable.box(0)
const c = mobx.computed(() => {
computedCalcs++
return o.get()
})

mobx.autorun(
() => {
c.get()
autoruns++
},
{ delay: 300 }
)
expect(computedCalcs).toBe(0)

setTimeout(() => {
// We expect the first run to run the computed
expect(autoruns).toBe(1)
expect(computedCalcs).toBe(1)
}, 400)

// We shouldn't eagerly calculate the state before the autorun runs because
// the state can be mutated again before `delay` expires - if that happens, a
// re-calculation here would be wasted.
setTimeout(() => {
o.set(1)
expect(computedCalcs).toBe(1)
}, 800)

// This will re-invalidate the state so we'll need to recalculate
// again when autorun runs (when `delay` expires).
setTimeout(() => {
o.set(3)
expect(computedCalcs).toBe(1)
}, 900)

setTimeout(() => {
expect(autoruns).toBe(2)
expect(computedCalcs).toBe(2)
done()
}, 1600)
})
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @realyze - your reproduction helped craft this test

Copy link
Author

@taj-p taj-p Jan 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For reference to reviewers: the same test fails on the latest main (where you can see the unnecessary derivations being computed):

image

@taj-p taj-p changed the title Draft Derive computed states only when required in Reactions Jan 24, 2024
@taj-p taj-p marked this pull request as ready for review January 24, 2024 03:29
@taj-p taj-p changed the title Derive computed states only when required in Reactions Derive computed states only when required for delayed Reactions Jan 24, 2024
@taj-p
Copy link
Author

taj-p commented Jan 25, 2024

As per @upsuper's comments, I've realised this implementation won't work exactly as we like. Will make it a draft for the time being. In this diagram, it would emit at 2400 not 2600.

@taj-p taj-p marked this pull request as draft January 25, 2024 06:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[performance] autorun with delay deriving state unnecessarily
1 participant