纳尼?后端要一次性返回我10万条数据!且看我这8种方案机智应对


问题描述

问题考察点

看似无厘头的问题,实际上考查候选人知识的广度和深度,虽然在工作中这种情况很少遇到...

文末会提供完整代码,供大家更好的理解

使用express创建一个十万条数据的接口

若是道友对express相关不太熟悉的话,有空可以看看笔者的这一篇全栈文章(还有完整代码哦):《Vue+Express+Mysql全栈项目之增删改查、分页排序导出表格功能》

js复制代码route.get("/bigData", (req, res) => {
  res.header('Access-Control-Allow-Origin', '*'); // 允许跨域
  let arr = [] // 定义数组,存放十万条数据
  for (let i = 0; i < 100000; i++) { // 循环添加十万条数据
    arr.push({
      id: i + 1,
      name: '名字' + (i + 1),
      value: i + 1,
    })
  }
  res.send({ code: 0, msg: '成功', data: arr }) // 将十万条数据返回之
})

点击按钮,发请求,获取数据,渲染到表格上

html结构如下:

html复制代码点击请求加载


  
  
  
  


data() {
    return {
      arr: [],
      loading: false,
    };
},

async plan() {
    // 发请求,拿数据,赋值给arr
}

方案一 直接渲染所有数据

如果请求到10万条数据直接渲染,页面会卡死的,很显然,这种方式是不可取的

js复制代码 async plan() {
      this.loading = true;
      const res = await axios.get("http://ashuai.work:10000/bigData");
      this.arr = res.data.data;
      this.loading = false;
}

方案二 使用定时器分组分批分堆依次渲染(定时加载、分堆思想)

用户所看到的效果图是如下

效果图

分组分批分堆函数

分组分批分堆函数(一堆分10个)

js复制代码function averageFn(arr) {
  let i = 0; // 1. 从第0个开始截取
  let result = []; // 2. 定义结果,结果是二维数组
  while (i < arr.length) { // 6. 当索引等于或者大于总长度时,即截取完毕
    // 3. 从原始数组的第一项开始遍历
    result.push(arr.slice(i, i + 10)); // 4. 在原有十万条数据上,一次截取10个用于分堆
    i = i + 10; // 5. 这10条数据截取完,再截取下十条数据,以此类推
  }
  return result; // 7. 最后把结果丢出去即可
}

创建定时器去依次赋值渲染

比如我们每隔一秒钟去赋值渲染一次

js复制代码  async plan() {
      this.loading = true;
      const res = await axios.get("http://ashuai.work:10000/bigData");
      this.loading = false;
      let twoDArr = averageFn(res.data.data);
      for (let i = 0; i < twoDArr.length; i++) {
        // 相当于在很短的时间内创建许多个定时任务去处理
        setTimeout(() => {
          this.arr = [...this.arr, ...twoDArr[i]]; // 赋值渲染
        }, 1000 * i); // 17 * i // 注意设定的时间间隔... 17 = 1000 / 60
      }
    },

这种方式,相当于在很短的时间内创建许多个定时任务去处理,定时任务太多了,也耗费资源啊。

实际上,这种方式就有了大数据量分页的思想

方案三 使用requestAnimationFrame替代定时器去做渲染

关于requestAnimationFrame比定时器的优点,道友们可以看笔者的这篇文章:《性能优化之通俗易懂学习requestAnimationFrame和使用场景举例》

反正大家遇到定时器的时候,就可以考虑一下,是否可以使用请求动画帧进行优化执行渲染?

如果使用请求动画帧的话,就要修改一下代码写法了,前面的不变化,plan方法中的写法变一下即可,注意注释:

js复制代码async plan() {
  this.loading = true;
  const res = await axios.get("http://ashuai.work:10000/bigData");
  this.loading = false;
  // 1. 将大数据量分堆
  let twoDArr = averageFn(res.data.data);
  // 2. 定义一个函数,专门用来做赋值渲染(使用二维数组中的每一项)
  const use2DArrItem = (page) => {
    // 4. 从第一项,取到最后一项
    if (page > twoDArr.length - 1) {
      console.log("每一项都获取完了");
      return;
    }
    // 5. 使用请求动画帧的方式
    requestAnimationFrame(() => {
      // 6. 取出一项,就拼接一项(concat也行)
      this.arr = [...this.arr, ...twoDArr[page]];
      // 7. 这一项搞定,继续下一项
      page = page + 1;
      // 8. 直至完毕(递归调用,注意结束条件)
      use2DArrItem(page);
    });
  };
  // 3. 从二维数组中的第一项,第一堆开始获取并渲染(数组的第一项即索引为0)
  use2DArrItem(0); 
},

