经纬志
首页 / 实战手册 / Leaflet 容器与图层

Canvas 渲染后,拖动地图留下残影

先看结论

为了提升性能打开了 preferCanvas,结果平移时老的圆形没擦掉,缩小时整片线网糊成一团。

最后验证 2026-11-03 · 多技术栈 · 更新 2026-10-29 · 引用本文

现象

拖动过程中,前一帧的圆点残留在画布上,松手后逐渐消失。 缩放时线条变粗、交叉处出现深色斑块。 要素数量在 3000 以上时最明显,数量少时几乎看不出来。

原因

Leaflet 的 Canvas 渲染器只在视口范围内绘制,并留出一个小边距 padding(默认 0.1,即 10% 视口)。平移超过这个边距后, 新进入的区域没有预绘制,旧区域又来不及清除,就出现残影。

缩放过程中默认不重绘(updateWhenZooming: false), 画布被整体缩放,线条按比例放大后变粗,形成视觉上的“糊”。

排查过程

  1. 把 preferCanvas 关掉,用 SVG 渲染对照,确认残影只在 Canvas 下出现。
  2. 减少要素到 500 个,看残影是否消失,判断是否与重绘耗时有关。
  3. 打印渲染器选项:layer.getRenderer().options。
console.log(layer.getRenderer().options);
// { padding: 0.1, tolerance: 0, updateWhenZooming: false }

解决方案

按要素范围与移动幅度调大 padding,让渲染器提前绘制视口外的内容。 代价是每次重绘面积变大,需要与要素数量权衡:

var canvasRenderer = L.canvas({
  padding: 0.5,        // 预绘制 50% 视口边距
  tolerance: 5,        // 点击命中的像素容差
  updateWhenZooming: true
});

var layer = L.geoJSON(data, {
  renderer: canvasRenderer,
  pointToLayer: function (feature, latlng) {
    return L.circleMarker(latlng, {
      radius: 4,
      weight: 1,
      color: '#1c7ed6',
      fillOpacity: 0.6
    });
  }
}).addTo(map);

要素很多、缩放时确实卡,可以保留 updateWhenZooming: false, 改为缩放结束后统一重绘,同时把线条宽度设成固定像素:

L.geoJSON(data, {
  renderer: L.canvas({ padding: 0.3, updateWhenZooming: false }),
  style: function () {
    return { weight: 1, opacity: 0.8 };   // 用整数像素,缩放时不变形
  }
}).addTo(map);

仍然有残影时,把矢量图层拆成两个渲染器:点层用一个,面层用另一个。 分开清屏,避免大面积填充反复覆盖:

var pointRenderer = L.canvas({ padding: 0.5 });
var polygonRenderer = L.canvas({ padding: 0.2 });

L.geoJSON(points, { renderer: pointRenderer }).addTo(map);
L.geoJSON(polygons, { renderer: polygonRenderer }).addTo(map);

验证

连续拖动地图 10 秒,松开后画布上没有旧图形残留。 缩放到 16 级,线宽保持 1 像素左右,不出现成片深色斑块。 用 performance 面板确认拖动时帧率仍在 40 fps 以上。

优先加 padding:残影的根因是预绘制范围不足。 padding 从 0.1 提到 0.5 通常就能解决, 比换渲染方案改动小得多。

引用本文

经纬志(2026)。《Canvas 渲染后,拖动地图留下残影》。https://www.jingweizhi.com/cookbook/leaflet-canvas-ghost.html(访问日期 2026-11-03)。