Vue.js - Takes too long to 'destroy' components Vue.js - Takes too long to 'destroy' components vue.js vue.js

Vue.js - Takes too long to 'destroy' components


We have experienced similar issues and found that all share the same underlying problem: too many components that depend on the same reactive object. These are the 3 main cases that may impact any project:

  • Many router-link components
  • Many components (any kind) when Vue I18n is installed
  • Many components that directly access the Vuex store on its render or computed properties.

Our approach is to avoid accessing shared reactive objects on the render and computed properties functions. Instead, pass them as props (reactive) or access them on the created or updated hooks (not reactive) to store in the component's $data. Read below for more details and each of the 3 cases.

A brief explanation of Vue 2 reactivity

(skip this if you don't need it)

The Vue reactivity basically relays on two intertwined objects: Watcher and Dep. Watchers have a list of dependencies (Deps) in the deps attribute, and Deps have a list of dependants (Watchers) in the subs attribute.

For every reactive thing, Vue instantiates a Dep that tracks reads and writes on it.

Vue instantiates a Watcher for every component (actually, for the render function) and every Computed Property. The Watchers watch a function during its execution. While watching, if a reactive object is read, the associated Dep notices the Watcher, and they become related: The Watcher.deps contains the Dep, and the Dep.subs contains the Watcher.

Afterwards, if the reactive thing changes, the associated Dep notifies all its dependants (Dep.subs) and tells them to update (Watcher.update).

When a component is destroyed, all its Watchers are destroyed as well. This process implies iterating each Watcher.deps to remove the Watcher itself from the Dep.subs (see Watcher.teardown).

The problem

All the components that depend on the same reactive thing insert a Watcher on the same Dep.subs. In the following example, the same Dep.subs contains 10,000 watchers:

  • 1,000 items rendered (e.g. a grid, an infinite scroll, ...)
  • Each item implies 10 components: itself, 2 router-link, 3 buttons, 4 other (nested and not nested, from your code or third party).
  • All components depend on the same reactive object.

When destroying the page, the 10,000 watchers will remove themselves from the Dep.subs array (one by one). The cost of removing themselves is 10k * O(10k - i) where i is the number of watchers already removed.

In general, the cost of removing n items is O((n^2)/2).

Workarounds

In case you render many components, avoid accessing shared reactive dependencies on the render or computed properties.

Instead, pass them as props or access them on the created or updated hooks and store them on the component's $data. Bear in mind that the hooks aren't watched so the component won't be updated if the source of data changes, which is still suitable for many cases (any case where the data won't change once the component is mounted).

If your page renders a long list of items, the vue-virtual-scroller is bound to help. In this case, you can still access shared reactive dependencies because the vue-virtual-scroller reuses a small pool of your components (it does not render what is not seen).

Take into account that having thousands of component might be easier than you expect because we tend to write small components and compose them (actually a good practice)

Case: Vuex

If you do something like this in your render o computed property, your component depends on all the chain of reactive things: state, account, profile.

function myComputedProperty() {    this.$store.state.account.profile.name;}

In this example, if your account does not change once the component is mounted, you can read it from the created or beforeMount hook and store the name on the Vue $data. As this is not part of the render function nor part of a computed property, there is no Watcher watching the access to the store.

function beforeMount() {    this.$data.userName = this.$store.state.account.profile.name;}

Case: router-link

See the issue #3500

Case: Vue I18n

This has the same underlying problem but with a bit different explanation. See the issue #926


It's not a vue problem, see on your mixins/options.
Eg. i18n (my pain) in every of 200 components will show the same result. It removes a lot of watchers on beforeDestroy. Without i18n the list works 30 times faster.
How to fix it? Move the slow hook-handlers to the parent component and get needed data/methods from it.

The sample with i18n

Vue.mixin({    beforeCreate() {        if (this.$options.useParentLocalization) {            this._i18n = parent.$i18n;        }    },});

Usage:

new Vue({  // i18n, <-- before  useParentLocalization: true,  components: {    Component1  }})