feat(reactivity): bitwise dep markers to optimize re-tracking #4017
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Currently for every effect run, all existing dependencies are first cleaned up and then retracked. This involves Set additions and removals. In many cases, dependencies rarely change so we already discussed that there was room for optmization here (#2345 (comment)).
This PR optimizes re-tracking of effect dependencies.
It uses markers which identify whether a dependency is new, stable or old. Because effect execution/tracking may be recursive, the markers are actually bitwise and support up to 30 levels deep (the amount of bits we can safely use for a SMI small integer). When the depth is more than 30 levels (notice that this is unlikely to occur), the 'old' full cleanup method is used instead.
Component render/update recursion
Notice that this optimization, unlike the old cleanup/retrack method, postpones deleting the old dependencies until the end of the effect execution. Ideally, this should not make a difference, but I found one situation in which it did: the component update.
The
onBeforeUpdate
test in apiLifecycle.spec.ts failed. It turned out that the component update 'effect' has theallowRecursive
flag turned on, because child components must be able to re-trigger the parent. However, when one of the (beforeMount/beforeUpdate) life cycle hooks is invoked, it is possible that this triggers any of the dependencies of the render method. This causes the render/update effect to be queued again unnecessarily.Currently, the 'cleanup' cleared all dependencies just in time, making sure that this recursive trigger doesn't occur (I don't think this is intentional, it rather feels like a lucky side effect to me). To make this PR work, I had to explicitly disable allowRecursive while executing these pre-render hooks.
Performance effect
Ideally, this algorithm only needs to iterate the Dep array twice without having to touch any of the sets. It is a robust performance enhancement, and performs stable even when the order or tracked dependencies changes!
This has a huge boost on all effects, and especially those with a lot of dependencies. The mix benchmark, which represents a real-world scenario, was boosted by almost 40%!