本文深入探讨了Leaflet在渲染海量点数据时面临的性能挑战,提出了一种创新的解决方案——利用leaflet-marker-canvas插件。传统的循环绘制Marker方式在数据量巨大时会导致明显的性能下降,而通过将点数据加入Canvas进行批量渲染,显著提高了绘图效率。文章还细致分析了leaflet-marker-canvas的源码,揭示了其内部优化机制,为开发者提供了有效利用该插件的深入见解。

传统方式海量数据加载

在实际的业务开发当中,我们需要加载海量数据,那么我们通常会使用类似下面的方式来加载数据:同时添加一个测试加载耗时的输出

const addManyPoint = () => {
  console.time('代码执行时长');
  const myIcon = L.icon({
    iconUrl: CITY_IMG,
    iconSize: [20, 20]
  });
  const data = [];
  for (let i = 0; i < 1000; i++) {
    data.push({name: i, lng: Math.random() * 360 - 180, lat: Math.random() * 180 - 90});
  }
  data.forEach((item) => {
    const marker = L.marker([item.lat, item.lng], {icon: myIcon}).addTo(map);
    marker.bindPopup(`<b>${item.name}</b>`);
  });
  console.timeEnd('代码执行时长');
};

在上面模拟添加了1000个点,现在看起来还不是很卡,这和浏览器本身性能有关,但是随着数据量的增加,浏览器会卡顿,甚至崩溃。

原因:这是因为leaflet通过这种方式添加的marker对象都是一个dom元素,而浏览器渲染dom元素非常耗费性能,所以导致页面卡顿。

<img src="/web-vite-vue3/src/assets/image/city.png"
     class="leaflet-marker-icon leaflet-zoom-animated leaflet-interactive" alt="Marker" tabindex="0" role="button"
     style="margin-left: -10px; margin-top: -10px; width: 20px; height: 20px; transform: translate3d(925px, 270px, 0px); z-index: 270;">

在这里插入图片描述

leaflet-markers-canvas

在浏览leaflet插件的时候,发现一个叫leaflet-markers-canvas的插件,这个插件可以解决上述的问题,它通过canvas来渲染marker,从而解决上述的问题。

npm install leaflet-markers-canvas

使用插件

  • 创建canvas对象,拿到markersCanvas将其添加到地图上
  • 模拟数据,得到十万个点
  • 通过markersCanvas.addMarkers()方法将marker添加到图层上,添加完之后,就可以看到marker了。
import 'leaflet-markers-canvas';

const addManyMarker = () => {
  // https://www.npmjs.com/package/leaflet-markers-canvas
  console.time('代码执行时长');
  const markersCanvas = new L.MarkersCanvas();
  markersCanvas.addTo(map);

  var icon = L.icon({
    iconUrl: Money,
    iconSize: [20, 20],
    iconAnchor: [0, 0]
  });

  const markers = [];

  for (let i = 0; i < 100000; i++) {
    const marker = L.marker(
        [Math.random() * 180 - 90, Math.random() * 180 - 90],
        {icon}
      )
      .bindPopup(`${i}`)
      .on({
        mouseover(e) {
          this.openPopup();
        },
        mouseout(e) {
          this.closePopup();
        }
      });

    markers.push(marker);
  }

  markersCanvas.addMarkers(markers);
  console.timeEnd('代码执行时长');
};

在这里插入图片描述

方法说明

方法名说明
addTo(map)将该layer添加到map上
getBounds()获取当前的边界范围
redraw()重新绘制整个canvas图层
clear()清空canvas图层
addMarker(marker)添加单个marker
addMarkers(markers)添加多个marker
removeMarker(marker)移除单个marker
removeMarkers(markers)移除多个marker

插件源码分析

看到这个插件的名字大概上也就能猜到他是如何实现的,无非就是将所有的marker通过canvas绘制出来,然后把整个canvas加到地图上就完事了。

源码位置:node_modules/leaflet-markers-canvas/src/leaflet-markers-canvas.js

  • 这一块关于执行逻辑可以看一下这个系列前面对其他插件的源码分析,插件都是继承Layer基类,那执行顺序也是按照基类来的。
  • 初始化initialize,将options赋值给this,
  • 执行onAdd,初始化canvas并且将canvas添加到地图上。添加对应的地图事件
  • addMarkers方法
    • 遍历markers对象,将他们存到_markersTree、_positionsTree当中
    • 上面存储的对象是new RBush(),RBush是一个二叉树,用来存储坐标点,通过二叉树来快速查找坐标点。
    • 在添加数据的时候,单个数据可以通过insert方法,批量数据可以通过load方法。
    • 同理在删除、清空图层的时候也是通过这个二叉树来操作。单个通过remove(marker),多个通过clear(),或者重新给他初始化
