目录

尝试1.使用table初级实现

动态加载关键代码

效果

尝试2.使用绝对定位优化表格

效果

尝试3.绝对定位+scroll动态加载优化尝试

效果

尝试4. table + 绝对定位 + scroll动态加载

效果

参考资料


大数据表格,就是能够没有分页的情况下,一次展示上万条数据的表格。

若直接渲染上万条数据的,页面会一直卡着,直到浏览器渲染完成后才显示且响应用户操作。

比如加载10000条数据,效果

那么如何做到打开、刷新大数据表格页面的时候能够马上显示用户可见部分的数据,剩下数据在后台慢慢加载呢。

但是理想是美好的,现实是骨感的。

这里就出现了矛盾,由于浏览器渲染线程与JS线程是互斥的,也就是说在渲染页面的时候js就停止执行,js执行时,页面停止渲染。[参考资料1]

所以在web前端中,难以将页面渲染放到“后台”执行(JS的话可以通过Web Workers 另启起一个线程进行复杂计算)

即便是这样,我想到了cpu时间片的概念,打算少量多次进行渲染表格—让出js线程—渲染表格—让出线程—...

下面动手实践:

尝试1.使用table初级实现

<!DOCTYPE html>
<html lang="zh_CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Large Table</title><style>#table{border-collapse: collapse;table-layout: fixed;width: 100%;}#table tr:hover{background-color: #bbb;}#table td {border: 1px solid #ddd;}</style>
</head>
<body><div style="height: 200px;overflow:auto;"><table id="table" ><colgroup><col style="background: #ddd;"><col style="font-weight: bold;"></colgroup></table></div><h2 id="loading"></h2>
</body>
<script>console.time()const ROW_TEMP = [{ type: 'id' }, '王小虎', '28', '170cm', '80kg', 'xx省xxxx有限公司', { type: 'button', label: '详情' }];const ROWS = 20000;let table = document.getElementById('table');let fgmt = document.createDocumentFragment();for (let i = 1; i <= ROWS; i++) { // rowlet tr = document.createElement('tr')for (let j = 0; j < ROW_TEMP.length; j++) { // columnlet item = ROW_TEMP[j];let td = document.createElement('td');td.className = 'item';if (typeof item == 'string') {td.textContent = ROW_TEMP[j];} else {if (item.type == 'id') {td.textContent = i;} else if (item.type == 'button') {let btn = document.createElement('button');btn.textContent = item.label;td.append(btn);}}tr.appendChild(td);}fgmt.appendChild(tr);if( !(i % 10) ){ // 10条数据加载一次let tmp = fgmt;setTimeout(() => {table.appendChild(tmp);document.querySelector('#loading').textContent = `loading...(${Math.ceil(i/ROWS*100)}%)`;}, i*3);fgmt = document.createDocumentFragment();}}console.timeEnd()
</script>
</html>

动态加载关键代码

if(!(i % 10)){ // 10条数据加载一次let tmp = fgmt;setTimeout(() => {table.appendChild(tmp);document.querySelector('#loading').textContent = `loading...(${Math.ceil(i/ROWS*100)}%)`;}, i*3);fgmt = document.createDocumentFragment();
}

加载10条数据后延时,延时的地方为3倍的 i,控制上一次加载和下一次加载间间隔为10 × 3 = 30 ms

是为了确保在30ms期间内能够把本次(10条)数据加载完。且有剩下的时间会交给js线程,使浏览器相应用户行为,防止页面卡住。

由于涉及到大量元素的新增和append,这里使用了DocumentFragement,来将保存创建的元素片段,之后一次性加到table中,据说能在一定程度上提升性能。

table添加子元素的时候会导致浏览器reflow(重排),因为table列宽会根据该列撑开的最大宽度调整。

因此这里CSS设置了table-layout:fixed 使每列的宽度固定,据说可以提升性能。

效果

这里看到,表中的数据是在不停增加的,右侧滚动条位置也在变化。滚动卡顿可接受。

但是加载完成后又流畅了,如果不介意的话,就可以直接用了。

尝试2.使用绝对定位优化表格

那么根据页面优化原则,减少浏览器reflow(重排),使用position:absolute定位的元素不会导致浏览器reflow。

那么自行用div布局实现一下表格,每行内容使用flex布局,行的位置使用absolute绝对定位计算出来

表格在渲染前,先计算总高度,再用一个元素来占位高度(.table-height元素),这样在加载表格的时候,右侧滚动条就不会乱动了。

<!DOCTYPE html>
<html lang="zh_CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Large Table</title><style>.table {position: relative;border-top: 1px solid #ddd;}.table .row {box-sizing: border-box;width: 100%;display: flex;position: absolute;height: 30px;line-height: 30px;}.table-height {width: 1px;background: #ddd;}.table .row .item {overflow: hidden;text-overflow: ellipsis;flex-grow: 1;width: 100px;border-right: 1px solid #ddd;border-bottom: 1px solid #ddd;}</style>
</head><body><div id="tableContent" style="height: 200px;overflow:auto;"><div class="table" id="table"><div class="table-height"></div></div></table></div><h2 id="loading"></h2>
</body><script>console.time()const LINE_HEIGHT = 30;const ROWS = 20000;const COLS = 10;const DATA_STEP = 200;const ROW_TEMP = [{ type: 'id' }, '王小虎', '28', '170cm', '80kg', 'xx省xxxx有限公司', { type: 'button', label: '详情' }];let table = document.getElementById('table');let fgmt = document.createDocumentFragment();document.querySelector('.table-height').style = 'height:' + LINE_HEIGHT * ROWS + 'px';for (let i = 1; i <= ROWS; i++) { // rowlet tr = document.createElement('div');tr.className = 'row';tr.style.top = (i - 1) * LINE_HEIGHT + 'px';for (let j = 0; j < ROW_TEMP.length; j++) { // columnlet item = ROW_TEMP[j];let td = document.createElement('div');td.className = 'item';if (typeof item == 'string') {td.textContent = ROW_TEMP[j];} else {if (item.type == 'id') {td.textContent = i;} else if (item.type == 'button') {let btn = document.createElement('button');btn.textContent = item.label;td.append(btn);}}tr.appendChild(td);}fgmt.appendChild(tr);if (ROWS >= DATA_STEP && !(i % DATA_STEP)) { // 多少条数据加载一次let tmp = fgmt;setTimeout(() => {table.appendChild(tmp);document.querySelector('#loading').textContent = `loading...(${Math.round(i / ROWS * 100)}%)`;}, i * 2); // 保证让出线程时间片fgmt = document.createDocumentFragment(); // 清空}if (ROWS < DATA_STEP) {table.appendChild(fgmt);}}console.timeEnd()
</script></html>

效果

可以看到刚开始加载的时候还是很流畅的,随着数据增多,滚动也变得卡了起来。且在加载中,下面未加载的数据都是空白,用户体验相对差一些(即使做了加载进度百分比提示)

问题是这个方法在表格加载完成后,也巨卡。

那么能不能用懒加载的形式加载数据呢,比如我滚动到哪个位置,哪里的数据就开始加载,继续尝试

尝试3.绝对定位+scroll动态加载优化尝试

正是使用了绝对定位自己实现了表格,所以懒加载可以轻易实现。否则普通table实现懒加载或需要其他特殊方式。

<!DOCTYPE html>
<html lang="zh_CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Large Table2</title><style>.table {position: relative;border-top: 1px solid #ddd;}.table .row {box-sizing: border-box;width: 100%;display: flex;position: absolute;height: 30px;line-height: 30px;}/*高度占位元素*/.table-height {width: 1px;background: #ddd;}.table .row:hover {background: #ddd;}.table .row .item {overflow: hidden;text-overflow: ellipsis;flex-grow: 1;width: 100px;border-right: 1px solid #ddd;border-bottom: 1px solid #ddd;}</style>
</head><body><div id="tableContent" style="height: 200px;overflow:auto;"><div class="table" id="table"><!--高度占位元素--><div class="table-height"></div></div></table></div>
</body>
<script>console.time()const LINE_HEIGHT = 30;const PAGE_SIZE = 100;const ROWS = 50000;const COLS = 10;const DATA_STEP = 200;const PRELOAD_PAGES = 2; // 预加载页数const ROW_TEMP = [{ type: 'id' }, '王小虎', '28', '170cm', '80kg', '浙江省杭州市xxxxxx有限公司', { type: 'button', label: '详情' }];const LOADED_INDEX = new Set(); // 存已经加载的页const TOTAL_PAGES = Math.floor(ROWS / PAGE_SIZE); // 总页数let tableContent = document.querySelector("#tableContent");let table = document.getElementById('table');document.querySelector('.table-height').style = 'height:' + LINE_HEIGHT * ROWS + 'px';/*** 找到未加载的页 */function findUnloadPage(pageIndex) {let arr = [];for (let i = pageIndex; i <= pageIndex + PRELOAD_PAGES && i < TOTAL_PAGES; i++) {if (!LOADED_INDEX.has(i)) arr.push(i);}return arr;}/*** 从第几条数据开始加载* @param startIndex 开始加载的数据index(通过scrollTop/height得出*/function loadPage(pageIndex) {if (pageIndex > TOTAL_PAGES) return;let unLoadedPages = findUnloadPage(pageIndex);if (!unLoadedPages.length) return;unLoadedPages.forEach((unLoadedPage) => {let start = unLoadedPage * PAGE_SIZE;let end = (unLoadedPage + 1) * PAGE_SIZE;LOADED_INDEX.add(unLoadedPage); // 记录已加载的let fgmt = loadRowsRange(start, end);table.appendChild(fgmt);})}/*** 加载数据区间* @param {Number} start 开始index* @param {Number} end   结束index* @return {DocumentFragement}*/function loadRowsRange(start, end) {let fgmt = document.createDocumentFragment();for (let i = start; i < end; i++) { // rowlet row = document.createElement('div');row.className = 'row';row.style.top = i * LINE_HEIGHT + 'px';for (let j = 0; j < ROW_TEMP.length; j++) { // columnlet item = ROW_TEMP[j];let td = document.createElement('div');td.className = 'item';if (typeof item == 'string') {td.textContent = ROW_TEMP[j];} else {if (item.type == 'id') {td.textContent = i;} else if (item.type == 'button') {let btn = document.createElement('button');btn.textContent = item.label;td.append(btn);}}row.appendChild(td);}fgmt.appendChild(row);}return fgmt;}loadPage(0);let debunceTimeout = null; // 防抖tableContent.addEventListener('scroll', (e) => {let pageIndex = Math.floor(e.target.scrollTop / (PAGE_SIZE * LINE_HEIGHT));console.log(pageIndex);if (debunceTimeout) {clearTimeout(debunceTimeout);}debunceTimeout = setTimeout(() => {loadPage(pageIndex);// console.log(LOADED_INDEX);}, 100);})console.timeEnd()</script></html>

效果

这里可以看到,通过懒加载的形式去加载数据,页面流畅度得到了很大的提高。

但是如果滚动条拉动过快,还是会有一瞬间的白屏问题。

而且也会随着已渲染数据量的增加而变卡。


接下来就得解决数据加载完成后,滚动表格卡的问题,我想到的方案分为两类

  1. 回头是岸,用回table标签,因为加载完后不卡(后来发现是td的overflow:hidden引起的)。缺点是不能懒加载。或者想办法使用table去实现懒加载。
  2. 仍旧使用绝对定位+懒加载,只是设置一个数据队列,最大值比如5000条,通过后面若再加载,就把最先加载的数据删除了。缺点是闪屏,数据永远都要加载。

继续探索


尝试4. table + 绝对定位 + scroll动态加载

综合考虑1.使用table初级实现 3.绝对定位+scroll动态加载优化尝试  后,由于table加载完数据后滚动的流畅性,因此打算用回table标签做表格。

那么,如何使table中的元素也懒加载呢——每页用单独一个table拼接起来,每个table再使用绝对定位。

其次,也加回了自动加载的代码(如下function autoLoadData),在用户没什么操作的时候,默默把剩下的数据也加载进去。同时,也支持懒加载,用户点到哪儿,哪儿的数据开始加载。

同时LINE_HEIGHT也根据第一次加载后,动态获取。因为系统缩放和浏览器缩放下,一行的高度不一定是30px,如下

下面改造第3部分的代码

<!DOCTYPE html>
<html lang="zh_CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Large Table4</title><style>.table-content {height: 200px;overflow:auto;position: relative;}.table {position: absolute;width: 100%;table-layout: fixed;border-collapse: collapse;border-spacing: 0px;}.table .row {box-sizing: border-box;height: 30px;}/*高度占位元素*/.table-height {float:left;width: 1px;}.table .row:hover {background: #ddd;}.table .row .item {padding: 0;white-space: nowrap;/* overflow: hidden; 严重影响性能*//* text-overflow: ellipsis; */border: 1px solid #ddd;}.loading{--width: 50%;height: 5px;width: var(--width);background-color: cadetblue;}</style>
</head><body><div id="tableContent" class="table-content"><div class="table-height"></div> <!--高度占位元素--><!-- <table class="table" id="table"></table> --></div><div class="loading"></div>
</body>
<script>console.time()let LINE_HEIGHT = 30;const PAGE_SIZE = 200;const ROWS = 50000;const AUTO_LOAD_MS = 20; // 自动加载间隔ms const PRELOAD_PAGES = 1; // 预加载页数const ROW_TEMP = [{ type: 'id' }, '王小虎', '28', '170cm', '80kg', '浙江省xxxxxx有限公司', { type: 'button', label: '详情' }];let LOADED_INDEX = new Set(); // 存已经加载的页const TOTAL_PAGES = Math.floor(ROWS / PAGE_SIZE); // 总页数let tableContent = document.querySelector("#tableContent");document.querySelector('.table-height').style = 'height:' + LINE_HEIGHT * ROWS + 'px';window.onload = function(){tableContent.addEventListener('scroll', scrollEvent)autoLoadData() // init page}/**找到未加载的页 */function findUnloadPage(pageIndex) {let arr = [];for (let i = pageIndex; i <= pageIndex + PRELOAD_PAGES && i < TOTAL_PAGES; i++) {if (!LOADED_INDEX.has(i)) arr.push(i);}return arr;}/*** 从第几条数据开始加载* @param startIndex 开始加载的数据index(通过scrollTop/height得出*/function loadPage(pageIndex) {if (pageIndex > TOTAL_PAGES) return;let unLoadedPages = findUnloadPage(pageIndex);if (!unLoadedPages.length) return;unLoadedPages.forEach((unLoadedPage) => {let start = unLoadedPage * PAGE_SIZE;let end = (unLoadedPage + 1) * PAGE_SIZE;LOADED_INDEX.add(unLoadedPage); // 记录已加载的let fgmt = loadRowsRange(start, end);tableContent.appendChild(fgmt);if(unLoadedPage == 0){let row = document.querySelector('.row');LINE_HEIGHT = parseFloat(getComputedStyle(row).height); // 计算出实际高度}})}/*** 加载数据区间* @param {Number} start 开始index* @param {Number} end   结束index* @return {DocumentFragement}*/function loadRowsRange(start, end) {let fgmt = document.createDocumentFragment();let table = document.createElement('table');table.classList.add('table');table.style.top = start * LINE_HEIGHT + 'px';for (let i = start; i < end; i++) { // rowlet row = document.createElement('tr');row.className = 'row';for (let j = 0; j < ROW_TEMP.length; j++) { // columnlet item = ROW_TEMP[j];let td = document.createElement('td');td.className = 'item';if (typeof item == 'string') {td.textContent = ROW_TEMP[j];} else {if (item.type == 'id') {td.textContent = i;} else if (item.type == 'button') {let btn = document.createElement('button');btn.textContent = item.label;td.append(btn);}}row.appendChild(td);}table.appendChild(row);}fgmt.appendChild(table);return fgmt;}let debunceTimeout = null; // 防抖function scrollEvent(e) {let pageIndex = Math.floor(e.target.scrollTop / (PAGE_SIZE * LINE_HEIGHT));// console.log(pageIndex);if (debunceTimeout) {clearTimeout(debunceTimeout);}debunceTimeout = setTimeout(() => {loadPage(pageIndex);// console.log(LOADED_INDEX);}, AUTO_LOAD_MS);}/*auto load data*/function autoLoadData(){let pageIndex = 0;let loading = document.querySelector('.loading');let interval = setInterval(() => {// console.log(pageIndex);if(pageIndex >= TOTAL_PAGES){ // fininsh load all dataclearInterval(interval);tableContent.removeEventListener('scroll', scrollEvent); // remove scroll listener to improve performanceLOADED_INDEX = null; // try to gc}loading.style.setProperty('--width', pageIndex/TOTAL_PAGES * 100 + '%');loadPage(pageIndex++);}, 100)}console.timeEnd()</script></html>

经过研究比较,发现css中给每个td设置overflow:hidden; 会严重影响滚动性能,因此我选择注释css中的那一部分。同时我也试着将本文尝试3中的over-flow:hidden去除,数据加载完成后,果然流畅不少,但仍比不上table标签。

去掉overflow:hidden后,在数据加载完成后滚动表格就变得丝般顺滑。

这样导致如果仍按30px来计算高度的话,得到的top值会出现问题

其中有几个关键的变量与加载性能挂钩,如下

  • PAGE_SIZE: 表示每页的大小,这个值越大,那么加载一页的时候,渲染线程占用的时间就越长。
  • AUTO_LOAD_MS:自动加载时,每次加载的间隔时间,值越小,则渲染线程执行完后,剩下的时间给js就越短。

  • PRELOAD_PAGES: 懒加载时,预加载的页数,如拉滚动条瞬间跳转到第10页,如果这个值设置为2,则会预加载11,12页的内容。

  • ROWS: 加载的记录数,直接影响页面性能,不过多介绍

效果

加载过程中有点卡,但是加载完成后就很流畅了。


接下来就是讨论overflow:hidden的问题,一个表格中td内容总会溢出td的,那既然over-flow:hidden; 这么影响性能,如何解决表格内容溢出问题呢。

大体思考了几个方向。

  1. 尝试用css选择器或js,单独将可能溢出的列td设置为over-flow:hidden;
  2. 设置懒加载队列,队列溢出时,将最先加载的页删除。
  3. 使其换行,但是换行的话会影响表格的高度,使懒加载时不好计算位置。
  4. 虚拟滚动(这个在行业内已经很成熟了)
  5. css 属性content-visibility 优化性能,chrome > 85

有关大数据表格加载,这个方向大体上是有了,之后的代码我就放到github上了,欢迎指导 GitHub - 601286825nj/big-table


参考资料

  1. 从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理——“GUI渲染线程与JS引擎线程互斥”小节

若有错误/补充,敬请指出更改

JS 原生自动加载的大数据表格探索相关推荐

  1. pace.js网页自动加载进度条插件-好东西

    pace.js – 网页自动加载进度条插件 源码地址 https://github.com/HubSpot/pace 入Pace.js以及主题文件 Pace.js公开的API列表, Pace.star ...

  2. JS 网页自动加载js函数

    1法:body 的onload的方法 <body onLoad="clockon(bgclock)"> 2法:window.onload方法 <script de ...

  3. Cesium加载3D Tiles数据

    使用原生Cesium加载3D Tiles数据,并实现对图层的选中.样式的修改.图元的隐藏. 实现代码 <!DOCTYPE html> <html lang="en" ...

  4. html底部自动加载下一页,js页面滚动到底部时自动加载下一页数据

    一段滚动到页面底部自动加载更多数据的js代码,需要jquery.js . jquey.tmpl.js 支持 var Data = { PageCount: 1, PageSize: 30, IsCom ...

  5. html分页自动加载数据,硕正控件默认会自动加载数据

    硕正控件 autoLoad 页面加载时是否自动加载表格数据 默认true,如果自己需要根据页面条件查询数据  建议设置为false  自己去查询加载数据. 硕正表格控件API 属性 属性的定义, 例如 ...

  6. 分享大麦UWP版本开发历程-03.GridView或ListView 滚动底部自动加载后续数据

    今天跟大家分享的是大麦UWP客户端,在分类.订单或是搜索时都用到的一个小技巧,技术粗糙大神勿喷. 以大麦分类举例,默认打开的时候,会为用户展示20条数据,当用户滚动鼠标或者使用手势将列表滑动到倒数第二 ...

  7. html动态加载js方法,原生JS实现动态加载js文件并在加载成功后执行回调函数的方法...

    本文实例讲述了原生JS实现动态加载js文件并在加载成功后执行回调函数的方法.分享给大家供大家参考,具体如下: 有的时候需要动态加载一个javascript文件,并且在加载成功后执行回调函数(例如文件中 ...

  8. recyclerview的数据刷新(下拉刷新和自动加载更多)以及添加提示语(例如:“数据已加载完毕”)

    下拉加载更多的核心是SwipeRefreshLayout搭配Recyclerview进行使用.布局为 <android.support.v4.widget.SwipeRefreshLayout ...

  9. ios 平滑移动view_iOS 关于列表上拉(平滑加载数据)自动加载数据的问题

    项目需求 我的的列表需要改变,原来的分页加载采用的是MJRefresh框架进行加载更多数据,这需要有一个上拉动作才能触发,而我的产品的意思是当快要滑动到底部时自动加载下一页数据.我自己看了一下,发现很 ...

最新文章

  1. C++ [](){} 匿名函数 lambda表达式
  2. 用WORD2010写博客,并发送至博客园
  3. Hibernate- QBC-基本查询
  4. 经常用everything对硬盘有伤害吗?
  5. 【图像隐写】基于matlab GUI DWT与SVD数字水印 【含Matlab源码 253期】
  6. C++ 与 python 语法 对比
  7. 置换选择排序算法详解(C语言实现)
  8. 数控编程也是c语言吗,学数控编程需要使用个人电脑吗
  9. oracle SQL以结尾函数,Oracle SQL 内置函数大全
  10. 【新技能get】让App像Web一样发布新版本
  11. C++ 单例模式学习(Singleton)
  12. oracle查询练习2(解析+答案)
  13. 状态机在PLC中的应用
  14. linux云计算架构师:搭建DHCP服务和NTP网络时间同步
  15. 命令提示符怎么进入D盘?简单的技能分享
  16. 北京宣武区一个居民区停电三个月
  17. Linux下安装配置Cobra教程
  18. 解读BLM业务设计中的战略控制点
  19. 非接触式射频读卡器 M1读卡,支持USB,ISO14443A/B,可读二代证ID
  20. 12.5.2 升12.5.3的补丁 linux下载,lolv3.2.3.2-V4.0.5.1升级-lolv3.2.3.2-V4.0.5.1升级补丁【7.12版本】下载官方最新版-西西软件下载...

热门文章

  1. 福州大学数学计算机学院,福州大学数学与计算机科学学院导师介绍:廖祥文
  2. 福州大学计算机学院印佳丽,福州大学数学与计算机科学学院导师介绍:曾有栋...
  3. Extjs TreePanel API详解
  4. 封基高折价 投资机会扩大
  5. DHU Deep Learning Practice_章节测验【1】
  6. spring mvc + JSR-303验证框架
  7. 2021上海国际生物工程装备与技术展
  8. swift block语法
  9. 第4代计算机逻辑器件,第4代电子计算机使用的逻辑器件是什么?
  10. thinkphp6 lib短信宝短信验证码封装