Front-end development - performance optimization Table performance optimization, reducing 85% rendering time

Original text: https://juejin.cn/post/7194516447932973112

Author: dev_zuo

Some time ago, the company had an important module upgraded from vue2 to vue3. After the upgrade, it was found that  the performance of element-plus table dropped significantly compared to the vue2 version .

In the scenario where all the custom columns are checked (20 rows x 180 columns), the switching time in the list will drop from the original 400-500 milliseconds to 7-8 seconds, which seriously affects the user experience. After a long period of performance Testing and debugging found several core optimization points.

Let’s take a look at the performance test data of each optimization point in the 20-row x 180-column scenario. In order to rule out chance, each scenario will be tested 3 times.

optimization type Table overall rendering time-consuming switch switching time-consuming
Before optimization 6.59s(6.71s、6.49s、6.577s) 3.982s(3.966s、3.947s、4.033s)
After changing data and columns from ref to shallowRef (17-20% reduction in time consumption) 5.18s(5.063s、5.104s、5.363s) 3.3s(3.175s、3.029s、3.122s)
getColspanRealWidth optimized (time-consuming reduced by 7-20%) 4.843(4.728s、4.703s 、5.098s) 2.65s(2.636s、2.645s、2.671s)
After business optimization removes the tooltip disabled attribute (80% reduction in time consumption) 1.008s(1.032s、0.997s、0.994s) 0.514s(0.517s、0.53s、0.495s)

The roughly optimized content is as follows

  • Modify the table source code, change data and columns from ref to shallowRef.

  • Modify the table source code, and optimize the responsive data in the getColspanRealWidth function.

  • Business optimization: remove the el-tooltip disabled attribute and change it to if.

Preparation

First initialize a vue3 project, introduce element-plus, and use el-table to implement a 20-row*180-column table.

  • 20 rows + 180 columns: 2 fixed columns (one text, one switch), 178 custom columns created by for loop

  • A switch to show/hide the table, used to test the time-consuming rendering of the table from hiding to showing

  • There is an el-tooltip + disabled logic in the custom column

1-table-base.png

Minimize business demo creation

The core table code code is as follows, for the complete code see: 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>

Rendering time-consuming calculation logic

Rendering time-consuming calculation logic is as follows, use script blocking to calculate rendering time-consuming

