<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Vue3 Table 性能優(yōu)化,減少 85% 渲染耗時

          共 16856字,需瀏覽 34分鐘

           ·

          2023-02-10 21:22

          大廠技術(shù)  高級前端  Node進階

          點擊上方 程序員成長指北,關(guān)注公眾號

          回復(fù)1,加入高級Node交流群

          原文鏈接: https://juejin.cn/post/7194516447932973112

          作者:dev_zuo

          前段時間公司有一個比較重要的模塊從 vue2 升級到 vue3,升級后發(fā)現(xiàn) element-plus table 的性能相比 vue2 版本下降非常嚴重

          自定義列全部勾選的場景下(20 行 x 180 列),列表中的開關(guān)切換,耗時從原先的 400-500 毫秒下降到 7-8 秒,嚴重影響用戶體驗,經(jīng)過較長時間的性能測試、debug,找到了幾處比較核心的優(yōu)化點。

          先來看一下 20 行 x 180 列場景下各個優(yōu)化點的性能測試數(shù)據(jù),為排除偶然性,每個場景都會測 3 次。

          優(yōu)化類型table 整體渲染耗時switch 切換耗時
          未優(yōu)化前6.59s(6.71s、6.49s、6.577s)3.982s(3.966s、3.947s、4.033s)
          data 與 columns 從 ref 改 shallowRef 后(耗時減少 17-20%)5.18s(5.063s、5.104s、5.363s)3.3s(3.175s、3.029s、3.122s)
          getColspanRealWidth 優(yōu)化后(耗時減少 7-20%)4.843(4.728s、4.703s 、5.098s)2.65s(2.636s、2.645s、2.671s)
          業(yè)務(wù)優(yōu)化 去除tooltip disabled屬性后(耗時減少 80%)1.008s(1.032s、0.997s、0.994s)0.514s(0.517s、0.53s、0.495s)

          大致優(yōu)化內(nèi)容如下

          • 修改 table 源碼,將 data 與 columns 從 ref 改為 shallowRef。
          • 修改 table 源碼,getColspanRealWidth 函數(shù)中響應(yīng)式數(shù)據(jù)優(yōu)化。
          • 業(yè)務(wù)優(yōu)化:去掉 el-tooltip disabled 屬性,改為 if。

          準備工作

          首先初始化一個 vue3 項目,引入 element-plus,并使用 el-table 實現(xiàn)一個 20 行 * 180 列表格。

          • 20 行 + 180 列:2 個固定列(一個文本、一個 switch),178 個通過 for 循環(huán)創(chuàng)建的自定義列
          • 一個顯示/隱藏 table 的 switch 開關(guān),用于測試 table 從隱藏到顯示,渲染耗時
          • 自定義列中有一個 el-tooltip + disabled 邏輯
          1-table-base.png

          最小化業(yè)務(wù) demo 創(chuàng)建

          核心 table 代碼代碼如下,完整代碼參見: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="狀態(tài)" 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>
          // 假數(shù)據(jù)邏輯
          const customColCount = 178// 自定義列數(shù)
          const rowCount = 20// 行數(shù)
          onBeforeMount(() => {
            // 初始化自定義列數(shù)據(jù)
            let temp = [];
            for (let i = 0; i < customColCount; i++) {
              temp.push({ prop`column${i + 1}`label`第${i + 1}列` });
            }
            customColumns.value = temp;

            // 初始化表格數(shù)據(jù)
            let dataTemp = [];
            for (let i = 0; i < rowCount; i++) {
              let row: any = { info`第${i + 1}行`statustrue };
              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>

          渲染耗時計算邏輯

          渲染耗時計算邏輯如下,利用 script 阻塞,來計算渲染耗時

          /*
          <div v-loading="showLoading" element-loading-text="數(shù)據(jù)加載中...">
            <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 合并到一個渲染周期,導(dǎo)致 loading 不顯示
            setTimeout(() => {
              let startTime = +new Date();
              showTable.value = !showTable.value; // 修改 table 顯示,會形成 script 阻塞
              showLoading.value = false// 這里的 loading 關(guān)閉,會在 table 阻塞完成后渲染關(guān)閉 dom
              // 創(chuàng)建一個宏任務(wù),等上面阻塞的微任務(wù)執(zhí)行完成后,再顯示計算耗時
              setTimeout(() => {
                let endTime = +new Date();
                ElMessage.success(`渲染耗時:${(endTime - startTime) / 1000}s`);
              }, 0);
            }, 200);
          };

          性能數(shù)據(jù),與 performance 耗時對比

          table 渲染、switch 切換測試耗時如下

          table-base-duration.png

          table 隱藏到顯示 gif 圖

          table-base-6-8-s.gif

          switch 從關(guān)到開 gif 圖

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

          為了驗證我們自己寫的耗時測試數(shù)據(jù)的準確性,這里在 switch 開關(guān)時,打開了 performance 錄制,具體如下圖

          頁面顯示渲染耗時:4.524s,performance 中兩個 Long Task:2.29s + 2.17,加上非 Long Task 部分,數(shù)據(jù)基本一致,因此我們自己寫的耗時計算邏輯是基本準確的

          table-base-switch-performance.gif

          另外,開啟 performance 錄制時,比不錄制時要稍微慢點。下面來開始優(yōu)化吧!

          ref 改 shallowRef

          理論依據(jù)與可行性分析

          列表中的開關(guān)切換時,table 雖然只是一個節(jié)點發(fā)生了變化,但依舊觸發(fā)了完整的 vue patch 比對更新邏輯,耗時較久。

          來看一個官方的解釋:渲染機制 | Vue.js[2]

          vue-render-logic.png

          理論上,減少響應(yīng)式數(shù)據(jù)依賴,就可以提升性能。

          shallowRef() 是 ref() 的淺層作用形式。僅當 xx.value 發(fā)生變更時,才觸發(fā)響應(yīng)更新,減少深層次的響應(yīng)依賴,可以提升 patch 比對性能。參考 指南 - 減少大型不可變結(jié)構(gòu)的響應(yīng)性開銷[3]

          const state = shallowRef({ count1 })

          // shallowRef 不會觸發(fā)更改,如果 state 為 ref 時,是可以觸發(fā)更新的。
          state.value.count = 2

          // shallowRef 會觸發(fā)更改
          state.value = { count2 }

          這里主要修改兩種數(shù)據(jù)從 ref 到 shallowRef

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

          這里有個問題,把 data、columns 改為 shallowRef 對功能會不會有影響?

          • 首選,每次列表數(shù)據(jù)更新,我們業(yè)務(wù)邏輯都會去請求列表,設(shè)置 list.value = xxx 可以觸發(fā) shallowRef 更新。
          • 經(jīng)過測試,就算是 switch 開關(guān) v-model 綁定的 scope.row.status 變更也可以正常更新。
          • 手動點擊測試選中、排序、分頁等均未發(fā)現(xiàn)異常。

          基于以上三點,在我們業(yè)務(wù)中,這個修改是可行的。提醒:如果想在你自己的項目中使用該優(yōu)化,需要先做好測試。

          下面來看具體修改細節(jié)

          拷貝 element-plus table 源碼到當前項目

          當前最新的版本是 2.2.8,打開 element-plus/releases[4],下載最新版本代碼,將 table 目錄(element-plus-2.2.28/packages/components/table) copy 到項目中的 src/table 下,刪除目中無用的 __test__ 測試目錄

          新開一個路由,/new 指定到一個新增的 table 組件內(nèi),相比原先 table 組件,只增加一行代碼,當前組件內(nèi)使用我們自定義修改的 table。完整代碼參見: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

          做一些修改,讓代碼可以在我們自己的項目中跑起來,方便修改、調(diào)試源碼

          1. 在 table 目錄中搜索 @element-plus 相關(guān)關(guān)鍵字,并進行批量替換
          // @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. 搜索 @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 改 shallowRef

          在 src/table/src/store/watcher.ts 中,將 data 和 columns 數(shù)據(jù)從 ref 改為 shallowRef,具體代碼參: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([]);
            // ...
          }

          另外在 中 表格前面增加下面一行,標記調(diào)用的是我們修改的 table 組件

          <!-- src/table/src/table.vue 表格頂部增加下面一行 --->
          <p style="color: red">來自 table 源碼</p>
          <!-- 內(nèi)部邏輯 -->
          <div :class="ns.e('inner-wrapper')" :style="tableInnerStyle">
              <!-- ... -->
          </div>

          性能數(shù)據(jù)(耗時減少17-20%)

          table 渲染、switch 切換測試耗時如下

          table-ref-shallow-ref-duration.png

          table 隱藏到顯示 gif 圖

          table-ref-shallowRef.gif

          switch 從關(guān)到開 gif 圖

          table-ref-shallowRef-switch.gif

          getColspanRealWidth 優(yōu)化

          當頁面卡頓時,可以通過 performance 測試性能。下圖是點擊 switch 開關(guān)后的性能數(shù)據(jù)。可以看到

          • 有兩個 Scripting 阻塞 longTask,1.89s + 1.73s,整體耗時 3.62s (performance開啟時,會變慢一點)
          • 主要有兩種耗時任務(wù):紫色小塊是 render 渲染耗時、綠色小塊是 patch 比對耗時,一般 patch 是 vue 內(nèi)部邏輯,比較難優(yōu)化
          • 通過查看 render 相關(guān)耗時,找到 getColspanRealWidth 耗時 212.2ms,這里有優(yōu)化的空間
          switch-performance-test.png

          我們來查看這個函數(shù)耗時的原因,主要是在 tr 渲染時調(diào)用該函數(shù),計算每列的寬度

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

          具體實現(xiàn)如下,只用到了 realWidth, width 屬性,且 columns.value 是響應(yīng)式依賴,可以修改為非響應(yīng)式數(shù)據(jù),看是否能減少耗時。

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

          這里我們新建 optimizeColumns 變量,存儲函數(shù)中使用的 realWidth 和 width,將這個非響應(yīng)式數(shù)據(jù)傳入到 getColspanRealWidth 函數(shù)內(nèi)部使用,完整代碼參見 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, // 傳入函數(shù)內(nèi)部時,使用非響應(yīng)式數(shù)據(jù)
          colspan,
          cellIndex
          );
          // ...
          })

          耗時從 200ms 下降到 0.7ms

          修改好后再次測試性能,驚喜的發(fā)現(xiàn),這個函數(shù)的耗時從 200ms+ 下降到 1ms 內(nèi),render 性能明顯提升。1.54s + 1.45s = 2.99s

          getColspanRealWidth-optimize.png

          性能數(shù)據(jù)(耗時減少7-20%)

          table 渲染、switch 切換測試耗時如下

          get-width-optimize-perf.png

          table 隱藏到顯示 gif 圖

          get-width-optimize-table.gif

          switch 從關(guān)到開 gif 圖

          get-width-optimize-switch.gif

          業(yè)務(wù)優(yōu)化 tooltip disabled 改 if

          經(jīng)過上面的優(yōu)化后,我們意識到,即使是很細微的響應(yīng)式數(shù)據(jù)優(yōu)化,也會對性能帶來較大影響。那業(yè)務(wù)邏輯中是否也存在這樣的數(shù)據(jù)呢?

          于是采用注釋 + 將 el-table-column 插槽換成靜態(tài)節(jié)點 <span>123</span> 的方法,測試具體是哪里耗時較長,然后針對性優(yōu)化

          經(jīng)過測試,發(fā)現(xiàn)將自定義列中的 el-tooltip 換成靜態(tài)節(jié)點后,性能有極大提升。

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

          如下圖,switch 開關(guān)切換耗時從 2.7s 左右減少到 0.5s 左右。performance 面板可以看到 patch 基本沒有了,應(yīng)該是模板編譯時靜態(tài)節(jié)點標記后,更新時就不用比對了。

          tooltip-static-node-test.png

          基于這個思路,el-tooltip 組件會成倍的增加 patch 比對耗時,減少這個節(jié)點數(shù)量即可增強性能。

          為了少些一些代碼,el-tooltip 使用 disabled 屬性,用于在特定場景下隱藏 tooltip,這一部分數(shù)據(jù)可以不使用 el-tooltip 節(jié)點,改動如下,使用 v-if 替換 disabled 屬性功能,這樣雖然會有重復(fù)代碼,但可以減少節(jié)點數(shù)。

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

          再次測試性能,可以看到性能并沒有下降多少,switch 開關(guān)切換可以做到 0.5s 左右刷新

          tooltip-optimize.png

          性能數(shù)據(jù)(耗時減少80%)

          table 渲染、switch 切換測試耗時如下

          tooltip-optimize-pref.png

          table 隱藏到顯示 gif 圖

          tooltip-optimize-table.gif

          switch 從關(guān)到開 gif 圖

          tooltip-optimize-switch.gif

          總結(jié)

          如下圖,通過 3 個小的細節(jié)改動,將 table 渲染耗時從 6.88s 減少到 1s 左右,平均減少 85% 渲染耗時,用戶體驗基本達到預(yù)期。完整 demo github 地址:github.com/zuoxiaobai/…[8]

          pref-summary.png

          在 vue3 項目中,響應(yīng)式數(shù)據(jù)這塊要特別注意。當遇到比較慢的場景時,建議采用如下方法進行性能優(yōu)化

          • 使用 performance 分析性能瓶頸,或者自己寫一個性能耗時邏輯,這樣在做性能優(yōu)化時有數(shù)據(jù)參考。
          • 針對業(yè)務(wù)代碼較多場景,采用注釋 + 替換成靜態(tài)節(jié)點方法排查耗時較長的邏輯,針對性優(yōu)化。
          • 另外,可以使用 Vue devtools 調(diào)試工具,查看組件更新渲染耗時,排查響應(yīng)式數(shù)據(jù)問題。
          Node 社群


          我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學(xué)習(xí)感興趣的話(后續(xù)有計劃也可以),我們可以一起進行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。

             “分享、點贊在看” 支持一波??

          參考

          • 一頓操作,我把 Table 組件性能提升了十倍[9]

          參考資料

          [1]

          https://github.com/zuoxiaobai/table-performance-demo/tree/table-base: https://github.com/zuoxiaobai/table-performance-demo/tree/table-base

          [2]

          https://cn.vuejs.org/guide/extras/rendering-mechanism.html#compiler-informed-virtual-dom: https://cn.vuejs.org/guide/extras/rendering-mechanism.html#compiler-informed-virtual-dom

          [3]

          https://cn.vuejs.org/guide/best-practices/performance.html#reduce-reactivity-overhead-for-large-immutable-structures: https://cn.vuejs.org/guide/best-practices/performance.html#reduce-reactivity-overhead-for-large-immutable-structures

          [4]

          https://github.com/element-plus/element-plus/releases: https://github.com/element-plus/element-plus/releases

          [5]

          https://github.com/zuoxiaobai/table-performance-demo/tree/2-table-use-source: https://github.com/zuoxiaobai/table-performance-demo/tree/2-table-use-source

          [6]

          https://github.com/zuoxiaobai/table-performance-demo/tree/table-ref-shallowRef: https://github.com/zuoxiaobai/table-performance-demo/tree/table-ref-shallowRef

          [7]

          https://github.com/zuoxiaobai/table-performance-demo/tree/getColspanRealWidth-optimize: https://github.com/zuoxiaobai/table-performance-demo/tree/getColspanRealWidth-optimize

          [8]

          https://github.com/zuoxiaobai/table-performance-demo: https://github.com/zuoxiaobai/table-performance-demo

          [9]

          https://mp.weixin.qq.com/s/Zj6h6szd4JTvYb5noYwxgQ#: https://mp.weixin.qq.com/s/Zj6h6szd4JTvYb5noYwxgQ#

          瀏覽 44
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  1000部无码操逼视频 | 97资源超碰 | 调教女M屁股撅虐调教 | 国产一区视频在线 | 豆花视频成人网址 |