我們是袋鼠雲數棧 UED 團隊,致力於打造優秀的一站式資料中臺產品。我們始終保持工匠精神,探索前端道路,為社群積累並傳播經驗價值。
本文作者:琉易 liuxianyu.cn
在日常工作中,較少的能遇到一次性往頁面中插入大量資料的場景,數棧的離線開發(以下簡稱離線)產品中,就有類似的場景。本文將分享一個實際場景中的前端開發思路,實現高效的資料渲染,提升頁面效能和使用者體驗。
在離線的資料開發模組,使用者可以在 sql 編輯器中編寫 sql,再通過 整段執行/分段執行
來執行 sql。在點選 整段執行
後,執行成功紀錄檔列印後到展示結果的過程中,有一段時間頁面很卡頓,主要表現為編輯器編寫卡頓。
我們是在解決 sql 最大執行行數
問題時,發現了上述需要進行效能優化的場景。
先來梳理下當前程式碼的設計邏輯:
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);
});
問題1 中的大批次請求 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);
1、解決巨量資料量渲染的問題,常見方法有:時間分片、虛擬列表等;
2、解決同步阻塞的問題,常見方法有:任務分解、非同步等;
3、如果某個任務執行時間較長的話,從優化的角度,我們通常會考慮將該任務分解成一系列的子任務。
在任務分組一節,我們將 setTimeout 的時間間隔設定為 100ms,也就是我認為最快在 100ms 內能完成渲染;但假設不到 100ms 就完成了渲染,那麼就需要白白等待一段時間,這是沒有必要的。這時可以考慮window.requestAnimationFrame 方法。
- setTimeout(() => {
+ window.requestAnimationFrame(() => {
loop(idx + renderOnce);
- }, 100);
+ });
第三節的請求分組,實際上達到了渲染任務分組的效果。本文更多的是提供一個解決思路,上述方式也是基於對時間分片的理解實踐。
在軟體開發中,效能優化是一個重要的方面,但並不是唯一追求,往往還需要考慮多個因素,包括功能需求、可維護性、安全性等等。根據具體情況,綜合使用多種技術和策略,以找到最佳的解決方案。
歡迎關注【袋鼠雲數棧UED團隊】~
袋鼠雲數棧UED團隊持續為廣大開發者分享技術成果,相繼參與開源了歡迎star