optimize array tracking (fix #4318) by jods4 · Pull Request #9511 · vuejs/core (original) (raw)
This PR implements the optimisations proposed in #4318. Shortly:
- It adds a special tracking key:
ARRAY_ITERATE_KEY
, which represents a full dependency on an array (not including extra keys when handling an array as an object). - It works simply by being automatically triggered whenever
length
or an integer key is triggered on an array. - Almost all array functions have been instrumented for speed using
ARRAY_ITERATE_KEY
. - With that, there's a single track record (vs O(N)) and no proxy overhead when manipulating the array.
- Should lead to big gains in memory and speed, especially with large arrays.
- Tests for all newly instrumented functions are included
Unrelated changes included in this PR
- I made the
arrayInstrumentations
object use anull
prototype -> no need to check forownKeys
when accessing, should be ever so slightly faster on misses. - For
indexOf
and co. I optimized the existing instrumentation slightly: when the needle is not found and is not a proxy, there's no reason to do a 2nd pass.
New public APIs
@vue/reactivity
exposes readArray(source, deep = false)
.
This is an advanced reactivity function that returns the underlying raw array and tracks it entirely.
As this is a performance oriented API, an extra parameter indicates if deep reactive arrays return proxified array items (false by default). Of course, deep: true
comes at the cost of one array copy (vs zero).
When source
is not reactive, it's returned as-is and no tracking occurs.
Why expose this? Because it's still useful for performance-conscious users when they want to perform an operation not well covered by built-in array functions. Examples:
Example 1:
v-for
uses it :)
Example 2: consider writing matrix multiplication with reactive matrices (arrays). I tested a computed that performs 10x10 reactive matrices multiplication. UsingreadArray
gave me more than 3x boost and those are pretty small matrices!
Example 3:Object.groupBy
is a new API that could become quite popular. For compat. concern it is not an array method, so can't be instrumented. Unless we patch the browser APIs,readArray
is the only way to optimize it in user-land.
Open points
- Instrumented functions are only returned for non-readonly proxies. This is detrimental to this PR and I'm not sure why it was done. Current instrumentations fall in 2 categories:
- Avoid nested triggering in some mutations. Mutations fail on readonly proxies anyway, so does not really matter.
- Searching for un-proxified values in
indexOf
and co. This seems useful on readonly arrays too?
- I have kept
v-for
very close to its current source code, sticking to thefor (i = 0..)
loop. UsingreadArray(source, true)
implies a full array copy on deeply reactive arrays. I think it'd be better to usemap
or an iterator (also instrumented for perf, requires no O(N) allocation) or maybereadArray(source, false)
and calltoReactive
inside the loop if required (more code). - I mentioned the new
Object.groupBy
API above. It's unfortunate it's not an array instance method. Do we want to patchObject
? It feels really bad to patch native objects, but this is an API I can imagine being used insidecomputed
a lot in the future... so it'd be nice to have the best perf by default. Maybe let users opt into that?
Noteworthy implementation details
- There's a comment in
iterator()
about the timing of tracking that is worth reading. - Some method can early exit, such as
find
,some
orevery
. After this PR they will track the full array and may trigger when not strictly necessary.
This is not totally new, the existing instrumentations ofindexOf
and co. did exactly this already.
Might be "fixed" if we introduce range dependencies (see below).
Further ideas
If this PR is merged, some ideas:
- A similar optimization could be made for mutations/triggers.
- Might be a big change because the reactivity structures may not support it as is: introduce tracked ranges? That could solve the imprecise tracking of
indexOf
,lastIndexOf
,find
,findLast
,findIndex
,findLastIndex
,some
,every
,includes
,slice
(not instrumented in this PR).