当前位置:   article > 正文

JS 原生自动加载的大数据表格探索

JS 原生自动加载的大数据表格探索

目录

尝试1.使用table初级实现

动态加载关键代码

效果

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

效果

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

效果

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

效果

参考资料


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

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

比如加载10000条数据,效果

加载10000条数据效果

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

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

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

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

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

下面动手实践:

尝试1.使用table初级实现

  1. <!DOCTYPE html>
  2. <html lang="zh_CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Large Table</title>
  7. <style>
  8. #table{
  9. border-collapse: collapse;
  10. table-layout: fixed;
  11. width: 100%;
  12. }
  13. #table tr:hover{
  14. background-color: #bbb;
  15. }
  16. #table td {
  17. border: 1px solid #ddd;
  18. }
  19. </style>
  20. </head>
  21. <body>
  22. <div style="height: 200px;overflow:auto;">
  23. <table id="table" >
  24. <colgroup>
  25. <col style="background: #ddd;">
  26. <col style="font-weight: bold;">
  27. </colgroup>
  28. </table>
  29. </div>
  30. <h2 id="loading"></h2>
  31. </body>
  32. <script>
  33. console.time()
  34. const ROW_TEMP = [{ type: 'id' }, '王小虎', '28', '170cm', '80kg', 'xx省xxxx有限公司', { type: 'button', label: '详情' }];
  35. const ROWS = 20000;
  36. let table = document.getElementById('table');
  37. let fgmt = document.createDocumentFragment();
  38. for (let i = 1; i <= ROWS; i++) { // row
  39. let tr = document.createElement('tr')
  40. for (let j = 0; j < ROW_TEMP.length; j++) { // column
  41. let item = ROW_TEMP[j];
  42. let td = document.createElement('td');
  43. td.className = 'item';
  44. if (typeof item == 'string') {
  45. td.textContent = ROW_TEMP[j];
  46. } else {
  47. if (item.type == 'id') {
  48. td.textContent = i;
  49. } else if (item.type == 'button') {
  50. let btn = document.createElement('button');
  51. btn.textContent = item.label;
  52. td.append(btn);
  53. }
  54. }
  55. tr.appendChild(td);
  56. }
  57. fgmt.appendChild(tr);
  58. if( !(i % 10) ){ // 10条数据加载一次
  59. let tmp = fgmt;
  60. setTimeout(() => {
  61. table.appendChild(tmp);
  62. document.querySelector('#loading').textContent = `loading...(${Math.ceil(i/ROWS*100)}%)`;
  63. }, i*3);
  64. fgmt = document.createDocumentFragment();
  65. }
  66. }
  67. console.timeEnd()
  68. </script>
  69. </html>

动态加载关键代码

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

加载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元素),这样在加载表格的时候,右侧滚动条就不会乱动了。

  1. <!DOCTYPE html>
  2. <html lang="zh_CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Large Table</title>
  7. <style>
  8. .table {
  9. position: relative;
  10. border-top: 1px solid #ddd;
  11. }
  12. .table .row {
  13. box-sizing: border-box;
  14. width: 100%;
  15. display: flex;
  16. position: absolute;
  17. height: 30px;
  18. line-height: 30px;
  19. }
  20. .table-height {
  21. width: 1px;
  22. background: #ddd;
  23. }
  24. .table .row .item {
  25. overflow: hidden;
  26. text-overflow: ellipsis;
  27. flex-grow: 1;
  28. width: 100px;
  29. border-right: 1px solid #ddd;
  30. border-bottom: 1px solid #ddd;
  31. }
  32. </style>
  33. </head>
  34. <body>
  35. <div id="tableContent" style="height: 200px;overflow:auto;">
  36. <div class="table" id="table">
  37. <div class="table-height"></div>
  38. </div>
  39. </table>
  40. </div>
  41. <h2 id="loading"></h2>
  42. </body>
  43. <script>
  44. console.time()
  45. const LINE_HEIGHT = 30;
  46. const ROWS = 20000;
  47. const COLS = 10;
  48. const DATA_STEP = 200;
  49. const ROW_TEMP = [{ type: 'id' }, '王小虎', '28', '170cm', '80kg', 'xx省xxxx有限公司', { type: 'button', label: '详情' }];
  50. let table = document.getElementById('table');
  51. let fgmt = document.createDocumentFragment();
  52. document.querySelector('.table-height').style = 'height:' + LINE_HEIGHT * ROWS + 'px';
  53. for (let i = 1; i <= ROWS; i++) { // row
  54. let tr = document.createElement('div');
  55. tr.className = 'row';
  56. tr.style.top = (i - 1) * LINE_HEIGHT + 'px';
  57. for (let j = 0; j < ROW_TEMP.length; j++) { // column
  58. let item = ROW_TEMP[j];
  59. let td = document.createElement('div');
  60. td.className = 'item';
  61. if (typeof item == 'string') {
  62. td.textContent = ROW_TEMP[j];
  63. } else {
  64. if (item.type == 'id') {
  65. td.textContent = i;
  66. } else if (item.type == 'button') {
  67. let btn = document.createElement('button');
  68. btn.textContent = item.label;
  69. td.append(btn);
  70. }
  71. }
  72. tr.appendChild(td);
  73. }
  74. fgmt.appendChild(tr);
  75. if (ROWS >= DATA_STEP && !(i % DATA_STEP)) { // 多少条数据加载一次
  76. let tmp = fgmt;
  77. setTimeout(() => {
  78. table.appendChild(tmp);
  79. document.querySelector('#loading').textContent = `loading...(${Math.round(i / ROWS * 100)}%)`;
  80. }, i * 2); // 保证让出线程时间片
  81. fgmt = document.createDocumentFragment(); // 清空
  82. }
  83. if (ROWS < DATA_STEP) {
  84. table.appendChild(fgmt);
  85. }
  86. }
  87. console.timeEnd()
  88. </script>
  89. </html>