方案四 搭配分页组件,前端进行分页(每页展示一堆,分堆思想)

这种方式,笔者曾经遇到过,当时的对应场景是数据量也就几十条,后端直接把几十条数据丢给前端,让前端去分页

后端不做分页的原因是。他当时临时有事情请假了,所以就前端去做分页了。

js复制代码getShowTableData() { 
    // 获取截取开始索引 
    let begin = (this.pageIndex - 1) * this.pageSize; 
    // 获取截取结束索引
     let end = this.pageIndex * this.pageSize; 
    // 通过索引去截取,从而展示
    this.showTableData = this.allTableData.slice(begin, end); 
}

完整案例代码,请看笔者的这篇文章:《后端一次性返回所有的数据,让前端截取展示做分页》

实际上,这种大任务拆分成许多小任务,这种方式,做法,应用的思想就是分片的方式(时间),在别的场景,比如大文件上传的时候,也有这种思想,比如一个500MB的大文件,拆分成50个小文件,一个是10MB这样...至于大文件上传的文章,那就等笔者有空了再写呗...

方案五 表格滚动触底加载(滚动到底,再加载一堆)

这里重点就是我们需要去判断,何时滚动条触底。判断方式主要有两种

目前市面上主流的一些插件的原理,大致是这两种。

笔者举例的这是,是使用的插件v-el-table-infinite-scroll,本质上这个插件是一个自定义指令。对应npm地址:www.npmjs.com/package/el-…

当然也有别的插件,如vue-scroller 等:一个意思,不赘述

注意,触底加载也是要分堆的,将发请求获取到的十万条数据,进行分好堆,然后每触底一次,就加载一堆即可

在el-table中使用el-table-infinite-scroll指令步骤

安装,注意版本号(区分vue2和vue3)

cnpm install --save el-table-infinite-scroll@1.0.10

注册使用指令插件

js复制代码// 使用无限滚动插件
import elTableInfiniteScroll from 'el-table-infinite-scroll';
Vue.use(elTableInfiniteScroll);

因为是一个自定义指令,所以直接写在el-table标签上即可

js复制代码
  
  


async load() {
    // 触底加载,展示数据...
},

案例代码

为了方便大家演示,这里笔者直接附上一个案例代码,注意看其中的步骤注释即可

html复制代码


效果图

方案六 使用无限加载/虚拟列表进行展示

什么是虚拟列表?

关于前端障眼法,在具体工作中,如果能够巧妙使用,会大大提升我们的开发效率的

写一个简单的虚拟列表

效果图

这里笔者直接上代码,大家复制粘贴即可使用,笔者写了一些注释,以便于大家理解。当然也可以去笔者的仓库中去瞅瞅哦,GitHub仓库在文末

代码

html复制代码


使用vxetable插件实现虚拟列表

如果不是列表,是table表格的话,笔者这里推荐一个好用的UI组件,vxetable,看名字就知道做的是表格相关的业务。其中就包括虚拟列表。

vue2和vue3版本都支持,性能比较好,官方说:虚拟滚动(最大可以支撑 5w 列、30w 行)

强大!

官方网站地址:vxetable.cn/v3/#/table/…

效果图

效果很丝滑

安装使用代码

注意安装版本,笔者使用的版本如下:

cnpm i xe-utils vxe-table@3.6.11 --save

main.js

js复制代码// 使用VXETable
import VXETable from 'vxe-table'
import 'vxe-table/lib/style.css'
Vue.use(VXETable)

代码方面也很简单,如下:

html复制代码


方案七 开启多线程Web Worker进行操作

本案例中,使用Web Worker另外开启一个线程去操作代码逻辑,收益并不是特别大(假如使用虚拟滚动列表插件的情况下)

不过也算是一个拓展的思路吧,面试的时候,倒是可以说一说,提一提。

对Web Worker不熟悉的道友们,可以看看笔者之前的这篇文章:《性能优化之使用vue-worker插件(基于Web Worker)开启多线程运算提高效率》

方案八 未雨绸缪,防患于未然

以下为笔者愚见,仅供参考...

如果笔者是候选人,笔者在说了上述7种方案以后,会再补充第八种方案:未雨绸缪,防患于未然


