赞
踩
目录
尝试4. table + 绝对定位 + scroll动态加载
大数据表格,就是能够没有分页的情况下,一次展示上万条数据的表格。
若直接渲染上万条数据的,页面会一直卡着,直到浏览器渲染完成后才显示且响应用户操作。
比如加载10000条数据,效果
那么如何做到打开、刷新大数据表格页面的时候能够马上显示用户可见部分的数据,剩下数据在后台慢慢加载呢。
但是理想是美好的,现实是骨感的。
这里就出现了矛盾,由于浏览器渲染线程与JS线程是互斥的,也就是说在渲染页面的时候js就停止执行,js执行时,页面停止渲染。[参考资料1]
所以在web前端中,难以将页面渲染放到“后台”执行(JS的话可以通过Web Workers 另启起一个线程进行复杂计算)
即便是这样,我想到了cpu时间片的概念,打算少量多次进行渲染表格—让出js线程—渲染表格—让出线程—...
下面动手实践:
- <!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++) { // row
- let tr = document.createElement('tr')
- for (let j = 0; j < ROW_TEMP.length; j++) { // column
- let 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 使每列的宽度固定,据说可以提升性能。
这里看到,表中的数据是在不停增加的,右侧滚动条位置也在变化。滚动卡顿可接受。
但是加载完成后又流畅了,如果不介意的话,就可以直接用了。
那么根据页面优化原则,减少浏览器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++) { // row
- let tr = document.createElement('div');
- tr.className = 'row';
- tr.style.top = (i - 1) * LINE_HEIGHT + 'px';
-
- for (let j = 0; j < ROW_TEMP.length; j++) { // column
- let 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>
可以看到刚开始加载的时候还是很流畅的,随着数据增多,滚动也变得卡了起来。且在加载中,下面未加载的数据都是空白,用户体验相对差一些(即使做了加载进度百分比提示)
问题是这个方法在表格加载完成后,也巨卡。
那么能不能用懒加载的形式加载数据呢,比如我滚动到哪个位置,哪里的数据就开始加载,继续尝试
正是使用了绝对定位自己实现了表格,所以懒加载可以轻易实现。否则普通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++) { // row
- let row = document.createElement('div');
- row.className = 'row';
- row.style.top = i * LINE_HEIGHT + 'px';
-
- for (let j = 0; j < ROW_TEMP.length; j++) { // column
- let 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初级实现 和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++) { // row
- let row = document.createElement('tr');
- row.className = 'row';
-
- for (let j = 0; j < ROW_TEMP.length; j++) { // column
- let 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 data
- clearInterval(interval);
- tableContent.removeEventListener('scroll', scrollEvent); // remove scroll listener to improve performance
- LOADED_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值会出现问题
其中有几个关键的变量与加载性能挂钩,如下
AUTO_LOAD_MS:自动加载时,每次加载的间隔时间,值越小,则渲染线程执行完后,剩下的时间给js就越短。
PRELOAD_PAGES: 懒加载时,预加载的页数,如拉滚动条瞬间跳转到第10页,如果这个值设置为2,则会预加载11,12页的内容。
ROWS: 加载的记录数,直接影响页面性能,不过多介绍
加载过程中有点卡,但是加载完成后就很流畅了。
接下来就是讨论overflow:hidden的问题,一个表格中td内容总会溢出td的,那既然over-flow:hidden; 这么影响性能,如何解决表格内容溢出问题呢。
大体思考了几个方向。
有关大数据表格加载,这个方向大体上是有了,之后的代码我就放到github上了,欢迎指导 GitHub - 601286825nj/big-table
若有错误/补充,敬请指出更改
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。