const markersCanvas = {
  // https://www.npmjs.com/package/rbush

  _markersTree: new RBush(),
  _positionsTree: new RBush(),
  
  // 添加多个个marker
  addMarkers(markers) {
    const markerBoxes = [];
    const positionBoxes = [];

    markers.forEach((marker) => {
      // 会通过_addMarker去组装这几个对象
      const {markerBox, positionBox, isVisible} = this._addMarker(marker);

      if (markerBox && isVisible) {
        markerBoxes.push(markerBox);
      }

      if (positionBox) {
        positionBoxes.push(positionBox);
      }
    });

    // 通过load全部添加到二叉树中
    this._markersTree.load(markerBoxes);
    this._positionsTree.load(positionBoxes);
  }
};

聚合cluster

除了将marker对象放在一个canvas上进行渲染,还可以通过聚合分组的方式进行渲染,也就是在最上面的层级不展示点信息,只有在放大到一定层级之后才进行渲染marker。可以使用leaflet当中的插件:leaflet.markercluster

npm install leaflet.markercluster

使用

通过聚合分组的方案也可以满足加载大量数据不卡顿,下面是使用案例,创建了十万个点渲染到页面上。

import 'leaflet.markercluster/dist/leaflet.markercluster';
import 'leaflet.markercluster/dist/MarkerCluster.css';
import 'leaflet.markercluster/dist/MarkerCluster.Default.css';

const addCluster = () => {
  const clusterLayer = L.markerClusterGroup({
    // showCoverageOnHover: false,
    // zoomToBoundsOnClick: false,
    // spiderfyOnMaxZoom: false,
    // removeOutsideVisibleBounds: false,
    // spiderLegPolylineOptions: {}
    // 自定义聚合样式
    // iconCreateFunction: function (cluster) {
    // return L.divIcon({html: '<b class="bg-#[f40]">' + cluster.getChildCount() + '</b>'});
    // }
  });
  for (let i = 0; i < 100000; i++) {
    const marker = L.marker([18.8567 + Math.random(), 102.3508 + Math.random()], {
      title: `${i}`,
      icon: L.icon({
        iconUrl: Money,
        iconSize: [20, 20],
        iconAnchor: [0, 0]
      })
    });
    marker.on('click', function (event) {
      console.log('marker ====', event.latlng);
    });

    marker.on('clusterclick', function (a) {
      // a.layer is actually a cluster
      console.log('cluster ' + a.layer.getAllChildMarkers().length);
    });
    clusterLayer.addLayer(marker);
  }
  map.addLayer(clusterLayer);
};

在这里插入图片描述

markerClusterGroup配置对象

属性/方法说明
showCoverageOnHover将鼠标悬停在集群上时,它会显示其标记的边界
zoomToBoundsOnClick单击集群时,会缩放到其边界
spiderfyOnMaxZoom当您单击底部缩放级别的集群时,我们会对其进行蜘蛛化,以便您可以查看其所有标记
removeOutsideVisibleBounds为了提高性能,将从地图中删除离视口太远的聚类和标记
spiderLegPolylineOptions允许您指定 PolylineOptions 来设置蜘蛛腿的样式。默认情况下,它们是 { weight: 1.5, color: '#222', opacity: 0.5 }
animate在缩放和蜘蛛化时平滑拆分/合并集群子项
iconCreateFunctionFn,自定义聚合样式
spiderfyShapePositionsFn,覆盖蜘蛛形状位置

拓展

  • openlayer:在openlayer当中那是否也可以通过构建一个canvas来存储这些海量数据加载捏?或者使用他的一个聚合类,一个层级只展示能完全展示的几个
  • mapbox:mapbox加载海量点数据会默认进行一个聚合操作,也就是他不会全部渲染出来,而是在渲染的时候会根据层级去默认隐藏很多,只有层级足够大能够全部展示出来才会显示
  • cesium:cesium当中有一个加载海量点的类
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