Desarrollo front-end: optimización del rendimiento Optimización del rendimiento de la tabla, reduciendo el tiempo de renderizado en un 85 %

Texto original: https://juejin.cn/post/7194516447932973112

Autor: dev_zuo

Hace algún tiempo, la empresa actualizó un módulo relativamente importante de vue2 a vue3.Después de la actualización, se descubrió que  el rendimiento de la tabla element-plus se redujo significativamente en comparación con la versión vue2 .

En el escenario donde se verifican todas las columnas personalizadas (20 filas x 180 columnas), el tiempo de cambio en la lista se reduce de los 400-500 milisegundos originales a 7-8 segundos, lo que afecta seriamente la experiencia del usuario.Después de un largo período de rendimiento Las pruebas y la depuración encontraron varios puntos básicos de optimización.

Echemos un vistazo a los datos de prueba de rendimiento de cada punto de optimización en el escenario de 20 filas x 180 columnas Para descartar la posibilidad, cada escenario se probará 3 veces.

tipo de optimización La representación general de la tabla requiere mucho tiempo interruptor de conmutación que requiere mucho tiempo
Antes de la optimización 6,59 s (6,71 s, 6,49 s, 6,577 s) 3,982 s (3,966 s, 3,947 s, 4,033 s)
Después de cambiar los datos y las columnas de referencia a referencia superficial (17-20 % de reducción en el consumo de tiempo) 5,18 s (5,063 s, 5,104 s, 5,363 s) 3,3 s (3,175 s, 3,029 s, 3,122 s)
getColspanRealWidth optimizado (consumo de tiempo reducido en un 7-20%) 4,843 (4,728 s, 4,703 s, 5,098 s) 2,65 s (2,636 s, 2,645 s, 2,671 s)
Después de que la optimización comercial elimine el atributo deshabilitado de información sobre herramientas (reducción del 80% en el consumo de tiempo) 1,008 s (1,032 s, 0,997 s, 0,994 s) 0,514 s (0,517 s, 0,53 s, 0,495 s)

El contenido más o menos optimizado es el siguiente

  • Modifique el código fuente de la tabla, cambie los datos y las columnas de referencia a referencia superficial.

  • Modifique el código fuente de la tabla y optimice los datos de respuesta en la función getColspanRealWidth.

  • Optimización empresarial: elimine el atributo deshabilitado el-tooltip y cámbielo a if.

Preparación

Primero inicialice un proyecto vue3, introduzca element-plus y use el-table para implementar una tabla de 20 filas x 180 columnas.

  • 20 filas + 180 columnas: 2 columnas fijas (un texto, un interruptor), 178 columnas personalizadas creadas por bucle for

  • Un interruptor para mostrar/ocultar la tabla, que se usa para probar la lenta representación de la tabla de ocultar a mostrar

  • Hay un el-tooltip + lógica deshabilitada en la columna personalizada

1-tabla-base.png

Minimice la creación de demostraciones comerciales

El código del código de la tabla principal es el siguiente; para ver el código completo, consulte: table-base | table-performance-demo[1]

<el-table
  v-if="showTable"
  :data="tableData"
  style="width: 100%; height: 500px; overflow: scroll"
>
  <el-table-column prop="info" label="信息" width="80" fixed />
  <el-table-column prop="status" label="状态" width="80" fixed>
    <template #default="scope">
      <el-switch v-model="scope.row.status" @change="statusChange" />
    </template>
  </el-table-column>
  <el-table-column
    v-for="item in customColumns"
    :key="item.prop"
    :prop="item.prop"
    :label="item.label"
  >
    <template #default="scope">
      <el-tooltip
        placement="top-start"
        :disabled="!(item.prop === 'column1' && scope.row[item.prop])"
      >
        <template #content>
          <span>{
   
   { "tooltip显示" + scope.row[item.prop] }}</span>
        </template>
        <span>{
   
   { scope.row[item.prop] }}</span>
      </el-tooltip>
    </template>
  </el-table-column>
</el-table>