场景模拟

面试官随意打量着其手中我的简历,抚须怪叫一声:“小子,后端要一次性返回10万条数据给你,你如何处理?”

我眉毛一挑,歪嘴一笑:“在上述7种方案陈述完以后,我想类似的问题,我们可以从根本上去解决。即第八种方案,要未雨绸缪,防患于未然。”

“哦?”面试官心中疑惑,缓缓放下我的简历:“愿闻其详。”

我不紧不慢地答道:“在具体开发工作中,我们在接到一个需求时,在技术评审期间,我们就要和后端去商量比较合适的技术解决方案。这个问题是后端要一次性返回我10万条数据,重点并不在10万条这么多数据,而在于后端为什么要这样做?”

面试官抬头,瞳孔中倒映出我的身影,认真听了起来。

我一字一顿地说道:“除去**业务真正需要这种方案**的话(若是客户要求的,那就没啥好说的,干就完了),后端这样做的原因大致有两种,第一种他不太懂sql的limit语句,但这基本不可能,第二种就是他有事情,随便敷衍写了一下。所以,就是要和他沟通,从大数据量接口请求时长过长,以及过多的dom元素渲染导致性能变差,以及项目的可维护性等角度去沟通,我相信只要正确的沟通,就能从根源上去避免这种不太合理的情况发生。”

面试官又突然狡黠地发问:“要是沟通以后,后端死活不给你分页呢?你咋办?嗨嗨!你的沟通无效果!你如何处理!人家不听你的!”似乎是觉得这个问题很刁钻,他双臂抱在胸前,靠在椅背上,发出桀桀桀的诡异笑声,他等待着看到我脸上即将绽放的回答不上来的,尴尬笑容。

我内心冷哼一声:雕虫小技...

我盯着面试官的眼睛,认真说道:“如果工作中沟通无效果,要么是我自己沟通语言表达的问题,这一点我会注意,不断提升自己的沟通技巧和说话方式,要么就是...”

我声音扬起了三分:“我沟通的这个人有问题!他工作摸鱼偷懒耍滑!固执己见!为难他人!高高在上!自以为是!这种情况下,我会找到我的直属领导去介入,因为这已经不是项目的需求问题了,而是员工的基本素养问题!”

停顿了一秒,我声音又柔和了几分:“但是,但是我相信咱们公司员工中是绝对没有这样的人存在的,各个都是能力强悍,态度端正的优秀员工。毕竟咱们公司在行业中久负盛名,我也是因此慕名而来的。您说对吧?”

面试官眼中闪过震惊之色,他没有想到我居然把皮球又踢给他了,不过他为了维持形象,旋即恢复了镇定,只是面部肌肉在止不住的微微颤抖。

“那是当然,公司人才济济。”面试官随口接话道。

我又补充道:“实际上在工作中,前端作为比较贴近用户的角色而言,需要和各个岗位的同事进行沟通,比如后端、产品、UI、测试等。我们需要通过合理的沟通方式,去提升工作效率,完成项目,实现自己的价值,为公司创造收益,我想这是每一个员工需要做的,也是必须要做到的。”

面试官又抚须怪叫一声:“小子表现还行,你被录用了!一个月工资2200,自带电脑,无社无金,007工作制,不能偷吃公司零食,以及...”

我:阿哒...

定位性能瓶颈

凭直观感觉,数据量大是页面渲染的主要性能瓶颈,但是作为开发人员,还是要以客观事实为依据,不能凭感觉做事。那如何定位出页面卡顿的性能瓶颈呢?

其实前端有一些工具可以评估网站的性能,如lighthouse、chrome的devtool的Performance,下面主要是以这两个工具配合着来定位性能问题。

下面针对4000条数据的页面渲染,尝试使用lighthouse和chorme的Performance来进行性能开销定位。

lighthouse给出优化建议

首先,通过lighthouse工具给出的网站性能指标,其中的几项关键指标的定义可以参考Web 指标。如下图所示:

透过该工具,我们可以得到当前页面性能的一个大概得分,它是基于各个指标的分数按照一定的权重比例系列换算而来的,是一个综合性的评价结果,分数越低性能越差。当前页面性能的19分的计算如下图:

lighthouse每次跑的分数有波动,跟当前的网速有一定的关系;不过没有关系,最重要的看它给的优化建议。

针对当前页面,lighthouse给出的优化建议如下图所示:

