likes
comments
collection
share

H5端高德地图海量点渲染性能优化实践 > 作者:王宇 ## 一、背景 最近在做一个项目的重构,其中需要重构一个 `H5`

作者站长头像
站长
· 阅读数 53

作者:王宇

一、背景

最近在做一个项目的重构,其中需要重构一个 H5 端的地图,该地图使用高德 JS-SDK 实现并进行海量点的渲染,历史的实现存在一些性能问题,在低端机型上会有明显的卡顿现象 ,在重构时,除了基本的功能调整外,性能提升更是一个很重要的关键点,那么如何进行性能提升呢,本文将对过程进行详细分析并最终实现性能优化。

二、历史实现

1、页面解读

首先看一下页面,大致就是下面这个样子【其中网络请求看板为一次地图挪动后不同实体的接口请求情况】

H5端高德地图海量点渲染性能优化实践 > 作者:王宇 ## 一、背景 最近在做一个项目的重构,其中需要重构一个 `H5`

解读:地图中渲染了多种类的实体,之前为了优先保证数据的实时性,在用户滑动或缩放地图时,会根据当前视野范围通过接口动态查询数据,然后本地实时计算,进而决定使用聚合或散列状态展示(这里使用了高德地图的MarkerClusterer进行聚合散列状态的动态展示,其内部逻辑为:将屏幕分割成一定尺寸的单元格,单元格内数量超过设定值则聚合展示,低于则散列展示),聚合状态由于渲染更少的 dom 节点,性能会比离散状态直接渲染全量的点好,但即便如此,依然存在性能问题;

2、性能分析

页面动效图

H5端高德地图海量点渲染性能优化实践 > 作者:王宇 ## 一、背景 最近在做一个项目的重构,其中需要重构一个 `H5`

Performance 面板 (有大量的丢帧,红色和黄色部分)

H5端高德地图海量点渲染性能优化实践 > 作者:王宇 ## 一、背景 最近在做一个项目的重构,其中需要重构一个 `H5`

整体

H5端高德地图海量点渲染性能优化实践 > 作者:王宇 ## 一、背景 最近在做一个项目的重构,其中需要重构一个 `H5`

虽然页面中使用了聚合态的形式通过渲染更少的 dom 节点,可以提升一部分性能,但实际用户机型使用起来还是会有卡顿的现象:

1、由于地图视野变动是像素级别的回调, 做了500ms 的防抖处理,也即下方 Performance 面板中的红色条状部分【这部分时间是固定的】;

2、紧接着会并行请求各实体的数据,该过程一般在100- 300ms 之间;

3、在分别获取到各实体数据后,MarkerClusterer会本地计算进而决定是聚合态还是离散态进行展示;从最后一个接口拿到数据到本地计算后最后一帧渲染完毕,大概用了 60ms 左右【这是在电脑上的效果,该部分受机器性能影响较大,在工作人员的低端工作手机上,差异会大很多】

在性能良好的电脑上,一共硬性耗时 500ms + (100ms 至 300ms) + (60ms以上) = 66ms 至 1s 左右 也即用户拖动或缩放一次地图,大概需要1s 左右才会有界面响应(而实际手机上目测有 2s 左右甚至更多),在这 1s 内用户等待焦虑会触发持续频繁操作,进而连环触发并重叠执行该过程,就会出现较严重的页面卡顿现象;

那么总结下来:其实影响性能的关键有3个点,1、防抖的 500ms 2、数据实时获取 3、实时本地计算;也即下图中的红色部分;最要命的地方在于用户每触发一次地图缩放或平移,都会重复执行该过程;

H5端高德地图海量点渲染性能优化实践 > 作者:王宇 ## 一、背景 最近在做一个项目的重构,其中需要重构一个 `H5`

经过分析后,那么就可以有针对性的进行优化了

H5端高德地图海量点渲染性能优化实践 > 作者:王宇 ## 一、背景 最近在做一个项目的重构,其中需要重构一个 `H5`

3、历史代码

部分 hook 封装在古茗内部 SDK 中,不用太关注实现

import { useInitMap } from '@guming/amap';
import { useState , useRef } from 'react';


