VueJS, Vuetify, data-table - expandable, performance problem - javascript

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!

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
Consider displaying fewer rows at one time (biggest expected impact)
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?
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!

Related

Vue dynamic components not re rendering inside v-for loop when data changes

I have dynamic table component where I can modify columns (ordering, adding new, removing columns). Table body looks like this:
<tr v-for="row in data" :key="row.id">
<td v-for="column in columns" :key="column.slug">
<component
v-if="column.component"
:is="column.component"
v-bind="{ row: row, countries: row.reach }"
/>
<template v-else>
{{ row[column.slug] }}
</template>
</td>
</tr>
Everything works well except when I edit data (add new column or rearrange the order) then every component inside table body disappears and is not rendered. I tried to attach unique :key to <component> but was not able to make it work. When I inspect table body in Vue devtools I can see that components are inside just not rendered into DOM. When inspecting table body in chrome devtools I can see only <!--function(e,n,r,o){return dn(t,e,n,r,o,!0)}--> in place where component should be rendered. Any ideas what I might be doing wrong here?
You are using duplicated keys. You should provide unique keys for all v-for elements inside the whole component. Try this (see key for every column):
<tr v-for="row in data" :key="row.id">
<td v-for="column in columns" :key="`${row.id}/${column.slug}`">
<component
v-if="column.component"
:is="column.component"
v-bind="{ row: row, countries: row.reach }"
/>
<template v-else>
{{ row[column.slug] }}
</template>
</td>
</tr>
Also when you are in development it is highly recommended to use Vue development mode. It will highlight errors like this.
Turns out I was passing Component object into :is directive instead of string with Component name. That's why it was not rendered properly.

How to add tooltip to datatable header in vuetify?

In older version on vuetify you could access headerCell slot and easily add tooltips - see https://codepen.io/nueko/pen/dZoXeZ
In the latest version you have named slots, so you need to know header name before
<template v-slot:header.givenname="{ header }">
Is there a way to add a tooltip to all headers?
There are 2 ways to achieve this.
Option 1: Customizing whole table row
If you need to customize whole row element inside table heading this might be useful. Even though I would not recommend to follow this way if you don't want to loose sorting functionality which exist by default in v-data-table.
Example:
<template v-slot:header="{ props: { headers } }">
<thead>
<tr>
<th v-for="h in headers">
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<span v-on="on">{{h.text}}</span>
</template>
<span>{{h.text}}</span>
</v-tooltip>
</th>
</tr>
</thead>
</template>
Working pen: https://codepen.io/onelly/pen/QWWmpZN
Option 2: Customizing each heading without losing sorting functionality
I guess this is more like what you are trying to do and the replacement for the old way. You can loop <template v-slot:header> and use Dynamic Slot Names to accomplish this. Syntax for Dynamic slot name looks like this <template v-slot:[dynamicSlotName]>.
Example:
<template v-for="h in headers" v-slot:[`header.${h.value}`]="{ header }">
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<span v-on="on">{{h.text}}</span>
</template>
<span>{{h.text}}</span>
</v-tooltip>
</template>
Working pen: https://codepen.io/onelly/pen/ExxEmWd

How to access total items outside v-data-table in vuetify.js 2?

I have just migrated vuetify.js from 1.5 to 2.1.7. Previously I was able to access total no. of items (reactive) in the data-table outside of v-data-table using :pagination.sync="pagination" props and accessing with pagination.totalItems.
Vuetify 1.5
<template>
<div>
<span>Total: {{ pagination.totalItems }}</span>
<v-data-table :pagination.sync="pagination" :items="items">
...
</v-data-table>
</div>
</template>
<script>
export default {
name: "LoadTable",
data() {
return {
items: [...],
pagination: {}
};
}
}
</script>
But in version 2.x, pagination prop has been removed and replaced with options instead with breaking changes and couldn't able to access the total no. of items. Is there any way in version 2 to access total items which sync with search and filter?
<v-data-table> has an event named pagination. You can use the event to update your current pagination property unless you want to override pagination options.
Try to use it in this way:
<v-data-table :items="items" #pagination="pagination = $event">
This will synchronize pagination property each time there is a change on pagination options.
// Pagination object
{
page: number
itemsPerPage: number
pageStart: number
pageStop: number
pageCount: number
itemsLength: number
}
You can find more information here under Events tab.