因为是在本地开发环境跑的lighthouse,所以有些优化项如压缩js等不用关注,我们主要看红框标记的几项,可以说这几项是导致性能差的主要原因,需要重点关注优化。

通过lighthouse的优化建议,总结一下当前页面的主要问题:

针对主线程耗时长的问题,lighthouse会给出了浏览器渲染的整个流程中每一部分的耗时时长,但它不会详细告诉我们每一部分具体耗时在什么地方,这正是chrome devtool的Performance面板的强项。

Performance面板定位耗时真因

Performance用于记录和分析我们的应用在运行时的所有活动,它呈现多维度的数据,可以帮助我们很好地定位性能问题。其中,利用Performance面板main项,展示的是浏览器主线程有关的内容,包括:

如下图是使用chrome Performance面板跑出的结果。

从中可以得知,数据渲染完毕页面耗时近10s,其中js执行耗时花费7.6s左右,从js执行的调用栈看出主要是Microtasks下的花费3.4s的_next和3.94s的fulfilled两部分:

从图上可以看出,fulfilled部分耗时主要是循环添加marker及其popup到地图中并完成渲染。

结合lighthouse和Performance面板的分析,可以定位前端方面影响页面性能的主要原因有:

性能问题拆解

针对上面定位出页面的性能问题,想到的优化解决方案:

大数据列表渲染

分片渲染

针对大数据表格渲染,首先想到的是分片渲染,简单来说就是将大数据量列表划分为n个一组进行渲染,一组称做一个数据片。其设计思路:

建立一个队列,通过定时器来向渲染队列中添加渲染的切片数据。

提示一点,渲染队列中已经渲染的分片在进行渲染时,只是耗费节点diff时间,不会重新渲染。demo如下所示:

code.juejin.cn/pen/7142775…

分片渲染存在下面几个主要问题:

虚拟列表

虚拟列表是解决大数量表格渲染的另一种常见的解决方案,其设计思路是:

只对可视区域内的内容进行渲染,对非可视区域的内容不做渲染或者渲染一部分(俗称缓冲区)

这种方案要处理的是从大量数据中过滤出可视区或者加上缓冲区的数据并渲染,主要是根据滚动事件来进行筛选,其他大部分数据内容不会渲染真正的DOM,这大大减少表格的渲染时间以及页面dom数量,带来的性能提升是非常可观的。

一图胜千言,图片出自这里。

社区对于虚拟列表的介绍方案很多,具体的实现细节这里就不做过多介绍,可以参考下面两篇文章:

项目按照虚拟列表方案优化的参考antd提供的demo。

以上面提到的4000条数据做实验,在没有对地图数据做优化的前提下,使用虚拟列表优化后的效果如下图:

可以看到_next部分执行时间不到1s,并且js执行的时间减少至4621ms,性能提升明显。

地图大数据量渲染

地图marker元素是以dom方式来渲染的,数据量少的情况下没有什么问题,但随着数据量增多,dom渲染的性能越来越吃力,导致页面卡顿甚至崩溃。鉴于地图使用的是leaflet,所以想到的解决方案是用canvas来绘制,至于为什么canvas相较于dom渲染性能得到提升,可以参考这篇文章HTML界的“苏炳添”——详解Canvas优越性能和实际应用]。

因为地图中的marker交互比较简单,点击marker展示对应的popup,所以项目选用leaflet官方推荐的plugins Leaflet.Canvas-Markers来生成marker,该插件需要用图片来设置marker,具体可参考demo。

地图marker渲染优化前:

jsx复制代码
   {
       stops.map((stop) => {
         return ;
        })
    }
 
 
 // CustomMarker实现:
 function CustomMarker(props) {
     ...
     // react-leaflet提供的Marker是以Dom形式渲染的
     
      
        ... // popup内容
      
    
 }

优化后:

jsx复制代码 // 主页面的render部分有关地图部分
 
   
 
 
 // CanvasMarkers实现
 import 'leaflet-canvas-marker';
 function CanvasMarkers({data}) {
  
      const map = useMap();
      const canvasLayerRef = useRef();
      const icon = window.L.icon({
      iconUrl: '图片地址',
      iconSize: [8, 8],
      iconAnchor: [4, 4],
    });
      useEffect(() => {
        if (!data.length) {
          canvasLayerRef.current?.onRemove(map);
          return;
        }
        canvasLayerRef.current = window.L.canvasIconLayer({}).addTo(map);

        const canvasMarkers = [];
        for (let i = 0, len = data.length; i < len; i++) {
          const { lat, lng } = data[i];
          const marker = window.L.marker([lat, lng], {
            icon,
          }).bindPopup(popupHtml);
          canvasMarkers.push(marker);
        }
        canvasLayerRef.current?.addLayers(canvasMarkers);
      }, [markers, icon, map]);

      return null;
 }

