标记点一多就卡
三百个点还算流畅,八百个点开始掉帧,两千个点拖动像放幻灯片。这是矢量图层最常见的性能瓶颈。
现象
点位数量增长后,拖拽和缩放明显变卡,CPU 占用飙升。 打开性能面板可以看到大量 DOM 节点,或者每帧都在重绘矢量图层。
为什么会卡
Leaflet 的默认标记是真实的 DOM 元素。两千个标记就是两千个 DOM 节点, 每次地图移动都要重新计算它们的位置并写样式,浏览器要重排重绘两千次。 OpenLayers 的矢量层默认走 SVG 或 canvas,问题类似,只是瓶颈点在绘制调用上。
按数据量选方案
| 数据量 | 方案 | 预期效果 |
|---|---|---|
| 少于 300 | 默认标记即可,注意用图层组管理 | 无需优化 |
| 300–2000 | 改用 canvas 渲染,或使用聚合 | 可稳定流畅 |
| 2000–20000 | 必须聚合,或只渲染当前视野 | 可接受 |
| 20000 以上 | 换 deck.gl 之类走 WebGL 的图层引擎 | 十万级仍可满帧 |
方案一:改用 canvas 渲染
Leaflet 提供 preferCanvas,让矢量图层走 canvas 而不是 SVG,节点数不再随数据增长。
var map = L.map('map', {
center: [39.9, 116.4],
zoom: 11,
preferCanvas: true
});
OpenLayers 侧在图层上关闭 SVG 渲染,同时关掉每个要素的独立样式,尽量共用同一个样式对象。
var layer = new ol.layer.Vector({
source: source,
style: sharedStyleObject, // 共用一个样式,不要每条要素 new 一个
updateWhileAnimating: false,
updateWhileInteracting: false
});
方案二:只渲染当前视野
数据在视野外就完全不画。地图移动结束时重新筛选一次, 比全量渲染省一个数量级。
function refresh() {
var bounds = map.getBounds();
group.clearLayers();
points.forEach(function (p) {
if (bounds.contains([p.lat, p.lng])) {
group.addLayer(makeMarker(p));
}
});
}
map.on('moveend', refresh);
refresh();
这个做法的代价是移动过程中会短暂空窗,需要在 moveend 后补画,
所以适合点位分布稀疏的场景。
方案三:聚合
Leaflet 用 markercluster 插件,OpenLayers 用官方的 Cluster 数据源。
聚合的本质是把临近的点合并成一个带数字的圆,
不仅提升性能,也让密集区域更可读。
var cluster = new ol.source.Cluster({
distance: 40, // 聚合半径,单位像素
source: new ol.source.Vector({ features: features })
});
var layer = new ol.layer.Vector({
source: cluster,
style: function (feature) {
var size = feature.get('features').length;
return clusterStyle(size); // 数量不同给不同大小的圆
}
});
方案四:换渲染引擎
到几万点这个量级,任何基于 DOM 或 canvas 的方案都到顶了。 这时应该把可视化层换成 WebGL 实现,底图仍然可以是 Leaflet 或 MapLibre, 数据层交给 deck.gl 绘制。
排查顺序
- 先数清楚到底多少个点,不要凭感觉
- 少于 300 就别优化布局,多半是别的地方慢
- 300 以上先开 canvas 渲染,通常立竿见影
- 仍然卡就加视野裁剪或聚合
- 上万点直接换 WebGL 图层引擎
还有一个常被忽略的点:每给要素新建一个样式对象, 就等于让渲染层失去复用机会。几千个要素各自 new 一个 Style, 内存和绘制开销都会成倍增长。样式应该提前建好、反复使用。
经纬志(2026)。《标记点一多就卡》。https://www.jingweizhi.com/cookbook/marker-performance.html(访问日期 2026-11-03)。