Problemas para renderizar siete mil elementos en Vuetify

Prefacio

En el momento de escribir este artículo, me estaba preparando para un diploma y redactando un proyecto de diploma para las necesidades del Instituto Político de Moscú. Mi tarea es transferir la funcionalidad existente de la tabla PHP a algo moderno con un montón de comprobaciones y luego agregar esta funcionalidad. Motor - Nuxt, material-framework: Vuetify.





Después de escribir el código principal, yo, satisfecho, miré a mi mesa y me fui a la cama. Al día siguiente, tuve que importar más de 150 proyectos de clientes en mi hoja de cálculo. Después de importar, me sorprendió que el navegador estuviera congelado. Bueno, pasa, acabo de reabrir la pestaña. No ayudó. Primero encontré el problema de que estoy renderizando demasiado, tanto para el motor como para el navegador en sí. Tuve que empezar a pensar.





Primeros intentos

¿Qué hace un desarrollador cuando se enfrenta a un problema? Google. Esto fue lo primero que hice. Resulta que el problema de la reproducción lenta de la tabla Vuetify se encuentra con muchos menos elementos de los que tengo. Qué aconsejan:





  • Renderizar elementos pieza a pieza a través de setInterval







  • Establezca una condición para no representar elementos hasta que se active el enlace del ciclo de vida mounted()







  • Usar v-lazy



    para renderizado secuencial





Virtual Scroller, , . Vuetify Vuetify -_-





"" , Vuetify 3 ( ~) 50%, . , , . mounted , , , (, ?). v-lazy , 14 (Vuetify Transition Vue) .





, , . , , . . , StackOverflow, , , .





Mesa

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 , "...". , , .





Cargando...
...

( ), . - , : Vuetify Data Table , .





?





2.

, , , . , . .





:





  1. Vuetify





  2. ,





  3. - , ""





  4. 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 (/ .) , .





? . , , . , , , - , - .





Y sí: utilicé el depurador de rendimiento de Vue y vi quién lo consumía. A menudo, había literalmente uno o dos componentes, y reemplazándolos por otros con una lógica similar, el problema no se resolvió: el problema estaba en su número, no en la complejidad (sin contar la tabla Vuetify, hay muchos accesorios que se pasan de un componente a otro ).





Espero que las opciones que he dado impulsen a alguien a resolver su problema y que alguien aprenda algo nuevo =). Esperemos juntos por un Vue 3 estable con todo su ecosistema, al menos Nuxt 3. Algo promete muchas mejoras, tal vez algunas de las muletas de este artículo incluso desaparezcan.








All Articles