通过渲染后的dom结构可以看出,该插件最终将所有的marker元素在同一个canvas中渲染绘制出来。

为了跟上一节效果做对比,同样以4000条数据做实验,在使用虚拟列表优化的同时,使用canvas渲染地图元素优化后的效果如下图所示:

可以看出fulfilled部分执行时间减少至不到100ms,整个js执行时间降至1102ms,canvas渲染的性能提升较可见一斑。

长任务分割

根据前面4000条数据在未进行任何优化的Performance面板分析,js执行的长任务占比7.4s左右,占整个主流程的近75%,主要是表格和地图同时渲染导致,严重影响后面任务的执行。长任务分割的一个好处减少任务的执行时间,可以为后面的任务腾出执行时间。

借鉴于列表的分片渲染方案,可不可以将表格渲染和地图数据按照先后顺序依次渲染呢,并且地图数据采用分片渲染的机制呢?

这在技术上是可行的,而页面在大数据量下可以先让用户看比较重要的数据,然后逐渐渲染出整个页面的内容是可以接受的。

所以,在技术上做了如下两方面优化:

import 'leaflet-canvas-marker'; function CanvasMarkers({data}) { ... const [data, setData] = useState([]); // 地图marker数据,分割渲染核心逻辑 const sliceData = useCallback((list, index: number = 0, num = 100) => { const endIdx = Math.ceil(list.length / num); if (index === endIdx) { return; } setTimeout(() => { // 每200ms执行一个批次的渲染 const toBeRenderList = list.slice(index * num, (index + 1) * num); setData(toBeRenderList); console.log(toBeRenderList, toBeRenderList.length); sliceData(list, index + 1, num); }, 200); }, []); useEffect(() => { const len = markers.length; if (len === 0) { canvasLayerRef.current?.onRemove(map); return; } canvasLayerRef.current = window.L.canvasIconLayer({}).addTo(map); // 分割大小的一个简单设置策略 const sliceLen = len > 100000 ? 2000 : len > 50000 ? 1000 : 500; sliceData(data, 0, sliceLen); }, [markers, map, sliceData]); // 只关注当前data的内容,为其生成L.marker useEffect(() => { if (!data.length) { return; } const canvasMarkers = []; for (let i = 0, len = data.length; i < len; i++) { const { lat, lng } = data[i]; const marker = window.L.marker([lat, lng], { icon, }).bindPopup(popupHtml); canvasMarkers.push(marker); } canvasLayerRef.current?.addLayers(canvasMarkers); }, [data, map]); return null; }

我们以相同的5万条数据来进行实验,在上面两种优化的前提下,未进行长任务分割优化前的Performance面板结果如下图

经过上面两种方式的优化手段,得到的效果如下图:

可以看到页面总的渲染时间变长了,这是为何?

前面说了分片渲染会增加总的渲染耗时,因为每批次数据在指定的计时器间隔进行异步渲染,所以会拉长总的渲染时长。

但是这并不是我们关注的点,我们更关注页面的 TBT(总的阻塞时间): 由2690ms减少到1045ms,效率提升超过60%; 另外,页面数据在表格渲染完成并且地图第一批数据渲染完成时间大概在4.5s左右,较全量渲染的6.1s减少1.6s左右,性能提升比较明显。

优化效果

以文章开头提到的4000条数据进行比对,经过上面三种方式的优化后的结果如下图:

可以看到性能提升明显:

优化后的页面打开速度几乎达到秒开的效果,用户体验得到很大的提升。

题外话,性能优化是前端老生常谈的一个课题,需要前端开发重点关注的一个方向,尤其是在可视化领域。希望本文分享对大家有所帮助,也希望有经验大佬分享这方面的知识。

总结

有效的沟通,源自于解决问题的思维模式,在多数情况下,重要性,大于当下所掌握的技术知识点

展开阅读全文

页面更新:2024-05-14

标签:方案   数据   机智   笔者   表格   性能   代码   页面   方式   地图   列表

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top