export default () => {

  const ClusterRef = useRef();
    // 地图实例化
  const {Map, mapRef, mapInstance, currentZoom,  currentBound} = useInitMap(config);
  
   // 地图视野发生变动  防抖获取各种实体类型在视野范围内数据 性能损耗的关键
  useDebounceEffect(()=>{
    fetchShopDataAndDraw(currentBound);
    //...  fetchSomeOtherTypeData(currentBound);
    
  },[currentBound]);
  
  const fetchShopDataAndDraw = async (bounds)=> {
      const massData = await fetchShopData({bounds});
      drawMarkerOrCluster(massData)
  };
  
  // 渲染数据到地图中  【离散态、聚合态的频繁切换、计算 】
  const drawMarkerOrCluster = (massData = []) => {
    if(!ClusterRef.current){
      ClusterRef.current = new AMap.MarkerClusterer(mapRef.current, massData, {
        gridSize: 150, // 网格像素大小
        minClusterSize: 50, // 聚合的最小量
        averageCenter: true,
        maxZoom: 17,
        // 渲染聚合态
        renderClusterMarker(context) {
          const { marker, count } = context;
          marker.setAnchor('center');
          marker.setContent(
            `<div class="cluster-circle ${type}">${count}</div>`
          );
          marker.setzIndex(10);
        },
        // 渲染散列态
        renderMarker(context) {
          const { marker, data } = context;
          const markerData = data[0];
          const clueIconWidth = 26;
          // 考虑性能 在一定级别只展示icon  到达一定级别后同时展示icon + name
          if ((mapRef.current?.getZoom() || currentZoom) >= showNameZoom) {
            marker.setContent(createdMarker.getContent());
          } else {
            marker.setIcon(...);
          }
          marker.on('click', () => handleClickMarker(markerData, marker));
        },
      });
    
      ClusterRef.current.on('click', doSomeThing);
    }else{
      ClusterRef.current.setData(massData)
    }
  };
  
  return <Map />
}

三、优化实现

1、方案

H5端高德地图海量点渲染性能优化实践 > 作者:王宇 ## 一、背景 最近在做一个项目的重构,其中需要重构一个 `H5`

1.1、数据获取

历史的数据获取是视野范围内的数据,这里则是获取全量数据,数据量上有了增加,所以在数据获取这一块需要做一些调整优化:

  1. 页面初次进入获取所有实体数据;页面被激活时,对于数量庞大且实时性要求不高的竞品参考数据,则不再获取,使用旧数据;
  2. 对于数量庞大且实时性要求不高的竞品参考性数据,则静默多分页并行获取,并逐步展示;(主要是优化 handleQueryCrawler 方法,这里不再详细做代码阐述)

  const handleQueryClue = () => handleQuery('clue');
  const handleQueryCrawler = () => handleQuery('crawler');

  const queryAllTypeExcludeCarwlersData = async () => {
    // 数据量不大但实时性要求相对较高数据
    handleQueryClue();
    // ....其他实体数据
  };

  useDidShow(() => {
    queryAllTypeExcludeCarwlersData();
     // 数量庞大且实时性要求不高的数据  这里可以调整成多分页并行请求 加快展示呈现
    if (!typesDataAndApiConfig.current.crawler.hasLoaded) {
      handleQueryCrawler();
    }
  });
}

1.2、渲染

  • 使用可以不需要动态计算的 DistrictCluster 进行聚合态渲染【聚合态下渲染的 dom 节点极少,此时减少计算,就可以达到良好的性能】

// 实例化聚合态 DistrictCluster 
clusterRef.current = new DistrictCluster({
    map: mapInstance, //所属的地图实例
    zIndex: 11,
    topAdcodes: [330100],
    getPosition: function (item) {
      return !item ? null : [item.lng, item.lat];
    },
    boundsQuerySupport: true,
    renderOptions: {
      getClusterMarkerPosition:
        DistrictCluster.ClusterMarkerPositionStrategy.AVERAGE_POINTS_POSITION,
      getClusterMarker: function (feature, dataItems, recycledMarker) {
    
        if (!dataItems.length) return null;

        const counts = dataItems.reduce((ret, { dataItem }) => {
          if (!ret[dataItem.type]) {
            ret[dataItem.type] = 1;
          } else {
            ret[dataItem.type] += 1;
          }
          return ret;
        }, {});

        const content = `${feature.properties.name}${Object.keys(counts)
          .map((type) => `<div>${type}:<span style="color:red">${counts[type]}个</span></div>`)
          .join('')}`;

        const label = {
          offset: new AMap.Pixel(0, 0), //修改label相对于marker的位置
          content,
        };

        //存在可回收利用的marker
        if (recycledMarker) {
          //直接更新内容返回
          recycledMarker.setLabel(label);
          return recycledMarker;
        }

        //返回一个新的Marker
        return new AMap.Marker({
          // ... 标记点配置
        });
      },
    },
  });

 //  配置 散列状态的渲染
  const { clearAll: clearPoints, renderAll: renderPoints } = useRenderLabelMarker({
    mapInstance,
    dataKey: 'id',
    labelMarkerConfig({ dataItem }) {
      return {
        // ... 标记点配置
      };
    },
  });

