> **本文回答**：用 `source.setData(FeatureCollection)` 做大数据批量绘制 —— 增量更新、diff、性能。

## 一、一次性全量 setData

```js
map.addGeoJSONSource('pts', { type: 'FeatureCollection', features: [] });
map.addCircleLayer('pts-layer', 'pts', { circleColor: '#f00', circleRadius: 3 });

const features = [];
for (let i = 0; i < 100000; i++) {
  features.push({
    type: 'Feature',
    id: i,
    geometry: { type: 'Point', coordinates: map.toLngLat([Math.random()*1e6, Math.random()*1e6]) },
    properties: { level: Math.floor(Math.random()*5) }
  });
}

map.getSource('pts').setData({ type: 'FeatureCollection', features });
```

10 万点，`setData` 约 50–300 ms（取决于浏览器）。

## 二、增量更新（diff）

避免每次重建：

```js
class IncrementalSource {
  constructor(map, sourceId) {
    this.map = map;
    this.sourceId = sourceId;
    this.features = new Map();
  }
  add(feat) { this.features.set(feat.id, feat); this._flush(); }
  update(id, patch) {
    const f = this.features.get(id);
    if (!f) return;
    Object.assign(f.properties, patch);
    this._flush();
  }
  remove(id) { this.features.delete(id); this._flush(); }
  _flush() {
    if (this._pending) return;
    this._pending = requestAnimationFrame(() => {
      this.map.getSource(this.sourceId).setData({
        type: 'FeatureCollection',
        features: [...this.features.values()]
      });
      this._pending = null;
    });
  }
}

const src = new IncrementalSource(map, 'pts');
src.add(f1); src.add(f2); src.update(1, { level: 9 });
```

`requestAnimationFrame` 合并同一帧多次变更。

## 三、只更新 feature-state（最快）

仅改样式（颜色、透明度）时**不要重建 source**，用 feature-state：

```js
map.setFeatureState(
  { source: 'pts', id: 123 },
  { selected: true }
);

// 图层样式用表达式读取
map.addCircleLayer('pts-layer', 'pts', {
  circleColor: [
    'case',
    ['boolean', ['feature-state', 'selected'], false], '#f00',
    '#888'
  ]
});
```

详见 [primitive/geojson-source](../primitive/geojson-source.md)。

## 四、聚合（cluster）

```js
// 签名：addGeoJSONSource(id, geojsonData, props?: GeoJSONSourceSpecification)
// cluster 系列选项在第三个参数（source 属性），不是塞进 FeatureCollection
map.addGeoJSONSource('pts',
  { type: 'FeatureCollection', features },
  {
    cluster: true,
    clusterRadius: 40,
    clusterMaxZoom: 14,
    clusterProperties: {
      sumLevel: ['+', ['get', 'level']]
    }
  });
```

展示集群圆 + 点击展开：

```js
map.addCircleLayer('clusters', 'pts', {
  circleColor: ['step', ['get', 'point_count'], '#0f0', 50, '#ff0', 200, '#f00'],
  circleRadius: ['step', ['get', 'point_count'], 15, 50, 22, 200, 30],
  filter: ['has', 'point_count']
});
map.addCircleLayer('unclustered', 'pts', {
  circleColor: '#0af',
  filter: ['!', ['has', 'point_count']]
});

map.on('click', 'clusters', e => {
  const f = e.features[0];
  // cluster 要素的聚合 id 在 properties.cluster_id（不是 f.id）；
  // getClusterExpansionZoom 是回调式 API，不返回 Promise
  map.getSource('pts').getClusterExpansionZoom(f.properties.cluster_id, (err, zoom) => {
    if (err) return;
    map.easeTo({ center: f.geometry.coordinates, zoom });
  });
});
```

## 五、大数据分层

```js
function tieredSource(all) {
  return [
    { zoomMin: 0,  zoomMax: 8,  sample: all.filter((_,i) => i % 100 === 0) },
    { zoomMin: 8,  zoomMax: 12, sample: all.filter((_,i) => i % 10  === 0) },
    { zoomMin: 12, zoomMax: 24, sample: all }
  ];
}
```

按 zoom 切换源的 setData，低 zoom 抽稀。

## 六、分批插入

100 万点一次 setData 会阻塞主线程：

```js
function bulkInsert(all, batchSize = 20000) {
  let i = 0;
  function tick() {
    const slice = all.slice(i, i + batchSize);
    i += batchSize;
    src.features.push(...slice);
    src.setData({ type: 'FeatureCollection', features: src.features });
    if (i < all.length) requestAnimationFrame(tick);
  }
  tick();
}
```

## 七、使用 `promoteId` 把 DB id 映射为 feature id

```js
map.addGeoJSONSource('devices',
  { type: 'FeatureCollection', features },
  { promoteId: 'device_id' }    // feature.properties.device_id → feature.id
);
```

这样 `setFeatureState` 可以用真实业务 id，不用人工维护。

## 八、性能数据参考

| 数据量 | setData 时间 | 内存 |
|---|---|---|
| 1 万点 | < 10 ms | ~5 MB |
| 10 万点 | 50–150 ms | ~50 MB |
| 100 万点 | 500 ms ~ 2 s | ~500 MB |
| 100 万点 + cluster | 首次 setData 2 ~ 5 s | 高 |

## 九、常见坑

1. **setData 每帧重建**：浪费 GeoJSON worker 时间；diff + RAF 合并。
2. **不设 `promoteId`，feature.id 缺失**：`setFeatureState` 命中不到；要么 `generateId: true` 要么 `promoteId`。
3. **cluster 对 heatmap 不适用**：cluster + heatmap 组合不生效；如需热力图直接用 heatmap 图层。
4. **bbox 过大的 feature**：一个 Polygon 几千 ring 会严重拖慢 serialization；分割成多个 feature 更快。
