在日常工作中,有時會遇到一次性往頁面中插入大量數據的場景,在數棧的[離線開發](https://www.dtstack.com/dtinsight/batchworks?src=szsm)(以下簡稱離線)產品中,就有類似的場景。本文將通過分享一個實際場景中的[前端開發](https://www.dt ...
在日常工作中,有時會遇到一次性往頁面中插入大量數據的場景,在數棧的離線開發(以下簡稱離線)產品中,就有類似的場景。本文將通過分享一個實際場景中的前端開發思路,介紹當遇到大量數據時,如何實現高效的數據渲染,以達到提升頁面性能和用戶體驗的目的。
渲染大數據量時遇到的問題
在離線的數據開發模塊,用戶可以在 SQL 編輯器中編寫 SQL,再通過整段運行/分段運行來執行 SQL。在點擊整段運行後,從運行成功日誌列印後到展示結果的過程中,有一段時間頁面會很卡頓,主要表現為編輯器編寫卡頓。
我們是在解決 SQL 最大運行行數問題時,發現了上述需要進行性能優化的場景。
先來梳理下當前代碼的設計邏輯:
· 前端將選中的 SQL 傳遞給服務端,服務端返回一個調度運行的 jobId
· 前端接著以該 jobId 輪詢服務端,查詢任務的執行狀態
· 當輪詢到任務已完成時,選中的 SQL 中如果有查詢語句,服務端則會按 select 語句的順序返回一個 sqlId 的數組集合
· 前端基於n個 sqlId 的集合,併發 n個 selectData 的請求
· 所有的 selectData 請求完成後渲染數據
為了保證結果最終的展示順序和 select 語句順序一致,我們為單純的 sqlIdList 迴圈方法加上了 Promise.allsettled 的方法,使得n個 selectData 的請求順序和 select 語句順序一致。
由上述邏輯可以看出,問題可能出現在如果選中的 SQL 中有大量 select 語句的話,會在「整段運行」完成後大批量請求 selectData 介面,再等待所有 selectData 請求完成後,集中進行渲染。此時,就會出現一次性往頁面中插入大量數據的場景,導致卡頓。那麼,我們怎麼解決上述問題呢?
解決思路
可以看出,上述邏輯主要有兩個問題:大批量請求 selectData 介面和集中性數據渲染。我們通過如下所示的解決思路去處理這些問題。
任務分組
依舊通過 Promise.allsettled 拿到所有 selectData 介面返回的結果,將原先集中渲染看作是一個大任務,我們將任務拆分成單個的 selectData 結果渲染任務。再根據實際情況,對單個任務進行分組,比如兩個一組,渲染完一組再渲染下一組。
拆分完任務,就涉及到了任務的優先順序問題,優先順序決定了哪個任務先執行。這裡採用最原始的“搶占式輪轉”,按 sqlIdList 的順序保留編輯器中的 SQL 順序。
Promise.allSettled(promiseList).then((results = []) => {
const renderOnce = 2; // 每組渲染的結果 tab 數量
const loop = (idx) => {
if (promiseList.length <= idx) return;
results.slice(idx, idx + renderOnce).forEach((item, idx) => {
if (item.status === 'fulfilled') {
handleResultData(item?.value || {}, sqlIdList[idx]?.sqlId);
} else {
console.error(
'selectExecResultDataList Promise.allSettled rejected',
item.reason
);
}
});
setTimeout(() => {
loop(idx + renderOnce);
}, 100);
};
loop(0);
});
請求分組 + 任務分組
問題中的大批量請求 selectData 介面,也是一個突破點。我們可以將請求進行分組,每次以固定數量的 sqlId 去請求 selectData 介面,比如每組請求 6 個 sqlId 的結果,當前組的請求全部結束後再進行渲染。為了保證效果最優,這裡也引入任務分組的思路。
const requestOnce = 6; // 每組請求的數量
// 將一維數組轉換成二維數組
const sqlIdList2D = convertTo2DArray(sqlIdList, requestOnce);
const idx2D = 0; // sqlIdList2D 的索引
const requestLoop = (index) => {
if (!sqlIdList2D[index]) return;
const promiseList = sqlIdList2D[index].map((item) =>
selectExecResultData(item?.sqlId)
);
Promise.allSettled(promiseList)
.then((results = []) => {
const renderOnce = 2; // 每組渲染的結果 tab 數量
const loop = (idx) => {
if (promiseList.length <= idx) return;
results.slice(idx, idx + renderOnce).forEach((item, idx) => {
if (item.status === 'fulfilled') {
handleResultData(item?.value || {}, sqlIdList[idx]?.sqlId);
} else {
console.error(
'selectExecResultDataList Promise.allSettled rejected',
item.reason
);
}
});
setTimeout(() => {
loop(idx + renderOnce);
}, 100);
};
loop(0);
})
.finally(() => {
requestLoop(index + 1);
});
};
requestLoop(idx2D);
請求分組
上一種方案的代碼相對來說又些難以理解,屬於上午寫,下午忘的邏輯,註釋也不好寫,不利於維護。基於實際情況,我們嘗試下僅對請求作分組處理,看看效果。
const requestOnce = 3; // 每組請求的數量
// 將一維數組轉換成二維數組
const sqlIdList2D = convertTo2DArray(sqlIdList, requestOnce);
const idx2D = 0; // sqlIdList2D 的索引
const requestLoop = (index) => {
if (!sqlIdList2D[index]) return;
const promiseList = sqlIdList2D[index].map((item) =>
selectExecResultData(item?.sqlId)
);
Promise.allSettled(promiseList)
.then((results = []) => {
results.forEach((item, idx) => {
if (item.status === 'fulfilled') {
handleResultData(item?.value || {}, sqlIdList[idx]?.sqlId);
} else {
console.error(
'selectExecResultDataList Promise.allSettled rejected',
item.reason
);
}
});
})
.finally(() => {
requestLoop(index + 1);
});
};
requestLoop(idx2D);
解決思路解析
· 解決大數據量渲染的問題,常見方法有:時間分片、虛擬列表等
· 解決同步阻塞的問題,常見方法有:任務分解、非同步等
· 如果某個任務執行時間較長的話,從優化的角度,我們通常會考慮將該任務分解成一系列的子任務
在任務分組一節,我們將 setTimeout 的時間間隔設置為 100ms,也就是我認為最快在 100ms 內能完成渲染。但假設不到 100ms 就完成了渲染,那麼就需要白白等待一段時間,這是沒有必要的,這時可以考慮 window.requestAnimationFrame 方法。
- setTimeout(() => {
+ window.requestAnimationFrame(() => {
loop(idx + renderOnce);
- }, 100);
+ });
第三節的請求分組,實際上已經達到了渲染任務分組的效果。本文更多的是提供一個解決思路,上述方式也是基於對時間分片的理解實踐。
在軟體開發中,性能優化是一個重要的方面,但並不是唯一追求,往往還需要考慮多個因素,包括功能需求、可維護性、安全性等等。根據具體情況,綜合使用多種技術和策略,找到最佳的解決方案,才是最終目的。
《數棧產品白皮書》:https://www.dtstack.com/resources/1004?src=szsm
《數據治理行業實踐白皮書》下載地址:https://www.dtstack.com/resources/1001?src=szsm
想瞭解或咨詢更多有關袋鼠雲大數據產品、行業解決方案、客戶案例的朋友,瀏覽袋鼠雲官網:https://www.dtstack.com/?src=szbky
同時,歡迎對大數據開源項目有興趣的同學加入「袋鼠雲開源框架釘釘技術qun」,交流最新開源技術信息,qun號碼:30537511,項目地址:https://github.com/DTStack