> **本文回答**：大量 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）

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

### 新（Symbol）

```js
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 建议开聚合：

```js
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`（超大量 + 发光效果）

```js
// 把 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）

```js
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](profiling.md)。

## 十、常见坑

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`，或点数不够多聚不起来。

## 相关

- Marker → [marker/overview](../marker/overview.md)
- Symbol → [symbol/overview](../symbol/overview.md)
- Cluster → [marker/cluster](../marker/cluster.md)
- fx.symbolImage → [fx-overview](../../../vjmapext/references/perf/fx-overview.md)