<script lang="ts" setup>
// 假数据逻辑
const customColCount = 178; // 自定义列数
const rowCount = 20; // 行数
onBeforeMount(() => {
  // 初始化自定义列数据
  let temp = [];
  for (let i = 0; i < customColCount; i++) {
    temp.push({ prop: `column${i + 1}`, label: `第${i + 1}列` });
  }
  customColumns.value = temp;

  // 初始化表格数据
  let dataTemp = [];
  for (let i = 0; i < rowCount; i++) {
    let row: any = { info: `第${i + 1}行`, status: true };
    i === 0 && (row.status = false);
    for (let j = 0; j < customColCount + 2; j++) {
      row[`column${j + 1}`] = `第${i + 1}行${j + 1}列`;
    }
    dataTemp.push(row);
  }
  tableData.value = dataTemp;
});
</script>

Representación de lógica de cálculo que requiere mucho tiempo

La lógica de cálculo que requiere mucho tiempo de renderizado es la siguiente, utilice el bloqueo de secuencias de comandos para calcular el tiempo de renderizado

/*
<div v-loading="showLoading" element-loading-text="数据加载中...">
  <p>
    当前显示:{
   
   { `${rowCount}行${customColCount + 2}列` }}, 显示/隐藏 table:
    <el-switch :model-value="showTable" @click="switchTableShow"></el-switch>
  </p>
  <el-table v-if="showTable"> .... </el-table>
</div>
*/

// 显示/隐藏 table,计算 table 渲染耗时
const switchTableShow = () => {
  // 先展示 loading
  showLoading.value = true;

  // 200ms 后再修改 table 是否显示,防止和 loading 合并到一个渲染周期,导致 loading 不显示
  setTimeout(() => {
    let startTime = +new Date();
    showTable.value = !showTable.value; // 修改 table 显示,会形成 script 阻塞
    showLoading.value = false; // 这里的 loading 关闭,会在 table 阻塞完成后渲染关闭 dom
    // 创建一个宏任务,等上面阻塞的微任务执行完成后,再显示计算耗时
    setTimeout(() => {
      let endTime = +new Date();
      ElMessage.success(`渲染耗时:${(endTime - startTime) / 1000}s`);
    }, 0);
  }, 200);
};

Datos de rendimiento, en comparación con el consumo de tiempo de rendimiento

El consumo de tiempo de la representación de la tabla y la prueba de conmutación del interruptor es el siguiente

tabla-base-duración.png

tabla ocultar para mostrar gif

tabla-base-6-8-s.gif

cambiar de apagado a encendido gif

table-base-switch-3-8-s.gif

Para verificar la precisión de los datos de prueba que tomamos mucho tiempo y que escribimos nosotros mismos, aquí está el registro de rendimiento activado cuando se enciende el interruptor, como se muestra en la siguiente figura

页面显示渲染耗时:4.524s,performance 中两个 Long Task:2.29s + 2.17,加上非 Long Task 部分,数据基本一致,因此我们自己写的耗时计算逻辑是基本准确的

table-base-switch-rendimiento.gif

Además, cuando la grabación de interpretación está activada, es un poco más lenta que cuando no está grabada. ¡Comencemos a optimizar!

referencia cambiada a referencia superficial

Base Teórica y Análisis de Factibilidad

Cuando se cambia el interruptor en la lista, aunque solo un nodo de la tabla ha cambiado, aún activa la lógica de actualización de comparación de parches vue completa, lo que lleva mucho tiempo.

Veamos una explicación oficial: mecanismo de representación | Vue.js[2]

vue-render-logic.png

En teoría, reducir las dependencias de datos de respuesta puede mejorar el rendimiento.

superficialRef() es una forma superficial de ref(). Las actualizaciones de respuesta se activan solo cuando cambia xx.value, lo que reduce las dependencias de respuesta de nivel profundo y mejora el rendimiento de la comparación de parches. Guía de referencia: reducción de la sobrecarga de capacidad de respuesta para estructuras inmutables grandes [3]

const state = shallowRef({ count: 1 })

// shallowRef 不会触发更改,如果 state 为 ref 时,是可以触发更新的。
state.value.count = 2

// shallowRef 会触发更改
state.value = { count: 2 }

