关键点: 不卡顿, 交互流畅
一, 最传统, 最简单粗暴的方式
- <!DOCTYPE html>
- <HTML lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <meta http-equiv="X-UA-Compatible" content="ie=edge">
- <title>
- 如何渲染 10000 条数据在 dom 节点上
- </title>
- </head>
- <body>
- <ul id="root">
- </div>
- <script>
- function createOneHundredThousandData() {
- let arr = [];
- for (let i = 0; i < 100000; i++) {
- arr.push({
- imgUrl: 'https://zos.alipayobjects.com/rmsportal/hfVtzEhPzTUewPm.png',
- key: i
- })
- }
- return arr;
- }
- var beginTime = performance.now();
- console.log('beginTime', beginTime);
- let h = [];
- let data = createOneHundredThousandData()
- // 写法 1 原生 JS 的 for 循环
- for (let i = 0; i < data.length; i++) {
- h.push('<li>' + '<img src="' + data[i].imgUrl + '"\/>' + 'current index' + data[i].key + '<\/li>');
- }
- // 写法 2 数组自带的 map 方法
- // h = data.map((item,index)=>'<li>' + '<img src="'+ item.imgUrl +'"\/>'+ 'current index' + item.key + '<\/li>');
- document.getElementById('root').innerHTML = h.join('');
- document.addEventListener('DOMContentLoaded',
- function() {
- var endTime = performance.now();
- console.log('DOMContentLoaded endTime', endTime);
- var total = ((endTime - beginTime) / 1000).toFixed(5);
- console.log('DOMContentLoaded render 100000 items takes' + total + '秒');
- });
- Windows.onload = function() {
- var endTime = performance.now();
- console.log('window.onload endTime', endTime);
- var total = ((endTime - beginTime) / 1000).toFixed(5);
- console.log('window.onload render 100000 items takes' + total + '秒');
- }
- </script>
- </body>
- </HTML>
Chrome 浏览器 (版本 74.0.3729.169(正式版本) (64 位)) 控制台运行结果如下
- beginTime 398.8050000043586
- DOMContentLoaded endTime 9032.814999984112
DOMContentLoaded render 100000 items takes 8.63401 秒
Windows.onload endTime 17766.104999987874
Windows.onload render 100000 items takes 17.36730 秒
也就是说, 渲染包含十万条记录, 每一条数据仅仅只有图片和文字的简单组合, 就要花费将近 17 秒. 页面渲染完成之前, 估计用户早已不耐烦, 关掉该页面了. 这还是版本较新的 Chrome 浏览器. 换做其他浏览器, 可能效果更差. 很显然, 传统的方式肯定不合格.
关于上述 demo, 有几个问题可以补充说明一下:
1, 用 innerHtml 插入 dom, 而不是用 document.createElement,document.appendChild, 这两者性能上来说, innerHtml 优势明显
2, 用数组[] 来缓存 dom 字符串, 先 push 进来, 最后再直接 jion(''), 将数组里面每一项串联成字符串, 比一个一个字符串拼接的性能要强很多
3, 循环一个数组对象, 可以用 for 循环, 也可以用 map,forEach 等, 数据量少的时候两者差别不大, 在此例中, 可以看到 map 来循环十万条数据时间性能稍逊于普通 for 循环.
4, 插入 dom 节点, 还可以使用克隆技术, 文档断片 createDocumentFragment, 其根本目的在于尽可能减少 dom 操作次数, 从而使得重绘跟重排带来的性能影响降到最低. 若读者有兴趣深入研究, 可以查阅《高性能 JavaScript》(猫头鹰头像的封面).
5, 关于 DOMContentLoaded 事件和 Windows.onload 事件的对比, 也是页面渲染过程中比较关键的, 需要重点搞清楚的地方. 简单来说, DOMContentLoaded 表示 dom 家在完成, 通俗来说, 就是 dom 标签堆砌完毕, 至于 dom 标签引用什么资源, 有没有请求加载完毕, 那就不管了. 比如在此例子中, 十万条数据 img 标签堆上去, 不需要等到 img src 指向的资源全部加载完就可以触发 DOMContentLoaded; 而 Windows.onload 事件则不一样, 要等到全部的 src 指向的资源全部加载完才会被触发.
DOMContentLoaded 也多用于关键路径优化中(首屏操作优化), 因为页面 dom 加载完了就得给用户提供一些交互. 不能出现让用户看到 UI 界面却做不了任何交互操作的情况.
二, 解决卡顿问题之 setTimeout
卡顿, 多半是优于用户发起一个操作, 到页面响应这个操作, 把 UI 结果反馈给用户这个时间存在明显的延迟. 给人不流畅的用户体验.
从 JavaScript 这门语言来看, 它是单线程的, 注定了同一时间, 该线程只能处理一个任务, 该任何处理完毕后才能处理下一个任务, 你可以理解为串行执行.(更详细更严谨的, 可以去深入地了解 JavaScript Event Loop). 所以, 当页面在执行渲染, 或者很耗时 JavaScript 操作, 该操作还没完成, 而此时你在页面发起交互, 就得不到及时的响应.
回到此题, 我们在渲染十万掉数据的时候, 要用到切片(有点类似 react fiber 的思想). 怎么理解呢? 就是把十万掉数据分批次的渲染到页面, 这个批次任务必须放到异步回调(首批任务不用), 这样才能在后续的渲染中, 把优先级让出给执行队列线程, 当执行队列空闲时, 再回过头来继续取出异步回调里面的切片来执行
- <!DOCTYPE HTML>
- <HTML lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <meta http-equiv="X-UA-Compatible" content="ie=edge">
- <title>
- 如何渲染 10000 条数据在 dom 节点上
- </title>
- </head>
- <body>
- <ul id="root">
- </div>
- <script>
- function createOneHundredThousandData() {
- let arr = [];
- for (let i = 0; i < 100000; i++) {
- arr.push({
- imgUrl: 'https://zos.alipayobjects.com/rmsportal/hfVtzEhPzTUewPm.png',
- key: i
- })
- }
- return arr;
- }
- var beginTime = performance.now();
- console.log('beginTime', beginTime);
- let h = [];
- let data = createOneHundredThousandData();
- // 先渲染 100 条数据
- let firstScreenData = data.splice(0, 100); // 用数组的 splice 方法, 截取后并修改原数组
- for (let i = 0; i < 100; i++) {
- let li = document.createElement('li');
- let img = document.createElement('img');
- img.src = firstScreenData[i].imgUrl;
- li.appendChild(img);
- let text = document.createTextNode(firstScreenData[i].key);
- // console.log('partialData[i].key',partialData[i].key);
- li.appendChild(text);
- document.getElementById('root').appendChild(li);
- }
- // setTimeout 中的回调会在主线程空闲时被执行
- setTimeout(() = >{
- function renderHundred(n) {
- // console.log('n=',n);
- // 每次渲染 100 条
- let partialData = data.splice(0, 100);
- for (let i = 0; i < 100 && partialData.length > 0; i++) {
- let li = document.createElement('li');
- let img = document.createElement('img');
- img.src = partialData[i].imgUrl;
- li.appendChild(img);
- let text = document.createTextNode(partialData[i].key);
- // console.log('partialData[i].key',partialData[i].key);
- li.appendChild(text);
- document.getElementById('root').appendChild(li);
- }
- if (n) {
- setTimeout(() = >{
- renderHundred(n - 1);
- },
- 50)
- }
- }
- renderHundred(999); // 渲染除了首屏数据外的数据
- },
- 1000);
- document.addEventListener('DOMContentLoaded',
- function() {
- var endTime = performance.now();
- console.log('DOMContentLoaded endTime', endTime);
- var total = ((endTime - beginTime) / 1000).toFixed(5);
- console.log('DOMContentLoaded render 100000 items takes' + total + '秒');
- });
- Windows.onload = function() {
- var endTime = performance.now();
- console.log('window.onload endTime', endTime);
- var total = ((endTime - beginTime) / 1000).toFixed(5);
- console.log('window.onload render 100000 items takes' + total + '秒');
- }
- </script>
- </body>
- </HTML>
运行结果如下:
- beginTime 139.08000002265908
- DOMContentLoaded endTime 193.2200000155717
DOMContentLoaded render 100000 items takes 0.05414 秒
Windows.onload endTime 207.63000001898035
Windows.onload render 100000 items takes 0.06855 秒
这个数据体会不出来什么信息, 可以理解为首个切片实行的耗时统计(后面还有 999 个切片没有体现出来), 但是在交互方面的体验大大提升了. 至于具体卡不卡顿的数据支撑, 可以在 Chrome 控制台 performance 模块查看. 大家有兴趣的话, 拷贝这份代码尝试一下. 结论还是比较乐观的.
三, 除了 setTimeout, 还有其他的选择吗?
答案是, 必须有. 那就是 requestAnimationFrame
Windows.requestAnimationFrame() 告诉浏览器 -- 你希望执行一个动画, 并且要求浏览器在下次重绘之前调用指定的回调函数更新动画. 该方法需要传入一个回调函数作为参数, 该回调函数会在浏览器下一次重绘之前执行
注意: 若你想在浏览器下次重绘之前继续更新下一帧动画, 那么回调函数自身必须再次调用 Windows.requestAnimationFrame()
从官方文档可以看到, 这么几个关键字:"传入一个回调函数". 那么是不是可以用这个取代 setTimeout?
以下截取部分代码:
- let data = createOneHundredThousandData();
- let count = 0;
- let totalLoop = 1000;// 渲染 1000
- function animatonCb(){
- console.log(count);
- let partialData = data.splice(0,100); // 用数组的 splice 方法, 截取后并修改原数组
- for(let i=0;i<100 && partialData.length>=1;i++){
- let li = document.createElement('li');
- let img = document.createElement('img');
- img.src = partialData[i].imgUrl;
- li.appendChild(img);
- let text = document.createTextNode(partialData[i].key);
- // console.log('partialData[i].key',partialData[i].key);
- li.appendChild(text);
- document.getElementById('root').appendChild(li);
- }
- if(count < totalLoop){
- count ++;
- requestAnimationFrame(animatonCb)
- }
- }
- requestAnimationFrame(animatonCb);
看下控制台数据:
- beginTime 249.32000000262633
- 0
- DOMContentLoaded endTime 279.33499999926426
DOMContentLoaded render 100000 items takes 0.03001 秒
- 1
- 2
- Windows.onload endTime 308.28500000643544
Windows.onload render 100000 items takes 0.05897 秒
我们假如了循环次数 count 的打印, 发现这个穿插在了 DOMContentLoaded 和 onload 事件中间. 有兴趣的童鞋可以深入了解 requestAnimationFrame.
总之, 这个 requestAnimationFrame 也能实现我们的需求. 相比于 setTimeout 更好一点.
四, 十万条数据加载完成后呢?
上述两个方案, 也就是解决了如何渲染不卡顿的问题. 本例中每条记录 dom 结构不复杂, 可能看起来效果还行. 但实际业务场景肯定是比这个更复杂. 每次修改 dom 都会引起 10 万条数据但重回重排, 这样性能方面肯定也会有问题.
解决思路就是, 监听该元素是否在可视窗口 IntersectionObserver
IntersectionObserver 接口 (从属于 Intersection Observer API) 提供了一种异步观察目标元素与其祖先元素或顶级文档视窗 (viewport) 交叉状态的方法. 祖先元素与视窗 (viewport) 被称为根(root). 当一个 IntersectionObserver 对象被创建时, 其被配置为监听根中一段给定比例的可见区域. 一旦 IntersectionObserver 被创建, 则无法更改其配置, 所以一个给定的观察者对象只能用来监听可见区域的特定变化值; 然而, 你可以在同一个观察者对象中配置监听多个目标元素.
大概思路如下:
设置总数据源, 页面内容数据存储容器
制定页面内容数据存储容器规则(假设存储容器设置为 200 条, 一屏最多展示 20 条. 那么存储容器能展示 10 屏幕的数据.
当用户滑到地 6 屏数据的时候, 显然前面 5 屏数据不在可视窗口, 那你可以将存储容器的前 3 屏数据删除. 同时, 再从总数据源取第 11 屏到第 13 屏数据.
后续有空了再详细研究这块.
结语
此类问题, 也是 bat 这种大厂经常会问到的, 知识点涵盖也点广, 掌握好了后, 对前端性能, 卡顿这块的理解会更透彻了.
来源: https://www.cnblogs.com/ldld/p/11028179.html