前端开发中如何高效渲染大数据量
<blockquote><p>我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。</p>
</blockquote>
<blockquote>
<p>本文作者:琉易 liuxianyu.cn</p>
</blockquote>
<p> 在日常工作中,较少的能遇到一次性往页面中插入大量数据的场景,数栈的离线开发(以下简称离线)产品中,就有类似的场景。本文将分享一个实际场景中的前端开发思路,实现高效的数据渲染,提升页面性能和用户体验。</p>
<h3 id="一场景介绍">一、场景介绍</h3>
<p> 在离线的数据开发模块,用户可以在 sql 编辑器中编写 sql,再通过 <code>整段运行/分段运行</code> 来执行 sql。在点击 <code>整段运行</code> 后,运行成功日志打印后到展示结果的过程中,有一段时间页面很卡顿,主要表现为编辑器编写卡顿。</p>
<p><img src="https://img2023.cnblogs.com/other/2332333/202309/2332333-20230908171837166-952893346.gif"></p>
<h3 id="二性能问题">二、性能问题</h3>
<p> 我们是在解决 <code>sql 最大运行行数</code> 问题时,发现了上述需要进行性能优化的场景。<br>
先来梳理下当前代码的设计逻辑:<br>
<img src="https://img2023.cnblogs.com/other/2332333/202309/2332333-20230908171837503-180121138.png"></p>
<ul>
<li>前端将选中的 sql 传递给服务端,服务端返回一个调度运行的 jobId;</li>
<li>前端接着以该 jobId 轮询服务端,查询任务的执行状态;</li>
<li>当轮询到任务已完成时,选中的 sql 中如果有查询语句,服务端则会按 select 语句的顺序返回一个 sqlId 的数组集合;</li>
<li>前端基于 n 个 sqlId 的集合,并发 n 个 <code>selectData</code> 的请求;</li>
<li>所有的 <code>selectData</code> 请求完成后渲染数据;</li>
</ul>
<blockquote>
<p>为了保证结果最终的展示顺序和 select 语句顺序一致,我们为单纯的 sqlIdList 循环方法加上了 Promise.allsettled 的方法,使得 n 个 selectData 的请求顺序和 select 语句顺序一致。</p>
</blockquote>
<p><img src="https://img2023.cnblogs.com/other/2332333/202309/2332333-20230908171837681-969688222.png"></p>
<p> 由上述逻辑可以看出,问题可能出现在如果选中的 sql 中有大量 select 语句的话,会在「整段运行」完成后大批量请求 <code>selectData</code> 接口,再等待所有 <code>selectData</code> 请求完成后,集中进行渲染。此时,就会出现一次性往页面中插入大量数据的场景。那么,我们怎么解决上述问题呢?</p>
<h3 id="三解决思路">三、解决思路</h3>
<p> 可以看出,上述逻辑主要有两个问题:</p>
<ul>
<li>1、大批量请求 <code>selectData</code> 接口;</li>
<li>2、集中性数据渲染。</li>
</ul>
<h4 id="1任务分组">1、任务分组</h4>
<p> 依旧通过 <code>Promise.allsettled</code> 拿到所有 <code>selectData</code> 接口返回的结果,将原先集中渲染看作是一个大任务,我们将任务拆分成单个的 <code>selectData</code> 结果渲染任务;再根据实际情况,对单个任务进行分组,比如两个一组,渲染完一组再渲染下一组。<br>
拆分完任务,就涉及到了任务的优先级问题,优先级决定了哪个任务先执行。这里采用最原始的“抢占式轮转”,按 <code>sqlIdList</code> 的顺序保留编辑器中的 sql 顺序。</p>
<pre><code class="language-javascript">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?.sqlId);
} else {
console.error(
'selectExecResultDataList Promise.allSettled rejected',
item.reason
);
}
});
setTimeout(() => {
loop(idx + renderOnce);
}, 100);
};
loop(0);
});
</code></pre>
<h4 id="2请求分组--任务分组">2、请求分组 + 任务分组</h4>
<p> 问题1 中的大批量请求 <code>selectData</code> 接口,也是一个突破点。我们可以将请求进行分组,每次以固定数量的 sqlId 去请求 <code>selectData</code> 接口,比如每组请求 6 个 sqlId 的结果,当前组的请求全部结束后再进行渲染;为了保证效果最优,这里也引入任务分组的思路。</p>
<pre><code class="language-javascript">const requestOnce = 6; // 每组请求的数量
// 将一维数组转换成二维数组
const sqlIdList2D = convertTo2DArray(sqlIdList, requestOnce);
const idx2D = 0; // sqlIdList2D 的索引
const requestLoop = (index) => {
if (!sqlIdList2D) return;
const promiseList = sqlIdList2D.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?.sqlId);
} else {
console.error(
'selectExecResultDataList Promise.allSettled rejected',
item.reason
);
}
});
setTimeout(() => {
loop(idx + renderOnce);
}, 100);
};
loop(0);
})
.finally(() => {
requestLoop(index + 1);
});
};
requestLoop(idx2D);
</code></pre>
<h4 id="3请求分组">3、请求分组</h4>
<p> 上一种方案的代码写出来太难以理解了,属于上午写,下午忘的逻辑,注释也不好写,不利于维护。基于实际情况,我们尝试下仅对请求作分组处理,看看效果。</p>
<pre><code class="language-javascript">const requestOnce = 3; // 每组请求的数量
// 将一维数组转换成二维数组
const sqlIdList2D = convertTo2DArray(sqlIdList, requestOnce);
const idx2D = 0; // sqlIdList2D 的索引
const requestLoop = (index) => {
if (!sqlIdList2D) return;
const promiseList = sqlIdList2D.map((item) =>
selectExecResultData(item?.sqlId)
);
Promise.allSettled(promiseList)
.then((results = []) => {
results.forEach((item, idx) => {
if (item.status === 'fulfilled') {
handleResultData(item?.value || {}, sqlIdList?.sqlId);
} else {
console.error(
'selectExecResultDataList Promise.allSettled rejected',
item.reason
);
}
});
})
.finally(() => {
requestLoop(index + 1);
});
};
requestLoop(idx2D);
</code></pre>
<p><img src="https://img2023.cnblogs.com/other/2332333/202309/2332333-20230908171838249-709103049.gif"></p>
<h3 id="四思路理解">四、思路理解</h3>
<p>1、解决大数据量渲染的问题,常见方法有:时间分片、虚拟列表等;<br>
2、解决同步阻塞的问题,常见方法有:任务分解、异步等;<br>
3、如果某个任务执行时间较长的话,从优化的角度,我们通常会考虑将该任务分解成一系列的子任务。</p>
<p> 在任务分组一节,我们将 setTimeout 的时间间隔设置为 100ms,也就是我认为最快在 100ms 内能完成渲染;但假设不到 100ms 就完成了渲染,那么就需要白白等待一段时间,这是没有必要的。这时可以考虑window.requestAnimationFrame 方法。</p>
<pre><code class="language-diff">- setTimeout(() => {
+ window.requestAnimationFrame(() => {
loop(idx + renderOnce);
- }, 100);
+ });
</code></pre>
<p> 第三节的请求分组,实际上达到了渲染任务分组的效果。本文更多的是提供一个解决思路,上述方式也是基于对时间分片的理解实践。</p>
<h3 id="五写在最后">五、写在最后</h3>
<p> 在软件开发中,性能优化是一个重要的方面,但并不是唯一追求,往往还需要考虑多个因素,包括功能需求、可维护性、安全性等等。根据具体情况,综合使用多种技术和策略,以找到最佳的解决方案。</p>
<hr>
<h2 id="最后">最后</h2>
<p>欢迎关注【袋鼠云数栈UED团队】~<br>
袋鼠云数栈UED团队持续为广大开发者分享技术成果,相继参与开源了欢迎star</p>
<ul>
<li><strong>大数据分布式任务调度系统——Taier</strong></li>
<li><strong>轻量级的 Web IDE UI 框架——Molecule</strong></li>
<li><strong>针对大数据领域的 SQL Parser 项目——dt-sql-parser</strong></li>
<li><strong>袋鼠云数栈前端团队代码评审工程实践文档——code-review-practices</strong></li>
<li><strong>一个速度更快、配置更灵活、使用更简单的模块打包器——ko</strong></li>
</ul><br><br>
来源:https://www.cnblogs.com/dtux/p/17688135.html
頁:
[1]