Improving vue application performance

We have a wiki at TeamHood. There is a collection of recommendations, including how to improve the performance of a heavy frontend on vue.js. It was necessary to improve performance, because due to the specifics, our main screens do not have pagination. There are clients who have more than a thousand of these cards on one kanban / gantt board, all this should work without lags.





wiki, .





. vue2 , vue3. vue3 production-ready. vuex4 , ( , vue2+vuex3). javascript, vue-class-component, typescript, typed-vuex , .





1. (Deep) Object Watchers

- deep , watch . . items , vuex store, , item . isChecked , item, getter, :





export const state = () => ({
  items: [{ id: 1, name: 'First' }, { id: 2, name: 'Second' }],
  checkedItemIds: [1, 2]
})

export const getters = {
  extendedItems (state) {
    return state.items.map(item => ({
      ...item,
      isChecked: state.checkedItemIds.includes(item.id)
    }))
  }
}
      
      



, items , . - :





export default class ItemList extends Vue {
  computed: {
    extendedItems () { return this.$store.getters.extendedItems },
    itemIds () { return this.extendedItems.map(item => item.id) }
  },
  watch: {
    itemIds () {
      console.log('Saving new items order...', this.itemIds) 
    }
  }
}
      
      



item . - , . checkedItemIds extendedItems ( ), itemIds. -, , . , javascript, [1,2,3] != [1,2,3]. : example1.





- watcher . watcher computed . , {id, title, userId}



items, :





computed: {
  itemsTrigger () { 
    return JSON.stringify(items.map(item => ({ 
      id: item.id, 
      title: item.title, 
      userId: item.userId 
    }))) 
  }
},
watch: {
  itemsTrigger () {
    //    JSON.parse -    this.items; 
  }
}
      
      



, watcher, , .

watcher - , deep watcher - . deep - . , , - , - deep - .





- .. ( ).. , , , $emit('reinit'), $nextTick. .





2. Object.freeze

Object.freeze TeamHood 2 . , StarBright, nuxt . Nuxt , . vuex store ( ). , vuex. this.$store.dispatch('fetch', …), vuex .





, vuex . , , autocomplete , store . , vue (). , .





// 
state: () => ({
  items: []
}),
mutations: {
  setItems (state, items) {
    state.items = items
  },
  markItemCompleted (state, itemId) {
    const item = state.items.find(item => item.id === itemId)
    if (item) {
      item.completed = true
    }
  }
}

// 
state: () => ({
  items: []
}),
mutations: {
  setItems (state, items) {
    state.items = items.map(item => Object.freeze(item))
  },
  markItemCompleted (state, itemId) {
    const itemIndex = state.items.find(item => item.id === itemId)
    if (itemIndex !== -1) {
      //    item.completed = true ( ),    ;
      const newItem = {
        ...state.items[itemIndex],
        completed: true
      }
      state.items.splice(itemIndex, 1, Object.freeze(newItem))
    }
  }
}
      
      



: example2. , build- ( development).





3.

. . items.find :





// Vuex: 
getters: {
  itemById: (state) => (itemId) => state.items.find(item => item.id === itemId)
}
...
// Some <Item :item-id="itemId" /> component:
computed: {
  item () { return this.$store.getters.itemById(this.itemId) }
}
      
      



itemsByIds :





getters: {
  itemByIds: (state) => state.items.reduce((out, item) => {
    out[item.id] = item
    return out
  }, {})
}
// Some <Item :item-id="itemId" /> component:
computed: {
  item () { return this.$store.getters.itemsByIds[this.itemId] }
}
      
      



: example3.





4.

- vue. (shouldComponentUpdate) . - : - , .





, , - , , , . () :





// Store:
export const getters = {
  extendedItems (state) {
    return state.items.map(item => ({
      ...item,
      isChecked: state.checkedItemIds.includes(item.id)
    }))
  },
  extendedItemsByIds (state, getters) {
    return getters.extendedItems.reduce((out, extendedItem) => {
      out[extendedItem.id] = extendedItem
      return out
    }, {})
  }
}

