Vuex: Very slow performance when iterating over array in Vuex state

Created on 27 Jul 2017  路  5Comments  路  Source: vuejs/vuex

Version

2.3.0

Reproduction link

https://codepen.io/anon/pen/YxXMXb?editors=1111

Steps to reproduce

The store state has two values selectedItems and allCategories. A recursive category template contains a checkbox that is bound to a computed property isSelected, that just checks if a string (passed as template property name) is contained in the selectedItems. Clicking a checkbox is extremely slow. Is it possible to improve the performance?

What is expected?

Clicking the checkbox will instantly toggle the checkmark.

What is actually happening?

It takes over a second for the checkmark to appear.

Most helpful comment

@kpasko Behavior of observable array assumes that any call on changing the array (not the values) triggers notification on subscribers. In this case, there are 2 calls to .push and .splice methods in mutation: both of them change the array structure. While every component has computed property (isSelected) subscribed on the array (it cannot be subscribed on indexOf result), all of the components are being triggered to re-render.

In this case the solutions is to refactor the state to avoid nested data sets: make an array of IDs, which would represent ordering and a flat map of categories, keyed by theirs IDs. Categories themselves should contain references to children with IDs instead of including them. (See https://github.com/paularmstrong/normalizr)
Second to create separate isSelected value for each category in state and implement selectedItems as a getter instead.

// _ = require('lodash')

const state = {
     order: ['1', '2', '5', '7', /* ... */, '999'],
     items: {
          '1': { title: 'Food and drink', isSelected: false, children: ['2', '5'] },
          '2': { title: 'Food', isSelected: false, children: ['7'] },
          '5': { title: 'Bread and cereals', isSelected: false, children: [] },
          /* ... */
     },
}

const getItems = (state, ids) => {
    return _.map(state.order, (id) => {
         let item = state.items[id]
         return _.assign({}, item, {
              children: item.children.length > 0 ? getItems(state, item.children) : [],
         })
    })
}

const getters = {
     itemsList (state) {
          return getItems(state, state.order)
     },
     selectedItems (state) {
          return _.filter(state.order, (id) => state.items[id].isSelected)
     },
}

const mutations = {
    toggleItem (state, id) {
         state.items[id].isSelected = !state.items[id].isSelected
    },
}

All 5 comments

Note, I've also tried to bind the checkbox directly to the store state via <checkbox v-model="$store.state.selectedItems" but the performance is about the same ...

At first, you should specify what is the performance bottleneck. In your example, the bottleneck is that the app rerender entire DOM tree in every time when the checkbox is toggled. Rerendering >=1000 DOM nodes should be avoided.

Second, you should consider performant data structure since the current data structure can cause the performance drawback.

I tweak your example just as an example for improving response time of checkbox. https://codepen.io/ktsn/pen/rzVgbO?editors=1011
You may want to learn the reactivity system of Vue.js https://vuejs.org/v2/guide/reactivity.html

I'm closing this issue since I don't think we can improve the performance when the app renders such large amount of DOM nodes. The users should consider to eliminate a bottleneck at first.

@ktsn, can you explain why it is that the original version updates the entire DOM tree every click? it is not immediately apparent to me from looking at the two codepens. thanks!

@kpasko Behavior of observable array assumes that any call on changing the array (not the values) triggers notification on subscribers. In this case, there are 2 calls to .push and .splice methods in mutation: both of them change the array structure. While every component has computed property (isSelected) subscribed on the array (it cannot be subscribed on indexOf result), all of the components are being triggered to re-render.

In this case the solutions is to refactor the state to avoid nested data sets: make an array of IDs, which would represent ordering and a flat map of categories, keyed by theirs IDs. Categories themselves should contain references to children with IDs instead of including them. (See https://github.com/paularmstrong/normalizr)
Second to create separate isSelected value for each category in state and implement selectedItems as a getter instead.

// _ = require('lodash')

const state = {
     order: ['1', '2', '5', '7', /* ... */, '999'],
     items: {
          '1': { title: 'Food and drink', isSelected: false, children: ['2', '5'] },
          '2': { title: 'Food', isSelected: false, children: ['7'] },
          '5': { title: 'Bread and cereals', isSelected: false, children: [] },
          /* ... */
     },
}

const getItems = (state, ids) => {
    return _.map(state.order, (id) => {
         let item = state.items[id]
         return _.assign({}, item, {
              children: item.children.length > 0 ? getItems(state, item.children) : [],
         })
    })
}

const getters = {
     itemsList (state) {
          return getItems(state, state.order)
     },
     selectedItems (state) {
          return _.filter(state.order, (id) => state.items[id].isSelected)
     },
}

const mutations = {
    toggleItem (state, id) {
         state.items[id].isSelected = !state.items[id].isSelected
    },
}

Dear @rkgrep @katashin
I have a similar experience where I have about 1000 items in an array.

  • The array is a series of ID's in a certain order:
    idsArray = [8, 20, 322, 5]
  • These id's are equivalent to items in an object:
    itemsLookup = { '8': {itemInfo: ...}, '20': {itemInfo: ...} }

Currently I'm doing a map() on the idsArray to show each item in a long list. I have some action that allows an item to be selected and then to be moved up/down with the keyboard.

Moving is currently very slow: splice the id out and splice it back in on a different index.
I think because the whole list has to be re-rendered since the array has changed.

Is there any other way I could do this that gives better performance if my users need to have an ordered list of 1000 items or so?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Ge-yuan-jun picture Ge-yuan-jun  路  3Comments

weepy picture weepy  路  3Comments

gongzza picture gongzza  路  3Comments

james-wasson picture james-wasson  路  3Comments

visualjerk picture visualjerk  路  3Comments