How to enforce render table dynamically

I'm using bootstrap-vue b-table and b-pagination component. But since there two many records, so I'm using pagination call to backed, which means pass the paging information to the backend, only fetch corresponding records for each page.
Good thing:
1. Data are passing correctly between backend and frontend.
2. Debug the frontend page through VUE debug tool, the properties of the b-table component are correct.
The problem:
The table is not rendering, it looks blank in the page, if I check the elements through chrome debugging tool. The elements (table body) for table container is empty.
A brief sample code :
<b-container>
<div v-if="guards_for_asyc_fetch_data_call">
<b-row><b-col><h2>Title</h2></b-col></b-row>
<b-row>
<b-col>
<b-table
:items="items"
:fields="fields"
:current-page="currentPage"
:per-page="perPage"
:stacked="true"
small
striped
fixed
hover/>
</b-col>
</b-row>
<b-row>
<b-col>
<b-pagination
:total-rows="totalRows"
:per-page="perPage"
:number-of-pages="totalPages"
v-model="currentPage"
align="center"
/>
</b-col>
</b-row>
</div>
</b-container>
</template>
<script>
export default {
computed: {
totalRows() {
return {dynamic_udpate};
},
perPage() {
return {dynamic_udpate};
},
currentPage: {
get: function() {
return {dynamic_udpate};
},
set: function (newPage) {
{asyc_call_fetch_data_from_backend}
}},
totalPages() {
return {dynamic_udpate};
},
items() {
return {dynamic_udpate};
}
}
}
</script>
Expectation:
Table content is updated dynamically when the data is refreshed after the backend call.
Actual result is:
When I click the page navigation except the 1st page, the container just show blank.
For vue debugger you can see the elements are the new fetched elements, id started from 6 since I set to present 5 items per page. Here's a link!
If I go check the general page elements in Chrome debugger, the table content is empty. Here's a link!
Anyone gets an idea how I can solve this problem?

Reusing small bits of markup inside a component in a vue file

I am just starting to learn Vue and while doing a very basic template I found myself repeating small bits of HTML / components in the same template because of a v-if and different containing components depending on its result.
Though I have solved this repeating the same lines a few times I find it less than ideal and would like to know if there is a better way to make it DRY (without having to create a new component).
I would like to be able to reuse the following lines:
{{ item.name }}
And a bit more of markup, but not much and no "functionality"
Definitively creating a new vue file with a new component would be overkill. But having to repeat those lines (the real ones a little bit more complex) really gives me pain a little bit.
Here is a very simple example that illustrates the problem.
Please remember that this is not the real code, the real code is inside a vue file (but I do not know how to simulate this here or in codepen)!!
var app = new Vue({
el: '#app',
data: {
items: [
{
name: 'name 1',
link: 'http://www.google.com'
},
{
name: 'name 2',
link: ''
},
]
},
methods: {
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.1/vue.min.js"></script>
<div id="app">
<template v-for="item in items">
<a :href="item.link" v-if="item.link != ''">
{{ item.name }}<br>
And a bit more of markup, but not much and no functionality
</a>
<p v-else>
{{ item.name }}<br>
And a bit more of markup, but not much and no functionality
</p>
</template>
</div>
You can use a component with "is" property set to the tag type you want rendered:
<div id="app">
<template v-for="item in items">
<component :href="item.link || false" :is="item.link ? 'a' : 'p'">
{{ item.name }}<br>
And a bit more of markup, but not much and no functionality
</component>
</template>
</div>
false on href causes that attr to not be rendered at all

Categories

Resources