唯杰地图 logo唯杰地图
VJMAP
EN

large-markers

vjmap/references/perf/large-markers.md 5.1 KB187 行
原文

本文回答:大量 Marker(> 500)怎么优化 —— DOM Marker → Symbol 图层 / 聚合 / FX Symbol。

一、瓶颈根源

vjmap.Marker 每个是一个 DOM 节点(<div> + 布局 + transform):

  • 500 个:可能卡(浏览器布局线程繁忙)
  • 1000 个:明显卡顿
  • 5000 个:不可接受

而 Symbol 图层(WebGL)一帧可渲染 10 万+。

二、替代方案对比

方案 数量级 交互 动画 备注
DOM Marker < 500 富(HTML + CSS) 通过 CSS 动画 灵活但慢
Symbol 图层(map.addSymbolLayer) 10 万+ 通过 layer click 事件 样式表达式 推荐
Cluster(聚合) 百万+ 点到 cluster 再展开 - 很大数据必备
fx.symbolImage 10 万+ 继承 symbol GPU 动画(发光 / 流光) 最强

三、DOM Marker → Symbol 图层迁移

原(DOM)

points.forEach(p => {
  new vjmap.Marker({ element: buildIcon(p) })
    .setLngLat([p.lng, p.lat])
    .addTo(map);
});

新(Symbol)

const fc = {
  type: 'FeatureCollection',
  features: points.map(p => ({
    type: 'Feature',
    id: p.id,
    geometry: { type: 'Point', coordinates: [p.lng, p.lat] },
    properties: { name: p.name, level: p.level }
  }))
};

map.addGeoJSONSource('pois', fc);

// 1. 加载图标(只要加载 1 次)
map.loadImage('/icon.png', (err, img) => {
  map.addImage('poi-icon', img);

  map.addSymbolLayer('pois', 'pois', {
    iconImage: 'poi-icon',
    iconSize: ['case', ['==', ['get', 'level'], 'high'], 1.5, 1],
    textField: ['get', 'name'],
    textOffset: [0, 1.5]
  });
});

// 2. 事件
map.on('click', 'pois', e => {
  const f = e.features[0];
  console.log(f.properties);
});
map.on('mouseenter', 'pois', () => map.getCanvas().style.cursor = 'pointer');

四、聚合(Cluster)

数据 > 5000 建议开聚合:

map.addGeoJSONSource('pois', fc, {
  cluster: true,
  clusterRadius: 50,        // 像素
  clusterMaxZoom: 14        // 超过这个 zoom 不再聚合
});

// 聚合圆
map.addCircleLayer('clusters', 'pois', {
  filter: ['has', 'point_count'],
  circleRadius: ['step', ['get', 'point_count'], 15, 10, 20, 50, 25],
  circleColor: '#51bbd6',
  circleStrokeColor: '#fff',
  circleStrokeWidth: 2
});
// 聚合数字
map.addSymbolLayer('cluster-count', 'pois', {
  filter: ['has', 'point_count'],
  textField: ['get', 'point_count_abbreviated'],
  textSize: 12,
  textColor: '#fff'
});
// 非聚合点
map.addCircleLayer('unclustered', 'pois', {
  filter: ['!', ['has', 'point_count']],
  circleRadius: 6,
  circleColor: '#f28cb1',
  circleStrokeColor: '#fff',
  circleStrokeWidth: 1
});

// 点聚合圆放大
map.on('click', 'clusters', e => {
  const f = e.features[0];
  map.getSource('pois').getClusterExpansionZoom(f.properties.cluster_id, (err, zoom) => {
    map.easeTo({ center: f.geometry.coordinates, zoom });
  });
});

五、fx.symbolImage(超大量 + 发光效果)

// 把 FX halo 打包成 sprite 给 symbol 图层用
fx.symbolImage.addPreset('warn-halo', 'halo', {
  width: 48,
  height: 48,
  color: [1, 0, 0, 1]
});

map.addSymbolLayer('alerts', 'alert-src', {
  iconImage: 'warn-halo',
  iconSize: 1
});

100 万点也能跑,且每个点有 halo 光晕。

六、数量阈值速查

数量 推荐方案
≤ 50 DOM Marker 随便
50 ~ 500 DOM Marker 可接受;或 Symbol
500 ~ 5000 必须 Symbol 图层
5000 ~ 50000 Symbol + Cluster
> 50000 Cluster 必开;或 fx.symbolImage

七、DOM Marker 还要保留?

极少量但需要复杂交互(表单 / 按钮 / Vue 组件)的 Marker:

  • 只保留 10~50 个"重要" Marker 用 DOM
  • 其它走 Symbol
  • 视口内才 addMarker,视口外 remove(懒加载)

八、visible 字段控制 LOD(Level of Detail)

map.addCircleLayer('pois-low', 'pois', {
  minzoom: 12,              // 只有 zoom ≥ 12 时显示
  circleRadius: 5
});
map.addCircleLayer('pois-high', 'pois', {
  minzoom: 14,
  filter: ['==', ['get', 'level'], 'high']
});

不同 zoom 显示不同密度。

九、性能评测流程

  1. 打开 Chrome DevTools → Performance → Record 30s
  2. 做典型操作(缩放、平移)
  3. 看 FPS 曲线 / Main thread 活动
  4. 定位:是 DOM Marker 布局?是 WebGL draw?是 JS 忙?

详见 profiling。

十、常见坑

  1. 500 个 Marker 以为没事:低端机就是会卡;必须换 Symbol。
  2. 聚合图标不显示:忘 filter: ['has', 'point_count']。
  3. Marker 懒加载错:区分 "map.on('moveend')" 和 "map.on('zoomend')";建议 moveend 够用。
  4. Symbol 没加图标就 addLayer:iconImage 找不到 → 占位符;先 loadImage + addImage 再 addLayer。
  5. cluster 聚合失效:source 没设 cluster: true,或点数不够多聚不起来。

相关