Changing state in vuex store is slow

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.

    – 

Leave a Comment