// 配置散列状态文字渲染

  const { clearAll: clearText, renderAll: renderText } = useRenderText({
    mapInstance,
    dataKey: 'id',
    labelMarkerConfig({ dataItem }) {
      return {
        // ... 标记点配置
      };
    },
  });
  • 随着视野范围内点位数的不断减少,逐步切换至离散态、逐步渲染名称

// 渲染控制
useDebounceEffect(
  () => {
    if (!currentBound) return;
    const inViewDatas = clusterRef.current?.getDataItemsInView() || [];
    clearPoints();
    clearText();
    // 视野内数量大于500 则聚合展示
    if (inViewDatas?.length >= 500) {
      clusterRef.current?.show();
    } else {
      // 否则使用  labelMarker 散列展示
      clusterRef.current?.hide();
      renderPoints(inViewDatas.map(({ dataItem }) => dataItem));
      
      // 视野内数量少于150 则展示图标名字
      if(inViewDatas?.length <= 150){
          renderText();
      }
    }
  },
  [currentBound,currentZoom],
  { wait: 280 }
);

2、结果

2.1、优化效果

下图为 6267 个数据点,从大视野聚合状态 -> 小视野散列状态 -> 散列状态 -> 散列状态下平移缩放 的页面动效图及 Performance 性能面板

页面动效图

H5端高德地图海量点渲染性能优化实践 > 作者:王宇 ## 一、背景 最近在做一个项目的重构,其中需要重构一个 `H5`

Performance 性能面板(从大聚合态到小聚合态)

红色部分是切换过程中的丢帧 基本在16ms 以内

H5端高德地图海量点渲染性能优化实践 > 作者:王宇 ## 一、背景 最近在做一个项目的重构,其中需要重构一个 `H5`

Performance 性能面板 ( 从小聚合态到散列态)

H5端高德地图海量点渲染性能优化实践 > 作者:王宇 ## 一、背景 最近在做一个项目的重构,其中需要重构一个 `H5`

Performance 性能面板(滑动地图动态绘制不同视野范围内数据)

H5端高德地图海量点渲染性能优化实践 > 作者:王宇 ## 一、背景 最近在做一个项目的重构,其中需要重构一个 `H5`

整体

H5端高德地图海量点渲染性能优化实践 > 作者:王宇 ## 一、背景 最近在做一个项目的重构,其中需要重构一个 `H5`

2.2、前后对比

下面为一次用户完整的操作流程的前后对比

H5端高德地图海量点渲染性能优化实践 > 作者:王宇 ## 一、背景 最近在做一个项目的重构,其中需要重构一个 `H5`

总结就是:在同样的交互流程下,浏览器花了更少的时间,流畅的完整了渲染任务

四、总结

本文的主要优化方向

  • 减少数据请求频次【避免每次视图变更都去获取数据】
  • 减少海量计算频次【使用可以不需要频繁计算的 DistrictCluster 进行渲染、取消特定场景下的海量计算】
  • 减少不必要的信息渲染、已渲染点的批量更新【视野内一定数量下离散展示图标、更少数量下开始展示文字、图标和文字分开渲染】

地图性能优化,主要整体思路是:通过方案调整,在各个层面进行协调,避免将有限的算力消耗在价值不大或非必须的场景中; 达到良好的性能的目的,技术层面并没有高深的地方,但这个分析的过程和解决的思路在对于产品理解、技术实现上有更好的理解和把控,同时以后遇到类似的性能问题时,也会有一些好的参考思路 ~~

转载自:https://juejin.cn/post/7378452954221150218
评论
请登录