I have a vue single page app that accepts xml in a form of a string, parses it and creates clickable xml tree. Each node is it’s own component with a xml path property. I allow user to right-click a node on the xml tree and choose from a list of properties. Choosing one dispatches a mutation and the store states updates the chosen property with value
and path
. This choosing and saving takes several up to 1 second of delay/lag which I would like to get rid of.
I don’t know why this happens but I have a suspicion that since every xml node component has a computed property paths
and values
to know if it is already selected or not, every time I change values in store, every components refreshes causing a lot of performance overhead.
There are many copies of this component since every XML node creates one copy.
<template>
<span>
<span
class="attribute-value"
:id="path"
:class="selected ? 'selected' : ''"
@contextmenu.prevent="$refs.optionsMenu.open"
>{{ value }}</span
><vue-context ref="optionsMenu">
<li v-for="option in options" :key="option[0]">
<a href="#" @click.prevent="assignValue(option[0])">{{
option[1].label
}}</a>
</li>
</vue-context>
</span>
</template>
<script>
import { defineComponent } from '@vue/composition-api'
import VueContext from 'vue-context'
export default defineComponent({
name: 'XmlAttributeValue',
props: {
value: String,
path: String,
},
components: { VueContext },
mounted() {
for (const [key, value] of Object.entries(this.paths)) {
if (value == this.path) {
this.$store.dispatch('invoice/updateValue', {
name: key,
value: this.value,
})
this.$store.dispatch('invoice/updatePath', {
name: key,
value: this.path,
})
}
}
},
computed: {
options() {
console.log('options')
return Object.entries(this.$store.getters['invoice/values'])
.filter(([, value]) => value.value == '')
.filter(([key]) => key != 'invoices' && key != 'lineItems')
},
selected() {
console.log('selected')
return (
Object.entries(this.paths)
.map((value) => value[1])
.filter((path) => path == this.path).length > 0
)
},
paths() {
return this.$store.getters['invoice/paths']
},
formats() {
return this.$store.getters['invoice/formats']
},
idMapping() {
return this.$store.getters['invoice/idMapping']
},
},
methods: {
assignValue(key) {
this.$store.dispatch('invoice/updateValue', {
name: key,
value: this.value,
})
this.$store.dispatch('invoice/updatePath', {
name: key,
value: this.path,
})
this.$store.dispatch('invoice/save', {
idMapping: this.idMapping,
paths: this.paths,
formats: this.formats,
})
},
},
})
</script>
Vuex store:
import InvoiceAPI from "../api/invoice";
const SET_ID_MAPPING = "SET_ID_MAPPING";
const UPDATE_VALUE = "UPDATE_VALUE";
const UPDATE_PATH = "UPDATE_PATH";
const UPDATE_FORMAT = "UPDATE_FORMAT";
const SAVE = "SAVE";
const SAVE_SUCCESS = "SAVE_SUCCESS";
const SAVE_ERROR = "SAVE_ERROR";
const INIT = "INIT";
const INIT_SUCCESS = "INIT_SUCCESS";
const INIT_ERROR = "INIT_ERROR";
export default {
namespaced: true,
state: {
error: null,
isLoading: false,
isSaving: false,
idMapping: null,
fileContent: '',
formats: {
dateFormat: '',
decimalChar: null
},
values: {
invoices: {
label: 'Invoices',
value: ''
},
invoiceTypeCode: {
label: 'Invoice Type',
value: ''
},
invoiceNumber: {
label: 'Invoice Number',
value: ''
},
vatNumber: {
label: 'VAT Number',
value: ''
},
customerNumber: {
label: 'Customer Number',
value: ''
},
deliveryDate: {
label: 'Delivery Date',
value: ''
},
issueDate: {
label: 'Issue Date',
value: ''
},
dueDate: {
label: 'Due Date',
value: ''
},
currency: {
label: 'Currency',
value: ''
},
grossAmount: {
label: 'Gross Amount',
value: ''
},
netAmount: {
label: 'Net Amount',
value: ''
},
taxAmount: {
label: 'Tax Amount',
value: ''
},
lineItems: {
label: 'Line Items',
value: ''
},
lineItem_position: {
label: 'Position',
value: ''
},
lineItem_name: {
label: 'Name',
value: ''
},
lineItem_supplierNumber: {
label: 'Supplier Number',
value: ''
},
lineItem_quantity: {
label: 'Quantity',
value: ''
},
lineItem_unit: {
label: 'Unit',
value: ''
},
lineItem_unitPrice: {
label: 'Unit Price',
value: ''
},
lineItem_totalPrice: {
label: 'Total Price',
value: ''
},
lineItem_taxRate: {
label: 'Tax Rate',
value: ''
},
},
paths: {
invoices: '',
invoiceTypeCode: '',
invoiceNumber: '',
vatNumber: '',
customerNumber: '',
deliveryDate: '',
issueDate: '',
dueDate: '',
currency: '',
grossAmount: '',
netAmount: '',
taxAmount: '',
lineItems: '',
lineItem_position: '',
lineItem_name: '',
lineItem_supplierNumber: '',
lineItem_quantity: '',
lineItem_unit: '',
lineItem_unitPrice: '',
lineItem_totalPrice: '',
lineItem_taxRate: ''
}
},
getters: {
values(state) {
return state.values;
},
paths(state) {
return state.paths;
},
formats(state) {
return state.formats
},
idMapping(state) {
return state.idMapping
},
isLoading(state) {
return state.isLoading
},
isSaving(state) {
return state.isSaving
},
fileContent(state) {
return state.fileContent
},
error(state) {
return state.error
}
},
mutations: {
update(state, payload) {
state.values[payload.name] = payload.value;
state.paths[payload.name] = payload.path;
},
[UPDATE_VALUE](state, payload) {
state.values[payload.name].value = payload.value
},
[UPDATE_PATH](state, payload) {
state.paths[payload.name] = payload.value
},
[UPDATE_FORMAT](state, payload) {
state.formats[payload.name] = payload.value
},
[SAVE](state) {
state.isSaving = true;
state.error = null;
},
[SAVE_SUCCESS](state) {
state.isSaving = false;
state.error = null;
},
[SAVE_ERROR](state, error) {
state.isSaving = false;
state.error = error;
},
[SET_ID_MAPPING](state, payload) {
state.idMapping = payload.idMapping
},
[INIT](state) {
state.isLoading = true;
state.error = null;
},
[INIT_SUCCESS](state, payload) {
state.isLoading = false;
state.error = null;
state.fileContent = payload.fileContent
state.paths = payload.paths
state.formats = payload.formats
},
[INIT_ERROR](state, error) {
state.isLoading = false;
state.error = error;
},
},
actions: {
updateValue({commit}, payload) {
commit(UPDATE_VALUE, payload);
return null;
},
updatePath({commit}, payload) {
commit(UPDATE_PATH, payload);
return null;
},
updateFormat({commit}, payload) {
commit(UPDATE_FORMAT, payload);
return null;
},
setIdMapping({commit}, payload) {
commit(SET_ID_MAPPING, payload);
return null;
},
async save({commit}, data) {
commit(SAVE);
try {
let response = await InvoiceAPI.save(data);
commit(SAVE_SUCCESS, response.data);
return response.data;
} catch (error) {
commit(SAVE_ERROR, error);
return error;
}
},
async init({commit}, data) {
commit(INIT);
try {
let invoice = await InvoiceAPI.get(data);
if (invoice.data == "") {
throw "File was not recognized"
}
if (invoice.data.fileContent.slice(0, 5) != '<?xml') {
throw "File was not recognized"
}
commit(INIT_SUCCESS, invoice.data);
return invoice.data;
} catch (error) {
commit(INIT_ERROR, error);
return error;
}
}
},
};
Notice those console logs. When I click and the state updates, both of those trigger 14000+ lines of console output lines on a somewhat small xml file.
Please, provide stackoverflow.com/help/mcve and preferably a demo because performance is something that needs to be debugged. There should be no performance problems for state change, it’s the place where the state used that usually is a bottleneck
I have edited my question and provided more code. Sadly I can’t provide demo app.
You may want to reduce the amount of recomputations, keep as much common data inside a store or parent comp as possible, e.g. options() and partially selected().
.filter(...).length > 0
is suboptimal because it traverses the whole array unconditionally, use findIndex or better do this once in a parent.“console output lines” – this breaks the purity of the experiment, dev tools in general and specifically console output can cause a significant slowdown. After that you need to disable dom updates to check which part of the slowdown they are responsible for, use dummy watchers for the computeds instead.
The thing is, even if comment out all these filters, it still lags when clicked.
Show 1 more comment