性能与大量点
Leaflet 的默认矢量渲染器是 SVG,每个要素对应一个 DOM 节点。 超过几千个点,瓶颈就不再是网络,而是 DOM 节点数量与每次缩放时的重排。 解决方向只有三个:换渲染器、减少同时存在的要素、降低要素的复杂度。
先认清成本
| 方案 | 可承受数量级 | 交互能力 | 代价 |
|---|---|---|---|
L.marker | 数百到三千 | 每个都是 DOM,可放任意 HTML | 节点多、阴影多、事件多 |
L.circleMarker + SVG | 数千 | 支持 hover、click | 节点数量同样随点数增长 |
L.circleMarker + Canvas | 数万 | 支持点击命中检测,不支持 CSS hover | 整层重绘,单点更新成本高 |
| 聚合图层 | 十万以上 | 点被折叠,需展开才能点选 | 精度损失,需要额外插件 |
| 矢量瓦片 / WebGL | 百万级 | 样式受瓦片管线约束 | 需要服务端出瓦片或引入 WebGL 图层 |
这张表是量级参考,不是硬边界。同一台设备上,点的样式复杂度、 是否有弹窗、是否绑定 hover,都会让临界值上下浮动一个数量级。
换成 Canvas 渲染器
两种开法:地图级 preferCanvas: true,让所有矢量图层默认走 canvas;
或者建一个渲染器实例,显式传给图层。后者适合「只有点层用 canvas、
面和线仍要 SVG 的交互」的混合场景。
var map = L.map('map', {
center: [39.907359, 116.391263],
zoom: 10,
preferCanvas: true,
renderer: L.canvas({ padding: 0.5 })
});
var canvasLayer = L.canvas({ padding: 0.5 });
var points = [];
for (var i = 0; i < 50000; i++) {
points.push([
39.5 + Math.random() * 0.9,
116.0 + Math.random() * 0.8
]);
}
var dots = L.layerGroup();
points.forEach(function (ll) {
dots.addLayer(L.circleMarker(ll, {
radius: 3,
stroke: false,
fillColor: '#17694a',
fillOpacity: 0.6,
renderer: canvasLayer // 统一走同一个画布
}));
});
dots.addTo(map);
padding 是画布的预留边距,单位是容器尺寸的倍数。
0.5 表示在可视区外多画半屏,拖动时不会露出空白;
设得太大会增加每帧的绘制量,0.1 到 0.5 之间通常够用。
一个渲染器实例只应被一层共用。每层各建一个 L.canvas(),
页面上就会出现多张叠在一起的画布,内存与重绘次数一起翻倍。
减少同时存在的要素
数据有十万条,不等于要画十万个点。最有效的一步永远是在数据侧裁剪: 只取当前视野内的记录,或者按缩放级别取不同精度的数据集。 客户端也可以先做一次包围盒粗筛,再交给 Leaflet。
function inView(ll, bounds, pad) {
return ll[0] > bounds.getSouth() - pad &&
ll[0] < bounds.getNorth() + pad &&
ll[1] > bounds.getWest() - pad &&
ll[1] < bounds.getEast() + pad;
}
function renderVisible(all, layer) {
var b = map.getBounds();
var pad = (b.getNorth() - b.getSouth()) * 0.2;
layer.clearLayers();
var batch = [];
for (var i = 0; i < all.length; i++) {
if (inView(all[i], b, pad)) batch.push(all[i]);
}
// 分批加入,避免一次阻塞主线程
var step = 800;
(function addChunk(start) {
if (start >= batch.length) return;
for (var j = start; j < Math.min(start + step, batch.length); j++) {
layer.addLayer(L.circleMarker(batch[j], {
radius: 3, stroke: false, fillOpacity: 0.6
}));
}
requestAnimationFrame(function () { addChunk(start + step); });
})(0);
}
map.on('moveend', function () {
renderVisible(allPoints, dots);
});
每次 moveend 全量重建并不可取。实际项目里配合防抖、
按瓦片编号缓存已渲染的点集,才能把拖动时的开销压到可接受范围。
逐帧操作的三个开关
| 参数 / 做法 | 作用 | 建议 |
|---|---|---|
smoothFactor | 折线简化强度,值越大顶点越少 | 轨迹类数据设 1.5 到 2 |
updateWhenZooming | 缩放过程中是否持续重绘矢量层 | 数据多时设 false,缩放结束再画 |
updateWhenIdle | 拖动过程中是否加载新瓦片 | 移动端设 true,减少抖动 |
注意事项
Canvas 层没有 CSS hover
canvas 里画的是像素,不是节点,:hover、CSS 动画都用不上。
悬停高亮要在 mouseover 里改该图层的 setStyle,
并接受整层重绘的开销。
大量 Marker 时先怀疑阴影
每个 L.marker 默认带一张阴影图,等于节点数翻倍、
请求数翻倍。视觉上不需要阴影时,shadowUrl: null 是零成本的优化。
数据更新时反复重建图层
每次刷新都 removeLayer 再全量添加,会让地图闪烁并丢掉已打开的状态。
能增量更新的场景,用 layer.setLatLng()、
layer.setStyle() 改已有对象,比销毁重建便宜得多。
经纬志(2026)。《性能与大量点》。https://www.jingweizhi.com/leaflet/tutorial/performance-points.html(访问日期 2026-11-03)。