/*
<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);
};

Performance data, compared with performance time consumption

The time consumption of table rendering and switch switching test is as follows

table-base-duration.png

table hide to show gif

table-base-6-8-s.gif

switch from off to on gif

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

In order to verify the accuracy of the time-consuming test data we wrote ourselves, here is the performance recording turned on when the switch is switched on, as shown in the following figure

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

table-base-switch-performance.gif

In addition, when the performance recording is turned on, it is slightly slower than when it is not recorded. Let's start optimizing!

ref changed to shallowRef

Theoretical Basis and Feasibility Analysis

When the switch in the list is switched, although only one node of the table has changed, it still triggers the complete vue patch comparison update logic, which takes a long time.

Let's look at an official explanation: rendering mechanism | Vue.js[2]

vue-render-logic.png

In theory, reducing responsive data dependencies can improve performance.

shallowRef() is a shallow form of ref(). Response updates are triggered only when xx.value changes, reducing deep-level response dependencies and improving patch comparison performance. Reference Guide - Reducing Responsiveness Overhead for Large Immutable Structures [3]

const state = shallowRef({ count: 1 })

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

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

Here mainly modify two kinds of data from ref to shallowRef

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

Here is a question, will changing data and columns to shallowRef affect the function?

  • First, every time the list data is updated, our business logic will request the list, and setting list.value = xxx can trigger shallowRef update.

  • After testing, even the scope.row.status bound to the switch v-model can be updated normally.

  • No abnormality was found in manual click test selection, sorting, pagination, etc.

Based on the above three points, this modification is feasible in our business. Reminder: If you want to use this optimization in your own project, you need to test it first.

Let's see the specific modification details

Copy element-plus table source code to current project

The current latest version is 2.2.8, open element-plus/releases[4], download the latest version code, copy the table directory ( element-plus-2.2.28/packages/components/table) to src/table in the project, and delete the useless  __test__ test directory

Create a new route, and /new is assigned to a newly added table component. Compared with the original table component, only one line of code is added. The current component uses our custom modified table. See the complete code: 2-table-use-source | table-performance-demo[5]

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

error after import [plugin:vite:import-analysis] Failed to resolve import "@element-plus/directives" from "src\table\src\table.vue". Does the file exist?

element-table-error.png

Make some modifications so that the code can run in our own project, which is convenient for modifying and debugging the source code

  1. Search for @element-plus related keywords in the table directory and perform batch replacement

// @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. Search  @element-plus/components changed to import directly from 'element-plus'

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

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

Modify the source code - ref to shallowRef

In src/table/src/store/watcher.ts, change the data and columns data from ref to shallowRef, see the specific code: 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([]);
  // ...
}

In addition, add the following line in front of the middle table, the mark calls the table component we modified

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

Performance data (17-20% reduction in time consumption)

The time consumption of table rendering and switch switching test is as follows

table-ref-shallow-ref-duration.png

table hide to show gif

table-ref-shallowRef.gif

switch from off to on gif

table-ref-shallowRef-switch.gif

getColspanRealWidth optimization

When the page freezes, you can test the performance through performance. The figure below is the performance data after clicking the switch switch. can be seen

  • There are two Scripting blocking longTask, 1.89s + 1.73s, the overall time-consuming is 3.62s (when performance is turned on, it will be slower)

  • There are mainly two time-consuming tasks: the purple small block is the time-consuming rendering of render, and the green small block is the time-consuming patch comparison. Generally, patch is the internal logic of Vue, which is more difficult to optimize

  • By looking at the time-consuming related to render, it is found that getColspanRealWidth takes 212.2ms, and there is room for optimization here

switch-performance-test.png

Let's see why this function is time-consuming, mainly because it is called when tr is rendered to calculate the width of each column

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

The specific implementation is as follows, only the realWidth and width attributes are used, and columns.value is a responsive dependency, which can be modified to non-responsive data to see if it can reduce time-consuming.

// 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)
  )
}

Here we create a new optimizeColumns variable, store the realWidth and width used in the function, and pass this non-responsive data into the getColspanRealWidth function for internal use. For the complete code, see 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
  );
  // ...
})

Time-consuming dropped from 200ms to 0.7ms

After the modification, I tested the performance again. I was pleasantly surprised to find that the time-consuming of this function dropped from 200ms+ to within 1ms, and the render performance improved significantly. 1.54s + 1.45s = 2.99s

getColspanRealWidth-optimize.png

Performance data (7-20% reduction in time consumption)

The time consumption of table rendering and switch switching test is as follows

get-width-optimize-perf.png

table hide to show gif

get-width-optimize-table.gif

switch from off to on gif

get-width-optimize-switch.gif

Business optimization tooltip disabled changed to if

After the above optimizations, we realized that even subtle responsive data optimizations can have a big impact on performance. Does such data also exist in the business logic?

So the method of commenting + replacing the el-table-column slot with a static node is used  <span>123</span> to test where it takes a long time, and then optimize it accordingly .

After testing, it is found that after replacing the el-tooltip in the custom column with a static node, the performance is greatly improved.

<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>

As shown in the figure below, the switch switching time is reduced from about 2.7s to about 0.5s. In the performance panel, you can see that the patch is basically gone. It should be that after the template is compiled, the static node is marked, and there is no need to compare it when updating.

tooltip-static-node-test.png

Based on this idea, the el-tooltip component will double the time-consuming patch comparison, and reducing the number of nodes can enhance performance.

In order to save some code, el-tooltip uses the disabled attribute to hide the tooltip in a specific scenario. This part of the data does not need to use the el-tooltip node. The changes are as follows, using v-if to replace the disabled attribute function, although there will be repetitions code, but can reduce the number of nodes.

<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>

Test the performance again, you can see that the performance has not dropped much, and the switch switch can be refreshed in about 0.5s

tooltip-optimize.png

Performance data (80% reduction in time consumption)

The time consumption of table rendering and switch switching test is as follows

tooltip-optimize-pref.png

table hide to show gif

tooltip-optimize-table.gif

switch from off to on gif

tooltip-optimize-switch.gif

Summarize

As shown in the figure below, through 3 small details changes, the table rendering time is reduced from 6.88s to about 1s, with an average reduction of 85% rendering time, and the user experience basically meets expectations. Complete demo github address: github.com/zuoxiaobai/…[8]

pref-summary.png

In the vue3 project, special attention should be paid to the responsive data. When encountering a slow scene, it is recommended to use the following methods for performance optimization

  • Use performance to analyze performance bottlenecks, or write a performance time-consuming logic by yourself, so that you have data reference when doing performance optimization.

  • For scenarios with many business codes, the method of commenting + replacing with static nodes is used to troubleshoot time-consuming logic and perform targeted optimization.

  • In addition, you can use the Vue devtools debugging tool to view the time-consuming component update rendering and troubleshoot responsive data problems.

Refer to One operation, I improved the performance of the Table component by ten times [9]

Guess you like

Origin blog.csdn.net/helloyangkl/article/details/129085937