• 【面试题】前端开发中如何高效渲染大数据量?


    前端面试题库 (面试必备)            推荐:★★★★★

    地址:前端面试题库

    【国庆头像】- 国庆爱国 程序员头像!总有一款适合你!

    在日常工作中,较少的能遇到一次性往页面中插入大量数据的场景,数栈的离线开发(以下简称离线)产品中,就有类似的场景。本文将分享一个实际场景中的前端开发思路,实现高效的数据渲染,提升页面性能和用户体验。

    一、场景介绍

      在离线的数据开发模块,用户可以在 sql 编辑器中编写 sql,再通过 整段运行/分段运行 来执行 sql。在点击 整段运行 后,运行成功日志打印后到展示结果的过程中,有一段时间页面很卡顿,主要表现为编辑器编写卡顿。

    二、性能问题

      我们是在解决 sql 最大运行行数 问题时,发现了上述需要进行性能优化的场景。 先来梳理下当前代码的设计逻辑: 

    file

    • 前端将选中的 sql 传递给服务端,服务端返回一个调度运行的 jobId;
    • 前端接着以该 jobId 轮询服务端,查询任务的执行状态;
    • 当轮询到任务已完成时,选中的 sql 中如果有查询语句,服务端则会按 select 语句的顺序返回一个 sqlId 的数组集合;
    • 前端基于 n 个 sqlId 的集合,并发 n 个 selectData 的请求;
    • 所有的 selectData 请求完成后渲染数据;

    为了保证结果最终的展示顺序和 select 语句顺序一致,我们为单纯的 sqlIdList 循环方法加上了 Promise.allsettled 的方法,使得 n 个 selectData 的请求顺序和 select 语句顺序一致。

    file

      由上述逻辑可以看出,问题可能出现在如果选中的 sql 中有大量 select 语句的话,会在「整段运行」完成后大批量请求 selectData 接口,再等待所有 selectData 请求完成后,集中进行渲染。此时,就会出现一次性往页面中插入大量数据的场景。那么,我们怎么解决上述问题呢?

    三、解决思路

      可以看出,上述逻辑主要有两个问题:

    • 1、大批量请求 selectData 接口;
    • 2、集中性数据渲染。
    1、任务分组

      依旧通过 Promise.allsettled 拿到所有 selectData 接口返回的结果,将原先集中渲染看作是一个大任务,我们将任务拆分成单个的 selectData 结果渲染任务;再根据实际情况,对单个任务进行分组,比如两个一组,渲染完一组再渲染下一组。 拆分完任务,就涉及到了任务的优先级问题,优先级决定了哪个任务先执行。这里采用最原始的“抢占式轮转”,按 sqlIdList 的顺序保留编辑器中的 sql 顺序。

    1. Promise.allSettled(promiseList).then((results = []) => {
    2. const renderOnce = 2; // 每组渲染的结果 tab 数量
    3. const loop = (idx) => {
    4. if (promiseList.length <= idx) return;
    5. results.slice(idx, idx + renderOnce).forEach((item, idx) => {
    6. if (item.status === 'fulfilled') {
    7. handleResultData(item?.value || {}, sqlIdList[idx]?.sqlId);
    8. } else {
    9. console.error(
    10. 'selectExecResultDataList Promise.allSettled rejected',
    11. item.reason
    12. );
    13. }
    14. });
    15. setTimeout(() => {
    16. loop(idx + renderOnce);
    17. }, 100);
    18. };
    19. loop(0);
    20. });
    2、请求分组 + 任务分组

      问题1 中的大批量请求 selectData 接口,也是一个突破点。我们可以将请求进行分组,每次以固定数量的 sqlId 去请求 selectData 接口,比如每组请求 6 个 sqlId 的结果,当前组的请求全部结束后再进行渲染;为了保证效果最优,这里也引入任务分组的思路。

    1. const requestOnce = 6; // 每组请求的数量
    2. // 将一维数组转换成二维数组
    3. const sqlIdList2D = convertTo2DArray(sqlIdList, requestOnce);
    4. const idx2D = 0; // sqlIdList2D 的索引
    5. const requestLoop = (index) => {
    6. if (!sqlIdList2D[index]) return;
    7. const promiseList = sqlIdList2D[index].map((item) =>
    8. selectExecResultData(item?.sqlId)
    9. );
    10. Promise.allSettled(promiseList)
    11. .then((results = []) => {
    12. const renderOnce = 2; // 每组渲染的结果 tab 数量
    13. const loop = (idx) => {
    14. if (promiseList.length <= idx) return;
    15. results.slice(idx, idx + renderOnce).forEach((item, idx) => {
    16. if (item.status === 'fulfilled') {
    17. handleResultData(item?.value || {}, sqlIdList[idx]?.sqlId);
    18. } else {
    19. console.error(
    20. 'selectExecResultDataList Promise.allSettled rejected',
    21. item.reason
    22. );
    23. }
    24. });
    25. setTimeout(() => {
    26. loop(idx + renderOnce);
    27. }, 100);
    28. };
    29. loop(0);
    30. })
    31. .finally(() => {
    32. requestLoop(index + 1);
    33. });
    34. };
    35. requestLoop(idx2D);
    3、请求分组

      上一种方案的代码写出来太难以理解了,属于上午写,下午忘的逻辑,注释也不好写,不利于维护。基于实际情况,我们尝试下仅对请求作分组处理,看看效果。

    1. const requestOnce = 3; // 每组请求的数量
    2. // 将一维数组转换成二维数组
    3. const sqlIdList2D = convertTo2DArray(sqlIdList, requestOnce);
    4. const idx2D = 0; // sqlIdList2D 的索引
    5. const requestLoop = (index) => {
    6. if (!sqlIdList2D[index]) return;
    7. const promiseList = sqlIdList2D[index].map((item) =>
    8. selectExecResultData(item?.sqlId)
    9. );
    10. Promise.allSettled(promiseList)
    11. .then((results = []) => {
    12. results.forEach((item, idx) => {
    13. if (item.status === 'fulfilled') {
    14. handleResultData(item?.value || {}, sqlIdList[idx]?.sqlId);
    15. } else {
    16. console.error(
    17. 'selectExecResultDataList Promise.allSettled rejected',
    18. item.reason
    19. );
    20. }
    21. });
    22. })
    23. .finally(() => {
    24. requestLoop(index + 1);
    25. });
    26. };
    27. requestLoop(idx2D);

    file

    四、思路理解

    1、解决大数据量渲染的问题,常见方法有:时间分片、虚拟列表等;
    2、解决同步阻塞的问题,常见方法有:任务分解、异步等;
    3、如果某个任务执行时间较长的话,从优化的角度,我们通常会考虑将该任务分解成一系列的子任务。

      在任务分组一节,我们将 setTimeout 的时间间隔设置为 100ms,也就是我认为最快在 100ms 内能完成渲染;但假设不到 100ms 就完成了渲染,那么就需要白白等待一段时间,这是没有必要的。这时可以考虑window.requestAnimationFrame 方法。

    1. - setTimeout(() => {
    2. + window.requestAnimationFrame(() => {
    3. loop(idx + renderOnce);
    4. - }, 100);
    5. + });

      第三节的请求分组,实际上达到了渲染任务分组的效果。本文更多的是提供一个解决思路,上述方式也是基于对时间分片的理解实践。

    五、写在最后

      在软件开发中,性能优化是一个重要的方面,但并不是唯一追求,往往还需要考虑多个因素,包括功能需求、可维护性、安全性等等。根据具体情况,综合使用多种技术和策略,以找到最佳的解决方案。

    前端面试题库 (面试必备)            推荐:★★★★★

    地址:前端面试题库

    【国庆头像】- 国庆爱国 程序员头像!总有一款适合你!

  • 相关阅读:
    王道计算机考研 操作系统学习笔记 + 完整思维导图篇章五: IO管理
    Docker 概念构成
    华为云云耀云服务器L实例评测|基于canal缓存自动更新流程 & SpringBoot项目应用案例和源码
    如何删除重复文件?简单操作法方法盘点!
    【C++】常用算术生成算法
    Python基础总结(一)
    机器人控制算法简要概述
    哈希应用之布隆过滤器
    写代码生成流程图
    Unity Mirror学习(一) SyncVars属性使用
  • 原文地址:https://blog.csdn.net/weixin_42981560/article/details/132815711