2.2.6
https://github.com/andrewluetgers/vue2-perf-test
clone, npm install, npm run dev
in the ui click 100x100, click show, click run
frame rate should be above 100
frame rate in teens
compare to
https://github.com/andrewluetgers/vue-perf-test
exact same repo except uses vue 1 instead performs as expected at 100 fps
The key bit is in assets/src/components/AlphaTable.vue
<template>
<div id="alphaTable">
<ul v-for="row in layout" track-by="$index">
<li v-for="cell in row" track-by="$index" :style="{background: cell.active ? cell.color : '#444444'}">
{{cell.letter}}
</li>
</ul>
</div>
</template>
Is there some production mode switch that needs to be flipped or do I need to break the nested v-for loops into separate modules or something?
Would love to understand what is going on.
Please read the 2.0 docs. You are using the old track-by
which has been replaced by key
.
On a second look it's probably not related to track-by, I'll do some profiling to see what's going on...
look forward to see the answer
Ok, so there are a few things involved:
Your repo is using an old Vue 1.x config, using an outdated version of vue-loader
and aliasing Vue to a build hard-coded in dev mode... I'd suggest using vue-cli
to scaffold a fresh project and move the source code over. This doesn't affect the perf too much though.
The more fundamental difference is in the different render strategies and reactivity boundaries between Vue 1 and Vue 2.
In Vue 1, reactivity boundaries are per-binding - that is, every text or style
binding tracks its own dependencies. This has higher memory consumption cost but the advantage is the reactive updates are extremely granular by default: when a single cell's active
property is toggled, only that cell's style binding is re-evaluated and updated.
In Vue 2, reactivity boundaries are per-component. Therefore, the AlphaTable
component has a single watcher that keeps track of the dependencies used by every cell in the table. Every time a single cell changes, the entire component is re-rendered, incurring extra dependency collection, render and diffing costs.
This can be dealt with, however, by turning every cell into its own component. This way when a cell's state is updated, only the corresponding cell component is re-rendered. Making this change brings the FPS back to 110~120, as one would expect. The drawback is that this results in slower initial render due to the cost of creating backing component instances.
In fact, this is also the reason that your MobX + React test has high FPS because it is also using a component for each cell. If you use plain elements for each cell, you'd also see a significant FPS drop, down to just ~6 FPS.
It might seem that Vue 1's strategy is better in this particular case - but in practice, it is extremely rare for a single component to contain 10k plain elements. In general, we've found Vue 2's strategy to be a better tradeoff for the more common use cases.
@yyx990803 This makes a lot of sense! Thanks for the explanation, and yes, it is very common this days to have very small and granular components and having optimizations done on a per-component basis is just right.
@yyx990803 thanks for the detailed explanation, will update the project. As you point out this test is a very particular and not reflective of most apps, things like game engines, particle systems etc are what I'm thinking of. It is a reasonable tradeoff that was made for Vue 2. I was quite surprised by the performance of Vue 1 on this test. I see the decoupling of performance from component composition as a big win in the simplicity side of things. In my dream world there would be some way to opt back in to Vue 1 reactivity boundary but thats just me :-) THANKS!
@yyx990803 just curious - what tools are you using to profile Vue?
Dave
@davidm-public
probably chrome dev tools - performance
@yyx990803 @davidm-public it was tested with https://github.com/andrewluetgers/vue2-perf-test and https://github.com/andrewluetgers/vue-perf-test note the frame rate counter and yes chrome dev tools profile and flame graph are great tools
@andrewluetgers thanks
Most helpful comment
Ok, so there are a few things involved:
Your repo is using an old Vue 1.x config, using an outdated version of
vue-loader
and aliasing Vue to a build hard-coded in dev mode... I'd suggest usingvue-cli
to scaffold a fresh project and move the source code over. This doesn't affect the perf too much though.The more fundamental difference is in the different render strategies and reactivity boundaries between Vue 1 and Vue 2.
In Vue 1, reactivity boundaries are per-binding - that is, every text or
style
binding tracks its own dependencies. This has higher memory consumption cost but the advantage is the reactive updates are extremely granular by default: when a single cell'sactive
property is toggled, only that cell's style binding is re-evaluated and updated.In Vue 2, reactivity boundaries are per-component. Therefore, the
AlphaTable
component has a single watcher that keeps track of the dependencies used by every cell in the table. Every time a single cell changes, the entire component is re-rendered, incurring extra dependency collection, render and diffing costs.This can be dealt with, however, by turning every cell into its own component. This way when a cell's state is updated, only the corresponding cell component is re-rendered. Making this change brings the FPS back to 110~120, as one would expect. The drawback is that this results in slower initial render due to the cost of creating backing component instances.
In fact, this is also the reason that your MobX + React test has high FPS because it is also using a component for each cell. If you use plain elements for each cell, you'd also see a significant FPS drop, down to just ~6 FPS.
It might seem that Vue 1's strategy is better in this particular case - but in practice, it is extremely rare for a single component to contain 10k plain elements. In general, we've found Vue 2's strategy to be a better tradeoff for the more common use cases.