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
Reactivity: optimize full array iterations #673
Comments
I would suggest doing a PR with benchmarks, since this feels like premature optimization to me. The cost of managing |
@yyx990803 Sure, can do. Maybe we should define some kind of target size? |
I think around 1000 would be the upside of what we typically see? Reopening since I think this is worth pursuing if a benchmark shows significant improvement. |
@yyx990803 I built a benchmark to evaluate what kind of improvement we are talking about. I would like to engage in a discussion about what the best solution is here, as there are many possible designs and trade-offs. The benchmark is based on optimized arrays in userland. App under testI built a fake process explorer, with a little sparkline for each entry. Of course, this scenario is made specifically to stress test array observation. Source code is here: You can tweak the size of arrays and pick up an implementation amongst 3:
Implementation notesMy arrays are shallow (they don't make their contents reactive magically) but it's something that can be factored in. Goal here is to have ballpark numbers to discuss. Sparklines are arrays of numbers, so it would make sense to be shallow in this use-case anyway. I don't account for There's no solution that is best in every scenario. This benchmark focuses on full array iteration, which is in my opinion the most common usage of reactive arrays in applications, either directly ResultsAverage results on my machine, minified production build, Edge 81 64 bits.
So what I'm targeting here is the overhead of tracking every array entry individually. On an array of size This isn't strictly a micro-benchmark as some "real work" is done: painting a canvas, Vue updates the DOM. This explains why at very small sizes (20 arrays of 10 elements), there is no visible improvement: it doesn't matter compared to diffing and actual DOM work. But at larger sizes, the improvements can be big, as you can see above. Both in speed and memory allocation. ConclusionI think there are significant gains to be had, at least for scenarios handling large quantity of data such as Dataviz, Charts, Dashboards, Datagrids, ... I am not sure what the best API would be, though, as there are trade-offs everywhere. My original idea of magically detecting full iteration would work but it has limitations. It wouldn't work with random access (reverse iteration, full iteration starting at position 1, sorting, ...) which means lots of missed opportunities. Moreover it could be in the ballpark of What do you think? |
This is very informative, thanks! So here's my takeaways from the benchmark:
On the other hand, I noticed that you are using stateful objects that updates themselves (the If I were building the same application, I'd simply treat the entire data set as immutable and construct it fresh on every tick, and with useTimer(500, () => {
data.processes = markNonReactive(data.processes.map(p => {
return p.tick() // returns a new copy of itself
})
}) Btw, to properly encapsulate a timer that auto-stops on component unmount: // timer.js
import { onUmounted } from 'vue'
function useTimer(interval, cb) {
const handle = setInterval(cb, interval)
onUnmounted(() => {
clearInterval(handle)
})
} To sum up: I don't think we should introduce separate APIs for this type of Array access optimization. Rather, we should document and recommend strategies for optimizing large Array iterations (i.e. construct fresh non-reactive arrays on each update). |
Playing with this benchmark got me thinking. The beauty of having explicit reactivity is the flexibility it gives. I think it would be nice if:
This philosophy would tie in with #675. You wouldn't want to implement all possible primitives, much less maintain it in the public API surface of Vue. Not providing the required APIs stiffles innovations from the community. The faster array I used here (or a derivative of it) could be a NPM library. People who mutate large arrays may benefit from importing it. Not saying I'm gonna create a NPM package, just pointing out how such improvements could be opted-in by the community in specific cases. About swapping in an immutable array: yes it's a way to optimize this specific example. Thanks for the tips! ;) |
I'm closing this issue because I don't think there's more to say about it. With access to I think it's a better choice for most use cases, but in the end you can easily use the one you want in your code. The one change I'm still hoping we can have is an official way to mark our objects as being "reactive" without using the built-ins, so that they mesh well with Vue core. This is tracked by vuejs/rfcs#129 |
What problem does this feature solve?
I propose a performance optimization.
Applications use plenty of arrays. Full array iteration is extremely common:
v-for
performs afor (i = 0; i < length; i ) array[i]
loop when rendering;.filter()
,.sort()
,.map()
and co. All of these result in a full iteration;.forEach()
.This top use-case is tracked like any normal object, key by key. If I have a reactive array containing 100 items, then calling
.filter
in a computed will result in 102 new tracking entries: one forfilter
, one forlength
and 100 more from [0] to [99].Each one results in a
Set
allocation one entry in the reversedeps
array the required upkeep every time the effect runs again.Because both (1) full array iteration is very common; and (2) arrays can be quite big, making tracking expensive; I think this may be worthy of a special optimization.
Here's one idea: detect full enumeration and only register a single symbol
ALL_KEY
in response.It could work like this:
arrayRanges: Map<Array, number>
oneffect
.track
is called on an arraytarget
, if the key is numeric:target
is not inactiveEffect.arrayRanges
and key is 0, add it with value0
.target
is inarrayRanges
andkey === arrayRanges.get(target) 1
then incrementarrayRanges
.run
:arrayRanges
that is equals totarget.length - 1
, create a dependency with symbolALL_KEY
.arrayRanges
trigger
, iftarget
is an array andkey
is numeric, additionally triggerALL_KEY
.What does the proposed API look like?
No new public API
The text was updated successfully, but these errors were encountered: