需求背景
在高德地图上展示路线的成功或失败情况,数据量较大,且需要全部展示,核心在于保证渲染路线的性能
技术难点
- 渲染可见点
- 分批异步渲染
- 利用 Simplify.js 简化轨迹
渲染可见点
- 根据传入的经纬度以及地图边界来确定是否渲染
- 监听各种事件来重新渲染
const isPointInView = (map, lonLat) => {
   const bounds = map.getBounds();
   const NorthEast = bounds.getNorthEast();
   const SouthWest = bounds.getSouthWest();
   const SouthEast = [NorthEast.lng, SouthWest.lat];
   const NorthWest = [SouthWest.lng, NorthEast.lat];
   const path = [[NorthEast.lng, NorthEast.lat], SouthEast, [SouthWest.lng, SouthWest.lat], NorthWest];
   const isInView = window.AMap.GeometryUtil.isPointInRing(lonLat, path);
   return isInView;
};
const END_EVENTS = ['zoomend', 'moveend', 'resize'];
useAsyncEffect(async () => {
   if (!map) {
      return;
   }
   const handleEndEvents = debounce((...param) => {
      console.log('event', param);
      renderLines(map, dataRef.current);
   }, 1000);
   END_EVENTS.forEach((event) => {
      console.log('add event', event);
      map.on(event, handleEndEvents);
   });
   return () => {
      END_EVENTS.forEach((event) => {
         console.log('remove event', event);
         map.clearEvents(event);
      });
   };
}, [map]);分批异步渲染
核心渲染策略:模仿 react17 的 Scheduler 调度,切成小批量渲染任务,尽可能的防止页面卡顿,这里针对不同组件分为几种情况
PathSimplifier
- 采用分批异步渲染虽然加快了首次渲染速度,但卡顿还是存在,因为这里的 setData 是全量更新,没有增量更新的方法(尝试继承实现但难度较大)
- 分析 setData 函数源码 发现调用了 renderLater(10),也就是 10 毫秒后才会渲染,为此实现自定义方法然后调用render()
import { requestHostCallback, cancelHostCallback, shouldYieldToHost } from '@/utils/SchedulerHostConfig';
const batchRender = (pathSimplifierIns, lines) => {
   const size = 100;
   const totalPage = Math.ceil(lines.length / size);
   // 取消之前主线程的任务
   cancelHostCallback();
   const render = () => {
      let page = 0;
      return () => {
         // 是否需要让出主线程
         if (shouldYieldToHost() || page >= totalPage) {
            return;
         }
         const splitLines = lines.slice(0, page * size + size);
         console.log('draw splitLines length', splitLines.length);
         // pathSimplifierIns.setDataImmediate(splitLines)
         pathSimplifierIns.setData(splitLines);         page++;
         return page < totalPage;
      };
   };
   requestHostCallback(render());
};
const loadMyPathSimplifier = () => {
   return new Promise((resolve) => {
      window.AMapUI.load(['lib/utils'], function(utils) {
         // 新的类
         function MyPathSimplifier(opts) {
            // 调用父级的构造方法
            MyPathSimplifier.__super__.constructor.call(this, opts);
            // ..额外的初始化逻辑..
         }
         // 继承功能
         utils.inherit(MyPathSimplifier, window.AMapUI.PathSimplifier);
         // 增加或者覆盖接口
         utils.extend(MyPathSimplifier.prototype, {
            // ..新的接口
            setDataImmediate(data) {
               this._buildData(data);
               this.render();
            }
         });
         resolve({
            MyPathSimplifier
         });
      });
   });
};性能测试
以下测试都是在浏览器隐私模式下监控性能数据,所需操作如下
- 地图上从小数据一直加载到大数据
- 加载到大量数据时对地图进行缩放、拖拽
左侧为 setDataImmediate(), 右侧为 setData(),性能差异不大
 
    
  
  
  
    
实现效果
Polyline
- Polyline 有增量更新的方法,但同时需要考虑增量删除的方法,否则全量删除同样会造成页面卡顿
- v1.4 和 v2.0 在实现相同功能的情况下有性能差异,截止 2023-3-4 22:40:07 为止 v1.4 性能表现更好
const SIZE = 100;
const batchRender = (map, lines) => {
   const size = SIZE;
   const totalPage = Math.ceil(lines.length / size);
   const render = () => {
      let page = 0;
      return () => {
         if (shouldYieldToHost() || page >= totalPage) {
            console.log('shouldYieldToHost', page);
            return;
         }
         const splitLines = lines.slice(page * size, page * size + size);
         if (page + 1 >= totalPage) {
            console.log('draw polylines length', lines.length);
         }
         map.add(splitLines);         page++;
         return page < totalPage;
      };
   };
   requestHostCallback(render());
};
// 注意这里需要用闭包实现
const handleClearLines = (map, clearLines) => {
   (function(clearLines) {
      if (clearLines && clearLines.length > 0) {
         const size = SIZE;
         const totalPage = Math.ceil(clearLines.length / size);
         const clearFunc = (page) => {
            if (page >= totalPage) {
               return;
            }
            const splitLines = clearLines.slice(page * size, page * size + size);
            if (page + 1 >= totalPage) {
               console.log('remove polylines length', clearLines.length);
            }
            map.remove(splitLines);            requestHostTimeout(() => clearFunc(page + 1), 0);
         };
         clearFunc(0);
      }
   })(clearLines);
};性能测试
左侧为 v2.0,右侧为 v1.4,可以看到 v1.4 有以下优点
- 渲染速度快
- js 堆内存占用少
- 最大长任务 1.72s 少于 v2.0 的 2.16s
 
    
  
  
  
    
实现效果
v2.0
v1.4
未来优化点
参考资料:16 毫秒的挑战:图表库渲染优化
- 增量复用已有节点
- 大数据内存占用优化
- 根据分片运行时间自动调整分片大小
- 换 3d 渲染(3d 的缩放拖拽是通过摄像头视角改变来实现的,理论上比 2d 的渲染要快)