// App.vue:
<ItemById for="id in $store.state.ids" :key="id" :item-id="id />

// Item.vue:
<template>
  <div>{{ item.title }}</div>
</template>

<script>
export default {
  props: ['itemId'],
  computed: {
    item () { return this.$store.getters.extendedItemsByIds[this.itemId] }
  },
  updated () {
    console.count('Item updated')
  }
}
</script>
      
      



: example4p1. item <Item>. , <Item> extendedItemsByIds, item.





vue - , virtual DOM (memoization). - - dry run props $store. - , .





store . normalizr , . ids. , getter, . , :





// Store:
export const state = () => ({
  ids: [],
  itemsByIds: {},
  checkedIds: []
})

export const getters = {
  extendedItems (state, getters) {
    return state.ids.map(id => ({
      id,
      item: state.itemsByIds[id],
      isChecked: state.checkedIds.includes(id)
    }))
  }
}

export const mutations = {
  renameItem (state, { id, title }) {
    const item = state.itemsByIds[id]
    if (item) {
      state.itemsByIds[id] = Object.freeze({
        ...item,
        title
      })
    }
  },
  setCheckedItemById (state, { id, isChecked }) {
    const index = state.checkedIds.indexOf(id)
    if (isChecked && index === -1) {
      state.checkedIds.push(id)
    } else if (!isChecked && index !== -1) {
      state.checkedIds.splice(index, 1)
    }
  }
}

// Item.vue:
computed: {
  item () {
    return this.$store.state.itemsByIds[this.itemId]
  },
  isChecked () {
    return this.$store.state.checkedIds.includes(this.itemId)
  }
}
      
      



, renameItem state.itemsByIds, . rename : example4p2. isChecked state.checkedIds ( ), - <Item>.





, <Item> :





<Item
  v-for="extendedItem in extendedItems"
  :key="extendedItem.id"
  :item="extendedItem.item"
  :is-checked="extendedItem.isChecked"
/>
      
      



: example4p3.





5. IntersectionObserver

DOM- . . , gantt , , viewport. . , intersection observer. vuetify v-intersect , , IntersectionObserver , , .





, : example5. 100 ( 10), , . IntersectionObserver , ., - IntersectionObserver:





export default {
  inserted (el, { value: observer }) {
    if (observer instanceof IntersectionObserver) {
      observer.observe(el)
    }
    el._intersectionObserver = observer
  },
  update (el, { value: newObserver }) {
    const oldObserver = el._intersectionObserver
    const isOldObserver = oldObserver instanceof IntersectionObserver
    const isNewObserver = newObserver instanceof IntersectionObserver
    if (!isOldObserver && !isNewObserver) || (isOldObserver && (oldObserver === newObserver)) {
      return false
    }
    if (isOldObserver) {
      oldObserver.unobserve(el)
      el._intersectionObserver = undefined
    }
    if (isNewObserver) {
      newObserver.observe(el)
      el._intersectionObserver = newObserver
    }
  },
  unbind (el) {
    if (el._intersectionObserver instanceof IntersectionObserver) {
      el._intersectionObserver.unobserve(el)
    }
    el._intersectionObserver = undefined
  }
}
      
      



, , , . , , - vue , . , . - . , css:





<template>
  <div 
    v-for="i in 100" 
    :key="i" 
    v-node-intersect="intersectionObserver"
    class="rr-intersectionable"
  >
    <Heavy />
  </div>
</template>

<script>
export default {
  data () {
    return {
      intersectionObserver: new IntersectionObserver(this.handleIntersections)
    }
  },
  methods: {
    handleIntersections (entries) {
      entries.forEach((entry) => {
        const className = 'rr-intersectionable--invisible'
        if (entry.isIntersecting) {
          entry.target.classList.remove(className)
        } else {
          entry.target.classList.add(className)
        }
      })
    }
  }
}
</script>

<style>
.rr-intersectionable--invisible .rr-heavy-part
  display: none
</style>
      
      






All Articles