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
perf(engine-core): avoid Array#splice
in mutation tracking
#4129
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added this benchmark for the heck of it. There is no improvement from this PR, since this PR targets reset()
which happens during component removal.
// (Avoiding splice here is a perf optimization, and the order doesn't matter.) | ||
const index = ArrayIndexOf.call(set, this); | ||
set[index] = set[setLength - 1]; | ||
ArrayPop.call(set); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a performance difference between set.pop()
and set.length = setLength - 1
? We use the former here, but the latter in the if
block.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good catch. We can actually refactor the code to simplify it, if we're always just pop
ing or setting set.length = length - 1
in every case.
I did a quick perf test, and there's basically no difference between set.length = length - 1
:
... and pop()
:
Although maybe pop()
is slightly faster? Either way I would prefer pop()
since it's more idiomatic.
I'm also planning to run this through our full benchmark suite tonight, just in case there is some weird edge case I missed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
||
before(() => { | ||
treeElement = createElement('benchmark-tree', { is: Tree }); | ||
// Not really 3k, but close enough: 5^5 = 3,125 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Making the file 55 characters longer to save 2 characters in the filename! 🚀
packages/@lwc/perf-benchmarks-components/src/benchmark/tree/tree.js
Outdated
Show resolved
Hide resolved
…ee.js Co-authored-by: Will Harney <62956339+wjhsf@users.noreply.github.com>
OK, running the full benchmark suite of this PR against master, it looks like it's basically pure improvement. (There's one 1ms regression that I'm not too concerned about.) |
Details
Instead of doing an expensive
Array#splice
in this perf-sensitive mutation tracking code, we canArray#pop
insteadI couldn't find an existing benchmark to repro the improvement in Chrome, so I wrote a new one. This new test builds a tree structure with recursive components. The reason it repros the improvement is that it has lots of component instances that are tracking mutations on a single getter (which is probably something that happens in the real world fairly frequently).
The improvement is 20-24%:
Here it is in a manual test to show the improvement to the
reset()
function:FWIW I got this idea from Svelte here.
(I assume that the reason the splice is slow is because it has to create an additional array under the hood, plus it has to shift a bunch of array items around.)
Does this pull request introduce a breaking change?
Does this pull request introduce an observable change?
This is not an observable change because all the pending rehydration callbacks are queued up in a microtask, and then we sort the queue before running them:
lwc/packages/@lwc/engine-core/src/framework/vm.ts
Line 667 in 6c46adb
GUS work item