写一个栗子看看ivew table
承载的数据边界是多少
笔者写了一个简单的栗子来,测试页面卡顿的情况,新建一个index.html
,贴上关键代码
<html> ... <link rel="stylesheet" type="text/css" href="http://unpkg.com/view-design/dist/styles/iview.css" /> <script type="text/javascript" src="./js/vue.min.js"></script> <script type="text/javascript" src="./js/iview.min.js"></script> <script type="text/javascript" src="./js/mock-min.js"></script> <script type="text/javascript" src="./js/axios.min.js"></script> <script type="text/javascript" src="./mockserver.js"></script> <style> #app { margin: 10px; } </style> <body> <div id="app"> <Row align="middle" type="flex" gutter="10"> <i-col span="24"><h2>iview-table性能优化测试</h2></i-col> <i-col span="3"> pageNum<i-input v-model.number="pageParams.pageNum"></i-input> </i-col> <i-col span="3"> pageSize<i-input v-model.number="pageParams.pageSize"></i-input> </i-col> <i-col span="3"> total<i-input v-model.number="pageParams.total"></i-input> </i-col> <i-col span="3"> <i-button type="primary" @click="handleReflush">刷新</i-button> </i-col> </Row> <i-table row-key="id" :loading="loading" :columns="columns" :data="tableData" border ></i-table> <Page :total="pageParams.total" @on-change="handleChangePage" show-sizer ></Page> </div> </body> </html>
新建一个index.js
,引入页面中
<html> .... <body> ... <div id="app"> ... <Page :total="pageParams.total" @on-change="handleChangePage" show-sizer ></Page> </div> <script src="./index.js"></script> </body> </html>
我本地新建一个模拟接口数据的操作,这里笔者用了一个`mockjs`[1]造数据,使用axios
这个库做ajax
请求
具体看下index.js
这个主页面的js
// index.js var vm = new Vue({ el: "#app", data: { loading: false, tableData: [], pageParams: { pageNum: 1, pageSize: 10, total: 10, }, columns: [ { title: "序号", type: "index", }, { title: "Name", key: "name", tree: true, }, { title: "age", key: "age", }, { title: "address", key: "adress", }, ], }, methods: { // todo 请求数据 featchData() { const { pageParams } = this; this.loading = true; this.tableData = []; let timer; mockServer("http://test.com", pageParams).then((res) => { const { data: { result }, } = res; console.log(res, "=res"); this.tableData = result; if (timer) { clearTimeout(timer); } // todo 模拟数据延时loading timer = setTimeout(() => { this.loading = false; }, 2000); }); }, // todo 点击按钮刷新操作 handleReflush() { this.featchData(); }, // 分页参数请求 handleChangePage(pageNum) { this.pageParams = { ...this.pageParams, pageNum, }; this.featchData(); }, }, mounted() { this.featchData(); }, });
以上代码片段有些长,但是核心思想非常简单,我模拟了一个页面列表需要的数据以及入参请求的分页参数,列表会根据我设置的分页参数,请求拿到数据,渲染到页面中。
接下来看下mockserver.js
这个是一个模拟接口的一个工具库,可以看下片段
// 生成mock数据 const mockServer = (path, { pageNum, pageSize, total }) => { // 生成随机长度的数组 const createMapRandom = (len) => { const data = new Array(len); return data.fill('Maic') } const childrenData = Mock.mock({ [`data|${Math.floor(total / pageSize)}`]: [ { "name|1": createMapRandom(100).map(() => Mock.mock("@cname")), "age|1": createMapRandom(100).map(() => Mock.mock("@integer(0,100)")), "adress|1": createMapRandom(100).map(() => Mock.mock("@city")), "id|1": createMapRandom(100).map(() => Mock.mock("@id")), }, ], }); Mock.mock(path, { code: 0, message: "成功", [`result|${pageNum * pageSize}`]: [ { "name|+1": createMapRandom(10).map(() => Mock.mock("@cname")), "age|1": createMapRandom(10).map(() => Mock.mock("@integer(0,100)")), "adress|1": createMapRandom(10).map(() => Mock.mock("@city")), "id|1": createMapRandom(10).map(() => Mock.mock("@id")), children: childrenData.data, }, ], }); return axios.get(path) }
mock
数据已经准备ok,我们看下页面就是这样的
打开控制台netWork
的perfomance monitor
可以看到js heap size
右侧非常平稳(这里可以看到页面内存溢出情况,如果是平稳的,说明内存溢出的可能性很小),在10
条数据时候,页面也非常流畅
当我把总条数调至100
时
cpu
在我修改总条数,然后点击刷新按钮操作,cpu
和内存
都有往上飙升了,但是内存溢出依然不是很明显,点击页面也并无卡顿。
当我把页面总数调至500
时,此时页面内存溢出和cpu又是怎么样
当我点击页面刷新按钮操,然后点击列表的树操作时,页面已经有明显的卡顿了,但页面没有卡死,当我直接把总条数修改1000
时,整个页面已经卡死。
500
条数据就已经感受到页面卡顿了,当为1000
条时,页面直接卡死,因此在测试同学极限测试的情况下,生产环境页面直接崩了,这时候,你不可能跟测试说,你为啥要造那么多数据?
在极端情况下,也许就是有测试的这种情况,看了官方文档,临时做了一个补救方案,就是点击那个tree
的时候,再异步加载children
数据,但是...,第二天测试告诉我,页面又崩了,于是,这种方式是不行了,那么加个页面吧,把树的子集页面用一个弹框页面展示,这样首页只加载第一级数据,二级数据让后端做了个分页查询,再给前端渲染。
终于这样页面不卡顿了,测试添加1000
条数据,页面不卡顿了,但是为啥ivew
的table渲染数据,会造成页面内存溢出如此严重,去官方github
上看了一下table组件的源码
在ivew
的table
组件,是用render
,根据columns
生成colgroup
,根据data
生成tr
、td
,具体可以看下table-body[2]
... render(h) { let $tableTrs = []; this.data.forEach((row, index) => { let $tds = []; const $tableTr = h(TableTr, { props: { draggable: this.draggable, row: row, 'prefix-cls': this.prefixCls }, key: this.rowKey ? row._rowKey : index, nativeOn: { mouseenter: (e) => this.handleMouseIn(row._index, e), mouseleave: (e) => this.handleMouseOut(row._index, e), click: (e) => this.clickCurrentRow(row._index, e), dblclick: (e) => this.dblclickCurrentRow(row._index, e), contextmenu: (e) => this.contextmenuCurrentRow(row._index, e), selectstart: (e) => this.selectStartCurrentRow(row._index, e) } }, $tds); // 子数据 if (row.children && row.children.length) { const $childNodes = this.getChildNode(h, row, []); $childNodes.forEach(item => { $tableTrs.push(item); }); } ... }) ... }
在循环data
中创建tr
,而且还有递归寻找getChildNode
操作,tr
上还绑定了许多事件,当我们点击tree
时,会触发tr
的mouseenter,click等事件,如此多的事件绑定在tr
上,在数据量很大的时候,绑定的事件越多,造成内存泄漏越是严重,而且是每个tr
上都是直接绑定nativeOn
等这些事件。所以ivew
的table造成内存的泄漏直接让页面卡死。
ivew
的table既然这么不经打,那么我测试下elementUI
的table
是否比ivew
更好。
笔者糊了一个一模一样的测试页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>element-table</title> <style> #app { margin: 10px; } </style> <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css" /> <script type="text/javascript" src="./js/vue.min.js"></script> <script src="https://unpkg.com/element-ui/lib/index.js"></script> <script type="text/javascript" src="./js/mock-min.js"></script> <script type="text/javascript" src="./js/axios.min.js"></script> <script type="text/javascript" src="./mockserver.js"></script> </head> <body> <div id="app"> <el-row type="flex"> <el-col span="5"><h2>element-table性能优化测试</h2></el-col> <el-col span="3"> pageNum<el-input v-model.number="pageParams.pageNum"></el-input> </el-col> <el-col span="3"> pageSize<el-input v-model.number="pageParams.pageSize"></el-input> </el-col> <el-col span="3"> total<el-input v-model.number="pageParams.total"></el-input> </el-col> <el-col span="3"> <el-button type="primary" @click="handleReflush">刷新</el-button> </el-col> </el-row> <el-table row-key="id" :data="tableData" :tree-props="{children: 'children', hasChildren: 'hasChildren'}" border > <el-table-column type="index" label="序号" width="50"> </el-table-column> <el-table-column v-for="(item) in columns" :prop="item.key" :label="item.title"> </el-table-column> </el-table-column> </el-table> <el-pagination :total="pageParams.total" :page-size="pageParams.pageSize" :page-sizes="[10, 20, 30, 40]" @current-change="handleChangePage" ></el-pagination> </div> <script> var vm = new Vue({ el: "#app", data: { loading: false, tableData: [], pageParams: { pageNum: 1, pageSize: 10, total: 10, }, columns: [ { title: "Name", key: "name", tree: true, }, { title: "age", key: "age", }, { title: "address", key: "adress", }, ], }, methods: { // todo 请求数据 featchData() { const { pageParams } = this; this.loading = true; this.tableData = []; let timer; mockServer("http://test.com", pageParams).then((res) => { const { data: { result }, } = res; console.log(res, "=res"); this.tableData = result; if (timer) { clearTimeout(timer); } // todo 模拟数据延时loading timer = setTimeout(() => { this.loading = false; }, 2000); }); }, // todo 点击按钮刷新操作 handleReflush() { this.featchData(); }, // 分页参数请求 handleChangePage(pageNum) { this.pageParams = { ...this.pageParams, pageNum, }; this.featchData(); }, }, mounted() { this.featchData(); }, }); </script> </body> </html>
打开浏览器,直接设置1000
,elementUI
的table
真的吊打ivew
几条街
cpu几乎没有变化多少,内存泄漏也是几乎没有,在一段时间内,几乎是保持不变的。
用5000
调试,页面有稍微卡顿了,10000
条数据测试,终于把页面搞崩了。点击tree
页面明显卡顿,但即使是这样也比ivew
1000条的测试数据页面要好得多。
由此可见笔者已经把ivew
table最大的问题踩了一个坑。 关于elementUI
的table可以去官方库看下,比ivew
处理要优雅得多,具体参考ele-body[3]
看到这里,如果table
大数据渲染,有没有比较好的实践方案。因为1w条数据的情况,即使是elementUI
这么能扛也显得力不从心。
虚拟长列表方案优化
虚拟列表优化,这是大数据量优化的一种手段,大数据渲染dom导致页面卡顿,我们尝试用虚拟长列表方案去实践下
为快速实现业务table
性能,我们采用第三方虚拟列表`umy-ui`[4],专门解决table卡顿问题
新建一个index-vitual.html
... <u-table ref="table" :data="tableData" :height="height" use-virtual :row-height="rowHeight" :treeConfig="{ children: 'children', expandAll: false, lazy: true }" row-id="id" border > <u-table-column type="index" width="100" label="序号" fixed ></u-table-column> <u-table-column v-for="item in columns" :key="item.key" :prop="item.key" :tree-node="item.hasChildren" :label="item.title" > </u-table-column> </u-table> ...
// js // 引入UMYUI 组件 const { UTable, UTableColumn } = UMYUI; var vm = new Vue({ el: "#app", components: { UTable, UTableColumn, }, ... })
就是引入umy-ui
的两个组件即可,主要注意u-table
的几个props
1、use-virtual
主要是打开虚拟列表
2、height
设置一个固定的高度,或者设置一个max-height
,如果不需要设置高度,内容需根据内容滚动,则关闭虚拟列表use-virtual
这个参数不设置即可
3、treeConfig
这个参数设置是否有tree
,当设置树时u-table
上必须设置row-id="id"
,否则树不会出来,并且cloumns
上设置hasChildren
标识
4、u-table-column
设置:tree-node
属性,指定列中哪个props
展开
更多API可以参考官网[5]
当我们将参数调节至首页1000条时,其实table
的tr始终中16条左右
用该方案极大的减少了列表dom
的渲染,避免了一次性渲染1000个tr,td
。因此极大的提升了table
的渲染,页面的性能也会提升不少。
最后,如果你将总条数调至10000,你最后还是会发现页面cpu
直接上升至100%
,页面明显的卡顿了几秒钟,这也表明,此时无论页面是否虚拟列表方案,造成页面卡顿与js
声明数据量也有一定关系,当定义的数据过大,在内存没有释放的这段过程中,如果造成页面内存溢出或者堆栈过大,那么也会造成页面的卡死。
总结
1、ivew
的table
性能很差,比较elementUI
,1000
数据ivew
就能让你浏览器崩掉,所以慎用ivew
table的大数据量,有坑
2、elementUI
的table组件很优秀,1000
条能扛得住,但上了5000
后,就明显扛不住了,所以采用umy-ui
虚拟列表渲染
3、umy-ui
方案可以极大的优化大数据table
渲染,但是数据量超过1w+,甚至更多,那么虚拟列表也是没得救了,页面依然会卡顿。
因此造成页面卡顿的因素很多,我们减少事件操作、闭包、全局变量等等这些尽量减少内存的消耗,以及页面的GUI
渲染,这样就可以极大提高页面的访问性能。
关于虚拟长列表方案
,后续我会写一篇深究虚拟长列表的技术文章,除了这种方案优化table,笔者想到,另外两种方案
一种是假分页,如果后端一次性返回了1000
条数据,那么我在前端按照上拉滚动的方式,每次加载100条
的方式去渲染,这样分10页
就可以加载完毕了,比起一次性加载1w+
应该会有明显的提升,后续会写个测试demo验证一下。
二种是增加二级页面,将大数据做本地indexDB
存储,然后从indexDB
中做前端分页查询。