large-markers
本文回答:大量 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 显示不同密度。
九、性能评测流程
- 打开 Chrome DevTools → Performance → Record 30s
- 做典型操作(缩放、平移)
- 看 FPS 曲线 / Main thread 活动
- 定位:是 DOM Marker 布局?是 WebGL draw?是 JS 忙?
详见 profiling。
十、常见坑
- 500 个 Marker 以为没事:低端机就是会卡;必须换 Symbol。
- 聚合图标不显示:忘
filter: ['has', 'point_count']。 - Marker 懒加载错:区分 "map.on('moveend')" 和 "map.on('zoomend')";建议 moveend 够用。
- Symbol 没加图标就 addLayer:iconImage 找不到 → 占位符;先
loadImage+addImage再 addLayer。 - cluster 聚合失效:source 没设
cluster: true,或点数不够多聚不起来。
相关
- Marker → marker/overview
- Symbol → symbol/overview
- Cluster → marker/cluster
- fx.symbolImage → fx-overview