Foreword
At the time of this writing, I was preparing for a diploma and writing a diploma project for the needs of the Moscow Poly. My task is to transfer the existing functionality from the PHP table to something modern with a bunch of checks, and then add this functionality. Engine - Nuxt, material-framework: Vuetify.
After writing the primary code, I, satisfied, looked around my table and went to bed. The next day, I had to import 150+ customer projects into my spreadsheet. After importing, I was surprised that the browser was frozen. Well, it happens, I just reopen the tab. Did not help. This is the first time I encountered the problem that I am rendering too much, both for the engine and for the browser itself. I had to start thinking.
First attempts
What does a developer do when faced with a problem? Googling. This was the first thing I did. As it turns out, the problem of slow rendering of the Vuetify table is encountered with much fewer elements than I have. What they advise:
Render elements piece by piece through
setInterval
Set a condition to not render elements until the lifecycle hook is triggered
mounted()
Use
v-lazy
for sequential rendering
Virtual Scroller, , . Vuetify Vuetify -_-
"" , Vuetify 3 ( ~) 50%, . , , . mounted , , , (, ?). v-lazy , 14 (Vuetify Transition Vue) .
, , . , , . . , StackOverflow, , , .
1. Intersection Observer
, . v-lazy , 14 . Vuetify Virtual Scroller Vuetify Data Table - . , . , ? Intersection Observer.
Internet Explorer , .
: v-intersect
Vuetify. 7 =(. , .
mounted() {
// overflow: auto
// : 10%
this.observer = new IntersectionObserver(this.handleObserve, { root: this.$refs.table as any, threshold: 0.1 });
// observe ?
for (const element of Array.from(document.querySelectorAll('#intersectionElement'))) {
this.observer.observe(element);
}
},
handleObserve:
async handleObserve(entries: IntersectionObserverEntry[]) {
const parsedEntries = entries.map(entry => {
const target = entry.target as HTMLElement;
// data-
const project = +(target.dataset.projectId || '0');
const speciality = +(target.dataset.specialityId || '0');
return {
isIntersecting: entry.isIntersecting,
project,
speciality,
};
});
//
this.$set(this, 'observing', [
//
...parsedEntries.filter(x => x.isIntersecting && !this.observing.some(y => y.project === x.project && y.speciality === x.speciality)),
//
...this.observing.filter(entry => !parsedEntries.some(x => !x.isIntersecting && x.project === entry.project && x.speciality === entry.speciality)),
]);
//
Array.from(document.querySelectorAll('#intersectionElement')).forEach((target) => this.observer?.unobserve(target));
// Vuetify
await this.$nextTick();
// 300, ,
await new Promise((resolve) => setTimeout(resolve, 500));
//
Array.from(document.querySelectorAll('#intersectionElement'))
.forEach((target) => this.observer?.observe(target));
},
, 7 , Intersection Observer. observing, projectId specialityId, , . - v-if - . !
<template #[`item.speciality-${speciality.id}`]="{item, headers}" v-for="speciality in getSpecialities()">
<div id="intersectionElement" :data-project-id="item.id" :data-speciality-id="speciality.id">
<ranking-projects-table-item
v-if="observing.some(
x => x.project === item.id && x.speciality === speciality.id
)"
:speciality="speciality"
:project="item"
/>
<template v-else>
...
</template>
</div>
</template>
v-once. $forceUpdate
. , Vuetify , .
<v-data-table
v-bind="getTableSettings()"
v-once
:items="projects"
@update:expanded="$forceUpdate()">
:
. 7 , "...". , , .
( ), . - , : Vuetify Data Table , .
?
2.
, , , . , . .
:
Vuetify
,
- , ""
Intersection Observer , , (300 )
Virtual Scroller. Vuetify, ? display: grid
? - .
Virtual Scroller? . Grid'? . CSS- CSS:
<div class="ranking-table" :style="{
'--projects-count': getSettings().projectsCount,
'--specialities-count': getSettings().specialitiesCount,
'--first-column-width': `${getSettings().firstColumnWidth}px`,
'--others-columns-width': `${getSettings().othersColumnsWidth}px`,
'--cell-width': `${getSettings().firstColumnWidth + getSettings().othersColumnsWidth * getSettings().specialitiesCount}px`,
'--item-height': `${getSettings().itemHeight}px`
}">
display: grid;
grid-template-columns:
var(--first-column-width)
repeat(var(--specialities-count), var(--others-columns-width));
. ! , , Virtual Scroller ( ), , -
.ranking-table_v2__scroll::v-deep {
.v-virtual-scroll {
&__container, &__item, .ranking-table_v2__project {
width: var(--cell-width);
}
}
}
: <style>
scoped
, , : - App.vue, , v-deep.
: Virtual Scroller, , . : Expandable Items , . , , , Vuetify, , . , :
<v-virtual-scroll
class="ranking-table_v2__scroll"
:height="getSettings().commonHeight"
:item-height="getSettings().itemHeight"
:items="projects">
<template #default="{item}">
<div class="ranking-table_v2__project" :key="item.id">
<!-- ... -->
: , , 6 ( ), 6 + . 50. 300 . , 300 .
v-lazy: . 14 , 600 . ( ) v-lazy. , , .
<v-lazy class="ranking-table_v2__item ranking-table_v2__item--speciality"
v-for="(speciality, index) in specialities"
:key="speciality.id">
<!-- -->
</v-lazy>
, :
:
/
v-once $forceUpdate
:
(expand),
,
/ ( )
, , , Scroll
, window.innerHeight CSS VirtualScroll
, , UX .
2 Vuetify. , . , . , , Vuetify (/ .) , .
? . , , . , , , - , - .
And yes: I used Vue's performance debugger and watched who was consuming it. Often there were literally one or two components, and replacing them with some other with similar logic, the problem was not solved - the point was their number, not complexity (not counting the Vuetify table - there are many props passed from component to component ).
I hope that the options I have given will push someone to solve his problem, and someone will just learn something new =). Let's wait together for a stable Vue 3 with its entire ecosystem, at least Nuxt 3. Something promises a lot of improvements, maybe some of the crutches from this article will even disappear.