Avant-propos
Au moment d'écrire ces lignes, je préparais un diplôme et écrivais un projet de diplôme pour les besoins du Poly de Moscou. Ma tâche consiste à transférer la fonctionnalité existante de la table PHP vers quelque chose de moderne avec un tas de vérifications, puis d'ajouter cette fonctionnalité. Moteur - Nuxt, matériel-framework : Vuetify.
Après avoir écrit le code primaire, j'ai, satisfait, j'ai regardé autour de ma table et je suis allé me coucher. Le lendemain, j'ai dû importer plus de 150 projets clients dans ma feuille de calcul. Après l'importation, j'ai été surpris que le navigateur soit gelé. Bon, ça arrive, je viens de rouvrir l'onglet. N'a pas aidé. J'ai d'abord rencontré le problème que je rends trop, à la fois pour le moteur et pour le navigateur lui-même. Je devais commencer à réfléchir.
Premiers essais
Que fait un développeur face à un problème ? Googler. C'est la première chose que j'ai faite. Il s'avère que le problème du rendu lent de la table Vuetify est rencontré avec beaucoup moins d'éléments que moi. Ce qu'ils conseillent :
Rendre les éléments pièce par pièce
setInterval
Définir une condition pour ne pas rendre les éléments tant que le hook de cycle de vie n'est pas déclenché
mounted()
Utiliser
v-lazy
pour le rendu séquentiel
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 (/ .) , .
? . , , . , , , - , - .
Et oui : j'ai utilisé le débogueur de performances de Vue et j'ai regardé qui le consommait. Souvent, il y avait littéralement un ou deux composants, et en les remplaçant par un autre avec une logique similaire, le problème n'était pas résolu - la question était dans leur nombre, pas dans leur complexité (sans compter la table Vuetify - il y a beaucoup d'accessoires passés d'un composant à l'autre ).
J'espère que les options que j'ai données pousseront quelqu'un à résoudre son problème, et que quelqu'un apprendra simplement quelque chose de nouveau =). Attendons ensemble une Vue 3 stable avec tout son écosystème, au moins Nuxt 3. Quelque chose promet beaucoup d'améliorations, peut-être que certaines des béquilles de cet article disparaîtront même.