原文:https://juejin.cn/post/7194516447932973112
作者: dev_zuo
以前、比較的重要なモジュールを vue2 から vue3 にアップグレードしたところ、 vue2 バージョンに比べて要素プラス テーブルのパフォーマンスが大幅に低下したことが判明しました。
すべてのカスタム列がチェックされるシナリオ (20 行 x 180 列) では、リストの切り替え時間が元の 400 ~ 500 ミリ秒から 7 ~ 8 秒に短縮され、ユーザー エクスペリエンスに深刻な影響を与えます。パフォーマンスのテストとデバッグにより、いくつかの重要な最適化ポイントが見つかりました。
20 行 x 180 列のシナリオの各最適化ポイントのパフォーマンス テスト データを見てみましょう. 可能性を排除するために、各シナリオは 3 回テストされます.
最適化タイプ | テーブル全体のレンダリングに時間がかかる | スイッチの切り替えに時間がかかる |
---|---|---|
最適化前 | 6.59秒(6.71秒、6.49秒、6.577秒) | 3.982秒(3.966秒、3.947秒、4.033秒) |
データと列をrefからshallowRefに変更した後(消費時間の17〜20%削減) | 5.18秒(5.063秒、5.104秒、5.363秒) | 3.3秒(3.175秒、3.029秒、3.122秒) |
getColspanRealWidth の最適化 (時間が 7 ~ 20% 削減されました) | 4.843(4.728秒、4.703秒、5.098秒) | 2.65秒(2.636秒、2.645秒、2.671秒) |
ビジネスの最適化によりツールチップの無効化属性が削除された後 (時間の消費が 80% 削減) | 1.008秒(1.032秒、0.997秒、0.994秒) | 0.514秒(0.517秒、0.53秒、0.495秒) |
ざっくり最適化した内容は以下の通り
-
テーブルのソース コードを変更し、データと列を ref からshallowRef に変更します。
-
テーブルのソース コードを変更し、getColspanRealWidth 関数でレスポンシブ データを最適化します。
-
ビジネスの最適化: el-tooltip disabled 属性を削除し、if に変更します。
準備
最初に vue3 プロジェクトを初期化し、element-plus を導入し、el-table を使用して 20 行 x 180 列のテーブルを実装します。
-
20 行 + 180 列: 2 つの固定列 (1 つのテキスト、1 つのスイッチ)、for ループによって作成された 178 のカスタム列
-
表を表示/非表示にするスイッチ。時間のかかる表の非表示から表示へのレンダリングをテストするために使用されます
-
カスタム列に el-tooltip + disabled ロジックがあります
1-テーブルベース.png
ビジネスデモの作成を最小限に抑える
コア テーブル コード コードは次のとおりです。完全なコードについては、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>
時間のかかる計算ロジックのレンダリング
レンダリングに時間がかかる計算ロジックは次のとおりです。スクリプト ブロックを使用して、レンダリングに時間がかかる計算を行います。
/*
<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);
};
パフォーマンス データとパフォーマンス時間の消費量の比較
テーブルのレンダリングとスイッチの切り替えテストの所要時間は次のとおりです。
table-base-duration.png
表を非表示にして gif を表示
table-base-6-8-s.gif
gifをオフからオンに切り替えます
テーブル ベース スイッチ 3-8-s.gif
時間をかけて自分たちで書いたテストデータの精度を検証するために、次の図に示すように、スイッチをオンにしたときにパフォーマンスの記録をオンにしました。
页面显示渲染耗时:4.524s,performance 中两个 Long Task:2.29s + 2.17,加上非 Long Task 部分,数据基本一致,因此我们自己写的耗时计算逻辑是基本准确的
table-base-switch-performance.gif
また、演奏の録音をオンにすると、録音していないときよりも少し遅くなります。最適化を始めましょう!
ref は、shallowRef に変更されました
理論的根拠と実現可能性分析
リスト内のスイッチが切り替えられると、テーブルの 1 つのノードのみが変更されますが、完全な vue パッチ比較更新ロジックが引き続きトリガーされ、長い時間がかかります。
公式の説明を見てみましょう: レンダリング メカニズム | Vue.js[2]
vue-render-logic.png
理論的には、レスポンシブ データの依存関係を減らすことで、パフォーマンスを向上させることができます。
shadowRef() は ref() の浅い形式です。レスポンスの更新は、xx.value が変更された場合にのみトリガーされ、深いレベルのレスポンスの依存関係を減らし、パッチ比較のパフォーマンスを向上させます。リファレンス ガイド - 大規模な不変構造の応答性オーバーヘッドの削減 [3]
const state = shallowRef({ count: 1 })
// shallowRef 不会触发更改,如果 state 为 ref 时,是可以触发更新的。
state.value.count = 2
// shallowRef 会触发更改
state.value = { count: 2 }
ここでは、主にrefからshallowRefへの2種類のデータを変更します
// src/table/src/store/watcher.ts
function useWatcher<T>() {
const data: Ref<T[]> = shallowRef([]); // table data 数据
const columns: Ref<TableColumnCtx<T>[]> = shallowRef([]); // 列数据
// ...
}
ここで質問があります。データと列をshallowRefに変更すると関数に影響しますか?
-
まず、リスト データが更新されるたびに、ビジネス ロジックがリストを要求し、list.value = xxx を設定すると、shallowRef の更新をトリガーできます。
-
テスト後、switch v-model にバインドされた scope.row.status も正常に更新できます。
-
手動のクリックテスト選択、ソート、ページネーション等に異常は見られませんでした。
以上の 3 点を踏まえると、この変更は当社のビジネスにおいて実現可能です。注意: この最適化を自分のプロジェクトで使用する場合は、最初にテストする必要があります。
具体的な改造内容を見てみましょう
要素と表のソース コードを現在のプロジェクトにコピーする
現在の最新バージョンは 2.2.8 で、element-plus/releases[4] を開き、最新バージョンのコードをダウンロードし、テーブル ディレクトリ ( ) をelement-plus-2.2.28/packages/components/table
プロジェクト内の src/table にコピーし、不要な __test__
テスト ディレクトリを削除します。
新しいルートを作成し、新しく追加されたテーブル コンポーネントに /new を割り当てます。元のテーブル コンポーネントと比較して、1 行のコードしか追加されていません。現在のコンポーネントは、カスタムの変更されたテーブルを使用します。完全なコードを参照してください: 2-table-use-source | table-performance-demo[5]
import ElTable from "@/table/src/table.vue";
インポート後のエラー [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
コードを独自のプロジェクトで実行できるようにいくつかの変更を加えます。これは、ソース コードの変更とデバッグに便利です。
-
テーブルディレクトリで @element-plus 関連のキーワードを検索し、一括置換を実行
// @element-plus/directives => element-plus/es/directives/index
// @element-plus/hooks => element-plus/es/hooks/index
// @element-plus/utils => element-plus/es/utils/index
-
@element-plus/components
「element-plus」から直接インポートするように検索が 変更されました
// 比如:
import ElCheckbox from '@element-plus/components/checkbox'
// 改为
import { ElCheckbox } from 'element-plus'
// 注意:资源类的可以不用改,比如 import "@element-plus/components/base/style/css";
ソース コードを変更する - ref を shadowRef に変更します。
src/table/src/store/watcher.ts で、データと列のデータを ref から shadowRef に変更します。特定のコードを参照してください:
// 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([]);
// ...
}
さらに、中央のテーブルの前に次の行を追加します。マークは、変更したテーブル コンポーネントを呼び出します。
<!-- src/table/src/table.vue 表格顶部增加下面一行 --->
<p style="color: red">来自 table 源码</p>
<!-- 内部逻辑 -->
<div :class="ns.e('inner-wrapper')" :style="tableInnerStyle">
<!-- ... -->
</div>
パフォーマンス データ (所要時間の 17 ~ 20% の削減)
テーブルのレンダリングとスイッチの切り替えテストの所要時間は次のとおりです。
table-ref-shallow-ref-duration.png
表を非表示にして gif を表示
table-ref-shallowRef.gif
gifをオフからオンに切り替えます
table-ref-shallowRef-switch.gif
getColspanRealWidth の最適化
ページがフリーズしたら、パフォーマンスを通じてパフォーマンスをテストできます。下の図は、スイッチ スイッチをクリックした後のパフォーマンス データです。見られます
-
2 つのスクリプティング ブロック longTask があり、1.89 秒 + 1.73 秒、全体の所要時間は 3.62 秒です (パフォーマンスをオンにすると遅くなります)
-
時間のかかる主なタスクは 2 つあります: 紫色の小さなブロックは render のレンダリングに時間がかかり、緑色の小さなブロックはパッチの比較に時間がかかります. 一般に、パッチは Vue の内部ロジックであり、これはより困難です.最適化
-
レンダリングにかかる時間を見ると、getColspanRealWidth に 212.2ms かかっていることがわかり、ここには最適化の余地があります。
switch-performance-test.png
この関数が時間のかかる理由を見てみましょう。主に、各列の幅を計算するために tr がレンダリングされるときに呼び出されるためです。
// src\table\src\table-body\render-helper.ts
columns.value.map((column, cellIndex) => {
// ...
columnData.realWidth = getColspanRealWidth(
columns.value,
colspan,
cellIndex
);
// ...
})
具体的な実装は次のとおりです。realWidth 属性と width 属性のみが使用され、column.value はレスポンシブな依存関係であり、レスポンシブでないデータに変更して時間を短縮できるかどうかを確認できます。
// 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. 完全なコードについては、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
);
// ...
})
時間のかかる作業を 200ms から 0.7ms に短縮
修正後、再度パフォーマンスをテストしたところ、この関数の時間が 200 ミリ秒以上から 1 ミリ秒以内に短縮され、レンダリング パフォーマンスが大幅に向上したことに驚きました。1.54 秒 + 1.45 秒 = 2.99 秒
getColspanRealWidth-optimize.png
パフォーマンス データ (所要時間の 7 ~ 20% の削減)
テーブルのレンダリングとスイッチの切り替えテストの所要時間は次のとおりです。
get-width-optimize-perf.png
表を非表示にして gif を表示
get-width-optimize-table.gif
gifをオフからオンに切り替えます
get-width-optimize-switch.gif
ビジネス最適化ツールチップの無効化が if に変更されました
上記の最適化の後、わずかなレスポンシブ データの最適化でさえ、パフォーマンスに大きな影響を与える可能性があることに気付きました。そのようなデータはビジネスロジックにも存在しますか?
そのため、コメント + el-table-column スロットを静的ノードに置き換える方法を使用して、 <span>123</span>
時間がかかる場所をテストし、それに応じて最適化します。
テストの結果、カスタム列の el-tooltip を静的ノードに置き換えた後、パフォーマンスが大幅に向上することがわかりました。
<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>
下図に示すように、スイッチの切り替え時間が約 2.7 秒から約 0.5 秒に短縮されています。パフォーマンス パネルで、パッチが基本的になくなっていることがわかります.テンプレートがコンパイルされた後、静的ノードがマークされ、更新時に比較する必要がないはずです.
tooltip-static-node-test.png
この考えに基づいて、el-tooltip コンポーネントは時間のかかるパッチ比較を 2 倍にし、ノード数を減らすことでパフォーマンスを向上させることができます。
In order to save some code, el-tooltip uses the disabled attribute to hide the tooltip in a specific scenario. データのこの部分では、el-tooltip ノードを使用する必要はありません. 変更点は次のとおりです. v-if を使用して無効化された属性関数を置き換えますが、コードの繰り返しがありますが、ノードの数を減らすことができます。
<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>
再度性能をテストしてみると、性能はあまり落ちていないことがわかり、約0.5秒でスイッチの切り替えがリフレッシュできます
tooltip-optimize.png
パフォーマンスデータ (80% の時間消費の削減)
テーブルのレンダリングとスイッチの切り替えテストの所要時間は次のとおりです。
tooltip-optimize-pref.png
表を非表示にして gif を表示
tooltip-optimize-table.gif
gifをオフからオンに切り替えます
tooltip-optimize-switch.gif
要約する
下の図に示すように、3 つの細部の変更により、テーブルのレンダリング時間が 6.88 秒から約 1 秒に短縮され、レンダリング時間が平均 85% 短縮され、ユーザー エクスペリエンスは基本的に期待に応えます。完全なデモ github アドレス: github.com/zuoxiaobai/…[8]
pref-summary.png
vue3 プロジェクトでは、レスポンシブ データに特別な注意を払う必要があります。遅いシーンに遭遇した場合は、パフォーマンスの最適化のために次の方法を使用することをお勧めします
-
パフォーマンスを使用してパフォーマンスのボトルネックを分析するか、パフォーマンスに時間がかかるロジックを自分で作成して、パフォーマンスの最適化を行うときにデータを参照できるようにします。
-
業務コードが多いシナリオでは、コメント+静的ノード置換という手法で、時間のかかるロジックをトラブルシューティングし、的を絞った最適化を行います。
-
さらに、Vue devtools デバッグ ツールを使用して、時間のかかるコンポーネント更新レンダリングを表示し、レスポンシブ データの問題をトラブルシューティングできます。
ワン・オペレーションを参照してください。テーブル・コンポーネントのパフォーマンスを 10 倍に改善しました [9]