Aquí modifica principalmente dos tipos de datos de referencia a referencia superficial

// src/table/src/store/watcher.ts
function useWatcher<T>() {
  const data: Ref<T[]> = shallowRef([]); // table data 数据
  const columns: Ref<TableColumnCtx<T>[]> = shallowRef([]); // 列数据
  // ...
}

Aquí hay una pregunta, ¿el cambio de datos y columnas a ref superficial afectará la función?

  • En primer lugar, cada vez que se actualicen los datos de la lista, nuestra lógica comercial solicitará la lista y establecer list.value = xxx puede desencadenar una actualización de referencia superficial.

  • Después de la prueba, incluso el scope.row.status vinculado al modelo v del conmutador se puede actualizar normalmente.

  • No se encontró ninguna anomalía en la selección, clasificación, paginación, etc. de la prueba de clic manual.

Con base en los tres puntos anteriores, esta modificación es factible en nuestro negocio. Recordatorio: si desea utilizar esta optimización en su propio proyecto, primero debe probarla.

Veamos los detalles específicos de la modificación.

Copie el código fuente de la tabla element-plus al proyecto actual

La última versión actual es 2.2.8, abra element-plus/releases[4], descargue el código de la última versión, copie el directorio de la tabla ( element-plus-2.2.28/packages/components/table) a src/table en el proyecto y elimine el  __test__ directorio de prueba inútil

Cree una nueva ruta y /new se asigna a un componente de tabla recién agregado. En comparación con el componente de tabla original, solo se agrega una línea de código. El componente actual usa nuestra tabla modificada personalizada. Ver el código completo: 2-table-use-source | table-performance-demo[5]

import ElTable from "@/table/src/table.vue";

error después de la importación [plugin:vite:import-analysis] Failed to resolve import "@element-plus/directives" from "src\table\src\table.vue". Does the file exist?

elemento-tabla-error.png

Hacer algunas modificaciones para que el código pueda ejecutarse en nuestro propio proyecto, lo cual es conveniente para modificar y depurar el código fuente.

  1. Busque palabras clave relacionadas con @element-plus en el directorio de la tabla y realice el reemplazo por lotes

// @element-plus/directives => element-plus/es/directives/index
// @element-plus/hooks => element-plus/es/hooks/index
// @element-plus/utils => element-plus/es/utils/index
  1. La búsqueda  @element-plus/components cambió para importar directamente desde 'element-plus'

// 比如:
import ElCheckbox from '@element-plus/components/checkbox'
// 改为
import { ElCheckbox } from 'element-plus'

// 注意:资源类的可以不用改,比如 import "@element-plus/components/base/style/css"; 

Modificar el código fuente - referencia a ref.superficial

En src/table/src/store/watcher.ts, cambie los datos y los datos de las columnas de referencia a referencia superficial, consulte el código específico: table-ref-shallowRef | table-performance-demo[6]

// src/table/src/store/watcher.ts
function useWatcher<T>() {
  const data: Ref<T[]> = shallowRef([]);
  const _data: Ref<T[]> = shallowRef([]);
  const _columns: Ref<TableColumnCtx<T>[]> = shallowRef([]);
  const columns: Ref<TableColumnCtx<T>[]> = shallowRef([]);
  // ...
}

Además, agregue la siguiente línea al frente de la tabla del medio, la marca llama al componente de la tabla que modificamos

<!-- src/table/src/table.vue 表格顶部增加下面一行 --->
<p style="color: red">来自 table 源码</p>
<!-- 内部逻辑 -->
<div :class="ns.e('inner-wrapper')" :style="tableInnerStyle">
    <!-- ... -->
</div>

Datos de rendimiento (17-20 % de reducción en el consumo de tiempo)

El consumo de tiempo de la representación de la tabla y la prueba de conmutación del interruptor es el siguiente

tabla-ref-superficial-ref-duración.png

tabla ocultar para mostrar gif

tabla-ref-shallowRef.gif

cambiar de apagado a encendido gif

tabla-ref-shallowRef-switch.gif

optimización getColspanRealWidth

