• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

定位解决前端大数据量渲染下的性能问题

武飞扬头像
wonyun
帮助2

背景

最近实现如下图所示的一个简单功能:

学新通 要求:

  • 表格数据全量展示,不分页
  • 地图画出左侧表格所有站点列表对应的marker

技术栈:

  • 表格使用antdTable
  • 地图使用react-leaflet

大概代码:

<Row>
 <Col flex="500px">
    <ProTable
        columns={columns}
        rowKey="id"
        dataSource={stops}
        scroll={{ y: 400 }}
        pagination={false}
    />
 </Col>
 <Col flex="auto">
     <MapContainer {...props}>
       {
           stops.map((stop) => {
             return <CustomMarker key={stop.id} data={stop} />;
            })
        }
     </MapContainer>
  </Col>
 </Row>

一个简单的表格和地图渲染,简单! 于是三下五除二功能开发完了,心中暗喜,有时间摸鱼了(跑~)。

起初,感觉数据量不大,可现实打脸的太快,server吐回的数据从几百到10万 的都有。左边表格一次性的渲染,右边地图的marker渲染是以dom方式渲染,性能开销双面叠加,可想而知,大数据量下页面的性能是多么差劲。数据量达到4千条左右时,页面数据渲染出来需要8s左右;数据量达到5万左右时,页面卡到怀疑人生甚至崩溃,几乎不可用。

于是,页面性能优化就迫在眉睫了。

定位性能瓶颈

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

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

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

lighthouse给出优化建议

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

学新通

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

学新通

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

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

学新通

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

  • 减少主线程的工作

    主线程耗费9.4s,主要开销包括:

    • 14个长任务的执行
    • 纯js部分的执行耗时7.2s
  • 避免dom数过大

    页面有24530个dom元素,包括表格渲染的dom,地图marker渲染的dom等;dom过多会导致页面操作卡顿,可以参考网页dom元素过多为什么会导致页面卡顿

  • 避免大量的网络负载

    因为本地开发环境跑的结果,代码没有压缩可以先不管,真正要关注的是接口的请求响应时长,因为接口返回的数据量约48kb,导致接口耗时1.3s左右,后端接口需要优化

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

  • 主线程耗时长,包括14个长任务
  • 页面dom数量过大
  • 接口响应耗时长,达1.4s

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

Performance面板定位耗时真因

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

  • 查看浏览器渲染的整个过程,包括数据加载、html解析、样式解析计算、js加载执行、composite layers、绘制等各个阶段
  • 查看js脚本执行过程的调用栈信息及对应的耗时,很容易定位性能耗时长的地方

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

学新通

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

  • _next部分主要是页面组件的渲染耗时,包括表格的每行渲染,以及地图自定义marker组件的执行耗时

    学新通

  • fulfilled部分是将_next生成的marker和popup组件添加到地图中,因为是使用dom渲染,在添加地图后,涉及到marker和popup的html解析及渲染过程。

    学新通

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

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

  • 表格渲染因数据量大耗时过长,因为一次性全量渲染
  • 地图marker渲染以dom形式渲染,涉及到dom的解析和渲染,在大数据量时性能差
  • 表格和地图marker同时渲染,导致_next部分耗时长,阻塞后续其他重要流程的执行

性能问题拆解

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

  • 表格不要一次性渲染所有数据
  • 以dom形式渲染的marker改为canvas渲染
  • 表格和地图分开渲染,并且地图marker分片渲染

大数据列表渲染

分片渲染

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

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

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

code.juejin.cn/pen/7142775…

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

  • 总渲染时间增加,因为通过定时器分片渲染,存在间隔
  • 数据最终是全量渲染的结果,导致dom数量过大
  • 快速上拉加载闪屏,可以使用requestAnimationFrame来解决,参考高性能渲染10万条数据(时间分片)

虚拟列表

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

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

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

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

学新通

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

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

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

学新通

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

地图大数据量渲染

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

地图marker渲染优化前:

<MapContainer {...props}>
   {
       stops.map((stop) => {
         return <CustomMarker key={stop.id} data={stop} />;
        })
    }
 </MapContainer>
 
 // CustomMarker实现:
 function CustomMarker(props) {
     ...
     // react-leaflet提供的Marker是以Dom形式渲染的
     <Marker
      ...
      position={position}
    >
      <Popup
        {...props}
      >
        ... // popup内容
      </Popup>
    </Marker>
 }

优化后:

 // 主页面的render部分有关地图部分
 <MapContainer {...props}>
   <CanvasMarkers data={stops} />
 </MapContainer>
 
 // 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%,主要是表格和地图同时渲染导致,严重影响后面任务的执行。长任务分割的一个好处减少任务的执行时间,可以为后面的任务腾出执行时间。

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

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

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

  • 1、表格数据与地图数据先后渲染,先表格后地图元素展示

    // 先table渲染后地图数据渲染,关键代码
    setTableData(tableData); // setTableData为react的useState提供方法
    setTimeout(() => { // 延迟100ms初始化地图数据
      setMarkers(stops); // setMarkers为react的useState提供方法
    }, 100);
    
  • 2、地图元素分片渲染

    CanvasMarkers组件改造如下:

     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条数据进行比对,经过上面三种方式的优化后的结果如下图:

学新通

可以看到性能提升明显:

  • js执行时间降为由7453ms减少至1466ms
  • 页面总耗时由9979ms减少至2999ms,这其中包括分片总耗时,理论上可以拿地图第一批数据渲染完后的时间进行比较

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

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhckeigk
系列文章
更多 icon
同类精品
更多 icon
继续加载