效果

效果图

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

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

加载我完成后

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

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

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

  1. <!DOCTYPE html>
  2. <html lang="zh_CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Large Table2</title>
  7. <style>
  8. .table {
  9. position: relative;
  10. border-top: 1px solid #ddd;
  11. }
  12. .table .row {
  13. box-sizing: border-box;
  14. width: 100%;
  15. display: flex;
  16. position: absolute;
  17. height: 30px;
  18. line-height: 30px;
  19. }
  20. /*高度占位元素*/
  21. .table-height {
  22. width: 1px;
  23. background: #ddd;
  24. }
  25. .table .row:hover {
  26. background: #ddd;
  27. }
  28. .table .row .item {
  29. overflow: hidden;
  30. text-overflow: ellipsis;
  31. flex-grow: 1;
  32. width: 100px;
  33. border-right: 1px solid #ddd;
  34. border-bottom: 1px solid #ddd;
  35. }
  36. </style>
  37. </head>
  38. <body>
  39. <div id="tableContent" style="height: 200px;overflow:auto;">
  40. <div class="table" id="table">
  41. <!--高度占位元素-->
  42. <div class="table-height"></div>
  43. </div>
  44. </table>
  45. </div>
  46. </body>
  47. <script>
  48. console.time()
  49. const LINE_HEIGHT = 30;
  50. const PAGE_SIZE = 100;
  51. const ROWS = 50000;
  52. const COLS = 10;
  53. const DATA_STEP = 200;
  54. const PRELOAD_PAGES = 2; // 预加载页数
  55. const ROW_TEMP = [{ type: 'id' }, '王小虎', '28', '170cm', '80kg', '浙江省杭州市xxxxxx有限公司', { type: 'button', label: '详情' }];
  56. const LOADED_INDEX = new Set(); // 存已经加载的页
  57. const TOTAL_PAGES = Math.floor(ROWS / PAGE_SIZE); // 总页数
  58. let tableContent = document.querySelector("#tableContent");
  59. let table = document.getElementById('table');
  60. document.querySelector('.table-height').style = 'height:' + LINE_HEIGHT * ROWS + 'px';
  61. /**
  62. * 找到未加载的页
  63. */
  64. function findUnloadPage(pageIndex) {
  65. let arr = [];
  66. for (let i = pageIndex; i <= pageIndex + PRELOAD_PAGES && i < TOTAL_PAGES; i++) {
  67. if (!LOADED_INDEX.has(i)) arr.push(i);
  68. }
  69. return arr;
  70. }
  71. /**
  72. * 从第几条数据开始加载
  73. * @param startIndex 开始加载的数据index(通过scrollTop/height得出
  74. */
  75. function loadPage(pageIndex) {
  76. if (pageIndex > TOTAL_PAGES) return;
  77. let unLoadedPages = findUnloadPage(pageIndex);
  78. if (!unLoadedPages.length) return;
  79. unLoadedPages.forEach((unLoadedPage) => {
  80. let start = unLoadedPage * PAGE_SIZE;
  81. let end = (unLoadedPage + 1) * PAGE_SIZE;
  82. LOADED_INDEX.add(unLoadedPage); // 记录已加载的
  83. let fgmt = loadRowsRange(start, end);
  84. table.appendChild(fgmt);
  85. })
  86. }
  87. /**
  88. * 加载数据区间
  89. * @param {Number} start 开始index
  90. * @param {Number} end 结束index
  91. * @return {DocumentFragement}
  92. */
  93. function loadRowsRange(start, end) {
  94. let fgmt = document.createDocumentFragment();
  95. for (let i = start; i < end; i++) { // row
  96. let row = document.createElement('div');
  97. row.className = 'row';
  98. row.style.top = i * LINE_HEIGHT + 'px';
  99. for (let j = 0; j < ROW_TEMP.length; j++) { // column
  100. let item = ROW_TEMP[j];
  101. let td = document.createElement('div');
  102. td.className = 'item';
  103. if (typeof item == 'string') {
  104. td.textContent = ROW_TEMP[j];
  105. } else {
  106. if (item.type == 'id') {
  107. td.textContent = i;
  108. } else if (item.type == 'button') {
  109. let btn = document.createElement('button');
  110. btn.textContent = item.label;
  111. td.append(btn);
  112. }
  113. }
  114. row.appendChild(td);
  115. }
  116. fgmt.appendChild(row);
  117. }
  118. return fgmt;
  119. }
  120. loadPage(0);
  121. let debunceTimeout = null; // 防抖
  122. tableContent.addEventListener('scroll', (e) => {
  123. let pageIndex = Math.floor(e.target.scrollTop / (PAGE_SIZE * LINE_HEIGHT));
  124. console.log(pageIndex);
  125. if (debunceTimeout) {
  126. clearTimeout(debunceTimeout);
  127. }
  128. debunceTimeout = setTimeout(() => {
  129. loadPage(pageIndex);
  130. // console.log(LOADED_INDEX);
  131. }, 100);
  132. })
  133. console.timeEnd()
  134. </script>
  135. </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部分的代码

  1. <!DOCTYPE html>
  2. <html lang="zh_CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Large Table4</title>
  7. <style>
  8. .table-content {
  9. height: 200px;
  10. overflow:auto;
  11. position: relative;
  12. }
  13. .table {
  14. position: absolute;
  15. width: 100%;
  16. table-layout: fixed;
  17. border-collapse: collapse;
  18. border-spacing: 0px;
  19. }
  20. .table .row {
  21. box-sizing: border-box;
  22. height: 30px;
  23. }
  24. /*高度占位元素*/
  25. .table-height {
  26. float:left;
  27. width: 1px;
  28. }
  29. .table .row:hover {
  30. background: #ddd;
  31. }
  32. .table .row .item {
  33. padding: 0;
  34. white-space: nowrap;
  35. /* overflow: hidden; 严重影响性能*/
  36. /* text-overflow: ellipsis; */
  37. border: 1px solid #ddd;
  38. }
  39. .loading{
  40. --width: 50%;
  41. height: 5px;
  42. width: var(--width);
  43. background-color: cadetblue;
  44. }
  45. </style>
  46. </head>
  47. <body>
  48. <div id="tableContent" class="table-content">
  49. <div class="table-height"></div> <!--高度占位元素-->
  50. <!-- <table class="table" id="table"></table> -->
  51. </div>
  52. <div class="loading"></div>
  53. </body>
  54. <script>
  55. console.time()
  56. let LINE_HEIGHT = 30;
  57. const PAGE_SIZE = 200;
  58. const ROWS = 50000;
  59. const AUTO_LOAD_MS = 20; // 自动加载间隔ms
  60. const PRELOAD_PAGES = 1; // 预加载页数
  61. const ROW_TEMP = [{ type: 'id' }, '王小虎', '28', '170cm', '80kg', '浙江省xxxxxx有限公司', { type: 'button', label: '详情' }];
  62. let LOADED_INDEX = new Set(); // 存已经加载的页
  63. const TOTAL_PAGES = Math.floor(ROWS / PAGE_SIZE); // 总页数
  64. let tableContent = document.querySelector("#tableContent");
  65. document.querySelector('.table-height').style = 'height:' + LINE_HEIGHT * ROWS + 'px';
  66. window.onload = function(){
  67. tableContent.addEventListener('scroll', scrollEvent)
  68. autoLoadData() // init page
  69. }
  70. /**找到未加载的页 */
  71. function findUnloadPage(pageIndex) {
  72. let arr = [];
  73. for (let i = pageIndex; i <= pageIndex + PRELOAD_PAGES && i < TOTAL_PAGES; i++) {
  74. if (!LOADED_INDEX.has(i)) arr.push(i);
  75. }
  76. return arr;
  77. }
  78. /**
  79. * 从第几条数据开始加载
  80. * @param startIndex 开始加载的数据index(通过scrollTop/height得出
  81. */
  82. function loadPage(pageIndex) {
  83. if (pageIndex > TOTAL_PAGES) return;
  84. let unLoadedPages = findUnloadPage(pageIndex);
  85. if (!unLoadedPages.length) return;
  86. unLoadedPages.forEach((unLoadedPage) => {
  87. let start = unLoadedPage * PAGE_SIZE;
  88. let end = (unLoadedPage + 1) * PAGE_SIZE;
  89. LOADED_INDEX.add(unLoadedPage); // 记录已加载的
  90. let fgmt = loadRowsRange(start, end);
  91. tableContent.appendChild(fgmt);
  92. if(unLoadedPage == 0){
  93. let row = document.querySelector('.row');
  94. LINE_HEIGHT = parseFloat(getComputedStyle(row).height); // 计算出实际高度
  95. }
  96. })
  97. }
  98. /**
  99. * 加载数据区间
  100. * @param {Number} start 开始index
  101. * @param {Number} end 结束index
  102. * @return {DocumentFragement}
  103. */
  104. function loadRowsRange(start, end) {
  105. let fgmt = document.createDocumentFragment();
  106. let table = document.createElement('table');
  107. table.classList.add('table');
  108. table.style.top = start * LINE_HEIGHT + 'px';
  109. for (let i = start; i < end; i++) { // row
  110. let row = document.createElement('tr');
  111. row.className = 'row';
  112. for (let j = 0; j < ROW_TEMP.length; j++) { // column
  113. let item = ROW_TEMP[j];
  114. let td = document.createElement('td');
  115. td.className = 'item';
  116. if (typeof item == 'string') {
  117. td.textContent = ROW_TEMP[j];
  118. } else {
  119. if (item.type == 'id') {
  120. td.textContent = i;
  121. } else if (item.type == 'button') {
  122. let btn = document.createElement('button');
  123. btn.textContent = item.label;
  124. td.append(btn);
  125. }
  126. }
  127. row.appendChild(td);
  128. }
  129. table.appendChild(row);
  130. }
  131. fgmt.appendChild(table);
  132. return fgmt;
  133. }
  134. let debunceTimeout = null; // 防抖
  135. function scrollEvent(e) {
  136. let pageIndex = Math.floor(e.target.scrollTop / (PAGE_SIZE * LINE_HEIGHT));
  137. // console.log(pageIndex);
  138. if (debunceTimeout) {
  139. clearTimeout(debunceTimeout);
  140. }
  141. debunceTimeout = setTimeout(() => {
  142. loadPage(pageIndex);
  143. // console.log(LOADED_INDEX);
  144. }, AUTO_LOAD_MS);
  145. }
  146. /*auto load data*/
  147. function autoLoadData(){
  148. let pageIndex = 0;
  149. let loading = document.querySelector('.loading');
  150. let interval = setInterval(() => {
  151. // console.log(pageIndex);
  152. if(pageIndex >= TOTAL_PAGES){ // fininsh load all data
  153. clearInterval(interval);
  154. tableContent.removeEventListener('scroll', scrollEvent); // remove scroll listener to improve performance
  155. LOADED_INDEX = null; // try to gc
  156. }
  157. loading.style.setProperty('--width', pageIndex/TOTAL_PAGES * 100 + '%');
  158. loadPage(pageIndex++);
  159. }, 100)
  160. }
  161. console.timeEnd()
  162. </script>
  163. </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 
  6. setTimeout 换为 requestAnimationFrame 加载数据。

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


参考资料

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

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/579639
推荐阅读
相关标签
  

闽ICP备14008679号