Cuando la página se congela, puede probar el rendimiento a través del rendimiento. La siguiente figura son los datos de rendimiento después de hacer clic en el interruptor. puede ser visto

  • Hay dos LongTask de bloqueo de secuencias de comandos, 1,89 s + 1,73 s, el tiempo total que consume es de 3,62 s (cuando el rendimiento está activado, será más lento)

  • Hay dos tareas principales que consumen mucho tiempo: el bloque pequeño púrpura es la representación del renderizado que consume mucho tiempo, y el bloque pequeño verde es la comparación de parches que consume mucho tiempo. En general, el parche es la lógica interna de Vue, que es más difícil de optimizar

  • Al observar el tiempo relacionado con el procesamiento, se encuentra que getColspanRealWidth toma 212.2ms, y hay espacio para la optimización aquí.

cambiar-rendimiento-test.png

Veamos por qué esta función requiere mucho tiempo, principalmente porque se llama cuando se procesa tr para calcular el ancho de cada columna.

// src\table\src\table-body\render-helper.ts
columns.value.map((column, cellIndex) => {
  // ...
  columnData.realWidth = getColspanRealWidth(
    columns.value,
    colspan,
    cellIndex
  );
  // ...
})

La implementación específica es la siguiente, solo se usan los atributos realWidth y width, y column.value es una dependencia receptiva, que se puede modificar a datos que no responden para ver si puede reducir el tiempo.

// src\table\src\table-body\styles-helper.ts
const getColspanRealWidth = (
  columns: TableColumnCtx<T>[],
  colspan: number,
  index: number
): number => {
  if (colspan < 1) {
    return columns[index].realWidth
  }
  const widthArr = columns
    .map(({ realWidth, width }) => realWidth || width)
    .slice(index, index + colspan)
  return Number(
    widthArr.reduce((acc, width) => Number(acc) + Number(width), -1)
  )
}

Aquí creamos una nueva variableoptimizeColumns, almacenamos el ancho real y el ancho utilizados en la función, y pasamos estos datos que no responden a la función getColspanRealWidth para uso interno.Para obtener el código completo, consulte getColspanRealWidth-optimize | table-performance-demo[7 ]

// src\table\src\table-body\render-helper.ts
const optimizeColumns = columns.value.map((item) => {
  return { realWidth: item.realWidth, width: item.width };
});
columns.value.map((column, cellIndex) => {
  // ...
  columnData.realWidth = getColspanRealWidth(
    optimizeColumns, // 传入函数内部时,使用非响应式数据
    colspan,
    cellIndex
  );
  // ...
})

El consumo de tiempo se redujo de 200 ms a 0,7 ms

Después de la modificación, volví a probar el rendimiento. Me sorprendió gratamente descubrir que el tiempo que consumía esta función se redujo de más de 200 ms a 1 ms, y el rendimiento del renderizado mejoró significativamente. 1,54 s + 1,45 s = 2,99 s

getColspanRealWidth-optimize.png

Datos de rendimiento (7-20 % de reducción en el consumo de tiempo)

El consumo de tiempo de la representación de la tabla y la prueba de conmutación del interruptor es el siguiente

obtener-ancho-optimizar-perf.png

tabla ocultar para mostrar gif

get-width-optimize-table.gif

cambiar de apagado a encendido gif

get-width-optimize-switch.gif

La información sobre herramientas de optimización comercial deshabilitada cambió a si

Después de las optimizaciones anteriores, nos dimos cuenta de que incluso las optimizaciones sutiles de datos receptivos pueden tener un gran impacto en el rendimiento. ¿Tales datos también existen en la lógica de negocios?

Entonces, el método de comentar + reemplazar la ranura de la columna de la tabla con un nodo estático se usa  <span>123</span> para probar dónde lleva mucho tiempo y luego optimizarlo en consecuencia .

Después de la prueba, se encuentra que después de reemplazar la información sobre herramientas electrónicas en la columna personalizada con un nodo estático, el rendimiento mejora considerablemente.

<el-table-column
  v-for="item in customColumns"
  :key="item.prop"
  :prop="item.prop"
  :label="item.label"
