0

I've got a problem with my VueJS and Vuetify project. I wanna create a table with expandable rows. It'll be a table of orders with possibility to see bought products for each one. For one page it should show at least 100 rows of orders. For this, I used <v-data-table> from the Vuetify framework.


What is the problem?

After preparing everything I realized that it works, but for expansion for each row, I have to wait a few seconds (it's too long - it must be a fast system). And for expanding all visible records it is necessary to wait more than 20 seconds with whole page lag.


What I've tried?

I started with standard Vuetify <v-data-table> with a show-expand prop and with an expanded-item slot - it was my first try - the slowliest. Secondly, I tried to create on my own - but with Vuetify:

<v-data-table>
 <template v-slot:item="{item}">
   <td @click="item.expanded = !item.expanded">expand / hide</td>
   <!--- [my table content here - too long to post it here] -->
   <tr v-if="item.expanded">
     <td :colspan="headers.length">
     <v-data-table>
       <!--- [content of the nested table - also too long to post it here] -->
     </v-data-table>
   </tr>
 </template>
</v-data-table>

What's interesting - I realized that v-if works faster than v-show, which is a weird fact, because I thought that changing display: none to nothing should be less problematic than adding/removing whole objects to DOM.

This method was a little faster than first, but it is still too slow.

I found a hint to set :ripple="false" for every v-btn in my tables and I did it - helped, but only a bit. Everything was tested on Chrome and Firefox, on three devices with Windows and Linux Fedora and two android smartphones.


What should else I do?

Thank you in advance!

Community
  • 1
  • 1
Simon Jan
  • 22
  • 1
  • 6
  • What do the data items look like? Are you lazy loading them upon item expanded? – Carol Skelly Apr 27 '20 at 14:45
  • In mounted part of VueJS I run Vuex action, which gets orders with transactions from database at once, so when order is expanded, transactions are loading from state – Simon Jan Apr 27 '20 at 14:52
  • Really hard to say w/o seeing the actual data structure. Are the item objects large, complex or heavily nested? Can you simplify the initial db structure that is returned and then lazy load item details on demand as the rows are expanded? – Carol Skelly Apr 27 '20 at 15:24
  • Okay, it's one order with all data: [pastebin](https://pastebin.com/qZTW3Ucu). In my orders array should be at least 100 rows like this. – Simon Jan Apr 27 '20 at 16:01
  • I tried with pure js onClick="" event - simple function add / remove `display: none` css property. It works in no time at all. But how implement it in VueJS? – Simon Jan Apr 27 '20 at 16:35

1 Answers1

1

This excellent article suggests that the raw number of DOM nodes has the biggest impact on performance. That said, I didn't experience any real performance bottlenecks in the sample app that I built to learn more about your problem. The entire page with the table loaded in about 1.25s (from localhost), regardless of whether it was in dev mode or it was a production build. The JavaScript console timer reported that expanding or contracting ALL 100 rows simultaneously only took an average of about 0.3s. Bottom line, I think you can achieve the optimizations you're looking for and not have to give up the conveniences of Vuetify.

Recommendations

  1. Consider displaying fewer rows at one time (biggest expected impact)
  2. Streamline your template to use as few elements as possible, only display data that's really necessary to users. Do you really need a v-data-table inside a v-data-table?
  3. Streamline your data model and only retrieve the bare minimum data you need to display the table. As @Codeply-er suggested, the size and complexity of your data could be causing this strain

Testing Method

Here's what I did. I created a simple Vue/Vuetify app with a VDataTable with 100 expandable rows. (The data was pulled from the random user API). I used this method to count DOM nodes. Here are some of the parameters/info:

  • Rows: 100
  • Columns: 5 + the expansion toggler
  • Expansion row content: a VSimpleTable with the user's picture and address
  • Size of a single JSON record returned from the API: ~62 lines (about half the size of your sample object above)
  • Vue v2.6.11
  • Vuetify v2.3.0-beta.0
    (I realize this just came out, but I don't think you'd have different results using v2.2.x)
  • App was built with vue create myapp and vue add vuetify
  • VDataTable actually adds/removes the expansion rows from the DOM whenever the rows are expanded/contracted

Here's some approximate stats on the result (these numbers fluctuated slightly in different conditions--YMMV):

  • 773 (~7/row): number of DOM nodes in 100 rows/5 columns without expansion enabled
  • 977 (+2/row): number of nodes with expansion enabled
  • 24: number of nodes added to the table by expanding a single row
  • 3378 (+26/row): total nodes with ALL rows expanded
  • ~1.25s to load the entire page on a hard refresh
  • ~0.3s to expand or contract ALL of the nodes simultaneously
  • Sorting the columns with the built-in sorting tools was fast and very usable

Here's code of the App.vue page of my app. The v-data-table almost the only component on the page (except the toggle button) and I didn't import any external components.

<template>
  <v-app>
    <v-btn
      color="primary"
      @click="toggleExpansion"
    >
      Toggle Expand All
    </v-btn>
    <v-data-table
      :expanded.sync="expanded"
      :headers="headers"
      :items="items"
      item-key="login.uuid"
      :items-per-page="100"
      show-expand
    >
      <template #item.name="{ value: name }">
        {{ name.first }} {{ name.last }}
      </template>
      <template #expanded-item="{ headers, item: person }">
        <td :colspan="headers.length">
          <v-card
            class="ma-2"
            max-width="500px"
          >
            <v-row>
              <v-col cols="4">
                <v-img
                  :aspect-ratio="1"
                  contain
                  :src="person.picture.thumbnail"
                />
              </v-col>
              <v-col cols="8">
                <v-simple-table>
                  <template #default>
                    <tbody>
                      <tr>
                        <th>Name</th>
                        <td class="text-capitalize">
                          {{ person.name.title }}. {{ person.name.first }} {{ person.name.last }}
                        </td>
                      </tr>
                      <tr>
                        <th>Address</th>
                        <td class="text-capitalize">
                          {{ person.location.street.number }} {{ person.location.street.name }}<br>
                          {{ person.location.city }}, {{ person.location.state }} {{ person.location.postcode }}
                        </td>
                      </tr>
                      <tr>
                        <th>DOB</th>
                        <td>
                          {{ (new Date(person.dob.date)).toLocaleDateString() }} (age {{ person.dob.age }})
                        </td>
                      </tr>
                    </tbody>
                  </template>
                </v-simple-table>
              </v-col>
            </v-row>
          </v-card>
        </td>
      </template>
    </v-data-table>
  </v-app>
</template>

<script>
  import axios from 'axios'
  export default {
    name: 'App',
    data: () => ({
      expanded: [],
      headers: [
        { text: 'Name', value: 'name' },
        { text: 'Gender', value: 'gender' },
        { text: 'Phone', value: 'phone' },
        { text: 'Cell', value: 'cell' },
        { text: 'Country', value: 'nat' },
        { text: '', value: 'data-table-expand' },
      ],
      items: [],
    }),
    created () {
      axios.get('https://randomuser.me/api/?seed=stackoverflow&results=100')
        .then(response => {
          this.items = response.data.results
        })
    },
    methods: {
      toggleExpansion () {
        console.time('expansion toggle')
        this.expanded = this.expanded.length ? [] : this.items
        console.timeEnd('expansion toggle')
      },
    },
  }
</script>

You can see a working demo in this codeply. Hope this helps!

morphatic
  • 7,677
  • 4
  • 47
  • 61
  • Thank you so much! I streamlined my code, simplified data model, and replaced some Vuetify components with my own - more light than original and now it works better. It wasn't difficult to devise but to be honest, I hoped for a simpler and less laborious solution ;), and your answer proved that it's the only sensible option. Thanks! – Simon Jan Apr 28 '20 at 13:48