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.
// 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>