>
  <template #default="scope">
    <!-- <el-tooltip
      placement="top-start"
      :disabled="!(item.prop === 'column1' && scope.row[item.prop])"
    >
      <template #content>
        <span>{
   
   { "tooltip显示" + scope.row[item.prop] }}</span>
      </template>
      <span>{
   
   { scope.row[item.prop] }}</span>
    </el-tooltip> -->
    <span>123</span>
  </template>
</el-table-column>

Como se muestra en la figura a continuación, el tiempo de conmutación del interruptor se reduce de aproximadamente 2,7 s a aproximadamente 0,5 s. En el panel de rendimiento, puede ver que el parche básicamente se ha ido. Debería ser que después de compilar la plantilla, el nodo estático está marcado y no hay necesidad de compararlo al actualizar.

tooltip-static-node-test.png

Basado en esta idea, el componente de información sobre herramientas duplicará la comparación de parches que requiere mucho tiempo, y reducir la cantidad de nodos puede mejorar el rendimiento.

Para guardar algo de código, el-tooltip usa el atributo disabled para ocultar la información sobre herramientas en un escenario específico. Esta parte de los datos no necesita usar el nodo el-tooltip. Los cambios son los siguientes, usando v-if para reemplace la función de atributo deshabilitada, aunque habrá código de repeticiones, pero puede reducir el número de nodos.

<template #default="scope">
  <!-- 
    <el-tooltip
      placement="top-start"
      :disabled="!(item.prop === 'column1' && scope.row[item.prop])"
    >
      <template #content>
        <span>{
   
   { "tooltip显示" + scope.row[item.prop] }}</span>
      </template>
      <span>{
   
   { scope.row[item.prop] }}</span>
    </el-tooltip>
  -->
  <span v-if="!(item.prop === 'column1' && scope.row[item.prop])">
    {
   
   { scope.row[item.prop] }}
  </span>
  <el-tooltip v-else placement="top-start">
    <template #content>
      <span>{
   
   { "tooltip显示" + scope.row[item.prop] }}</span>
    </template>
    <span>{
   
   { scope.row[item.prop] }}</span>
  </el-tooltip>
</template>

Vuelva a probar el rendimiento, puede ver que el rendimiento no ha disminuido mucho, y el interruptor del interruptor se puede actualizar en aproximadamente 0,5 segundos

información sobre herramientas-optimizar.png

Datos de rendimiento (80% de reducción en el consumo de tiempo)

El consumo de tiempo de la representación de la tabla y la prueba de conmutación del interruptor es el siguiente

información sobre herramientas-optimizar-pref.png

tabla ocultar para mostrar gif

información sobre herramientas-optimizar-tabla.gif

cambiar de apagado a encendido gif

tooltip-optimize-switch.gif

Resumir

Como se muestra en la figura a continuación, a través de 3 pequeños cambios en los detalles, el tiempo de representación de la tabla se reduce de 6,88 s a aproximadamente 1 s, con una reducción promedio del tiempo de representación del 85 %, y la experiencia del usuario básicamente cumple con las expectativas. Dirección de github de demostración completa: github.com/zuoxiaobai/…[8]

pref-resumen.png

En el proyecto vue3, se debe prestar especial atención a los datos receptivos. Al encontrar una escena lenta, se recomienda utilizar los siguientes métodos para optimizar el rendimiento

  • Utilice el rendimiento para analizar los cuellos de botella del rendimiento, o escriba usted mismo una lógica de rendimiento que consuma mucho tiempo, de modo que tenga referencia de datos al realizar la optimización del rendimiento.

  • Para escenarios con muchos códigos comerciales, el método de comentar + reemplazar con nodos estáticos se usa para solucionar problemas de lógica que consume mucho tiempo y realizar una optimización específica.

  • Además, puede usar la herramienta de depuración Vue devtools para ver la representación de actualización de componentes que consume mucho tiempo y solucionar problemas de datos de respuesta.

Consulte Una operación, mejoré el rendimiento del componente Tabla diez veces [9]

Supongo que te gusta

Origin blog.csdn.net/helloyangkl/article/details/129085937
Recomendado
Clasificación