docs icon indicating copy to clipboard operation
docs copied to clipboard

[Enhancement]: Add "Large Mutable Structures" section to /guide/best-practices/performance.html?

Open felix-pb opened this issue 2 years ago • 0 comments

On the page about performance best practices, there's a section named Reduce Reactivity Overhead for Large Immutable Structures, which I found very enlightening. For example, if a Vue app fetches a long list of immutable objects used to populate a read-only table, then it makes sense to wrap that list in a shallowRef().

However, after reading the entire guide, I'm not sure what's the best way to handle large mutable state, specifically in terms of performance. For example, if I want to use a store to manage a lot of shared mutable state, which of the following 3 options is better strictly in terms of performance? (i.e. not in terms of code organization/code quality). Note that for simplicity, the state is not that big in the examples below, but image it contained 1000+ properties instead.

Option 1: Use a large and deeply-nested state object wrapped in a single ref()/reactive() instance:

// store.js
import { reactive } from "vue";

export const state = reactive({
    user: {
        name: "Alice",
        email: "",
        age: 0,
        subscribed: false,
    },
    // many other properties, and some of them could be nested 10+ levels
});
// App.vue
<script setup>
import { computed } from "vue";
import { state } from "./store.js";
const text = computed(() => state.user.subscribed ? "Unsubscribe" : "Subscribe");
const toggle = () => state.user.subscribed = !state.user.subscribed;
</script>

<template>
    <h1>Hello, {{ state.user.name }}!</h1>
    <button @click="toggle()">{{ text }}</button>
</template>

Option 2: Split the large state object into many shallow ref()/reactive() instances:

// store.js
import { ref } from "vue";

export const userName = ref("Alice");
export const userEmail = ref("");
export const userAge = ref(0);
export const userSubscribed = ref(false);
// many other refs, but all of them contain primitive types
// App.vue
<script setup>
import { computed } from "vue";
import { userName, userSubscribed } from "./store.js";
const text = computed(() => userSubscribed.value ? "Unsubscribe" : "Subscribe");
const toggle = () => userSubscribed.value = !userSubscribed.value;
</script>

<template>
    <h1>Hello, {{ userName }}!</h1>
    <button @click="toggle()">{{ text }}</button>
</template>

Option 3: Use a large but flat state object wrapped in a single ref()/reactive() instance:

// store.js
import { reactive } from "vue";

export const state = reactive({
    userName: "Alice",
    userEmail: "",
    userAge: 0,
    userSubscribed: false,
    // many other properties, but all of them contain primitive types (i.e. not nested)
});
// App.vue
<script setup>
import { computed } from "vue";
import { state } from "./store.js";
const text = computed(() => state.userSubscribed ? "Unsubscribe" : "Subscribe");
const toggle = () => state.userSubscribed = !state.userSubscribed;
</script>

<template>
    <h1>Hello, {{ state.userName }}!</h1>
    <button @click="toggle()">{{ text }}</button>
</template>

For option 1, my concern is that accessing or mutating very nested properties might cause more "reactivity overhead". For example, does the access of state.user.name and the mutation of store.user.subscribed in option 1 cause more overhead than the access of state.userName and the mutation of state.userSubscribed in option 3. Of course, in this case, the user name in option 1 is not that much more nested, but what if it was state.a.b.c.d.e.user.name instead, and we had hundreds or thousands of similar accesses/mutations across all the components in our app? Perhaps, there is zero overhead but it seems unclear from the documentation.

For option 2, my concern is that using many instances of ref()/reactive() could consume more memory and therefore could slow down performance for a large enough app. That said, I have a feeling that even when using a single ref()/reactive() instance as in options 1 and 3, Vue will recursively create more instances for child properties behind-the-scenes in order to provide "deep reactivity". Unfortunately, the simple implementation of ref() and reactive() in Reactivity in Depth doesn't handle deep reactivity, so it's unclear to me how the real Vue implementation would do it.

In short, code organization aside and strictly based on performance, is there one of these options that is better? I'm not sure if it's something that many Vue developers are struggling to understand (or even curious to know), so it might not warrant an addition to the performance page. You would know best, and thanks for your time!

felix-pb avatar Apr 04 '23 19:04 felix-pb