GeoJSONcn
GeoJSONcn / 技术专栏 / 前端一次画 2800 个区县面就…

前端一次画 2800 个区县面就卡?SVG、Canvas、WebGL 三条渲染路线怎么选

发布于 2026-07-30 · GeoJSONcn 技术团队 · 阅读约 11 分钟

--- title: 前端一次画 2800 个区县面就卡?SVG、Canvas、WebGL 三条渲染路线怎么选 ---

把全国区县级 GeoJSON 直接丢给 L.geoJSON(),页面多半会卡:拖动掉帧、缩放迟滞,严重时浏览器直接无响应。此前我们讨论过TopoJSON 压缩体积和Douglas-Peucker 顶点抽稀,这两招解决的是"传输"问题,文件变小、下载变快。但数据到了浏览器之后怎么画出来,是另一个独立的问题。渲染路线选错了,文件再小照样卡。这篇把前端画面数据的三条路线捋一遍:SVG、Canvas、WebGL,代码都能直接跑。

路线一:SVG,Leaflet 的默认选择

Leaflet 默认用 SVG 渲染矢量要素,每一个 Polygon 对应 DOM 里的一个 节点。全国 2800 多个区县面,就是 2800 多个 path 节点。每次拖动、缩放,浏览器都要对这些节点重新计算布局与合成。

SVG 的好处在交互:每个要素就是一个真实的 DOM 元素,可以直接绑定事件、用 CSS 控制 hover 样式、加 className。做省级地图(几十到几百个面)时体验很好。但要素数量上到数千,DOM 开销就压不住了,实测区县级全量数据在中端笔记本上拖动帧率会掉到 20 帧以下。

经验值:面数量在 1000 以内,用默认 SVG 渲染器没有问题。

路线二:Canvas,一行配置的免费加速

Leaflet 内置了 Canvas 渲染器,把所有矢量要素画到一张 2D 画布上,DOM 节点数从 2800 个变成 1 个。启用只要一个参数:

const map = L.map('map', { preferCanvas: true }).setView([35.8, 104.1], 4);

fetch('quxian.geojson')
  .then(r => r.json())
  .then(fc => {
    L.geoJSON(fc, {
      style: { color: '#1f6feb', weight: 1, fillOpacity: 0.25 },
      onEachFeature: (f, layer) => layer.bindTooltip(f.properties.name)
    }).addTo(map);
  });

画布上没有 DOM 元素了,点击和 hover 怎么还能触发?Leaflet 在 Canvas 模式下自己做命中检测:鼠标事件发生时,遍历画布上的要素,逐个判断鼠标点是否落在几何内部,本质上就是我们在R-tree 空间索引一文里讲过的 point-in-polygon 判定。要素多的时候这一步有开销,但比起 DOM 重排仍然便宜得多。

有一点要留意,Canvas 模式下 SVG 专属的能力会失效:className 选项不再有 CSS 效果,hover 样式要改成在 mouseover 事件里调用 layer.setStyle()。

经验值:区县级 2800 多个面,Canvas 渲染器可以流畅拖动缩放;但数据上到数万面(乡镇级),每帧全量重绘 2D 画布也会吃力。

路线三:WebGL,MapLibre GL 扛住十万级要素

要素规模到几万以上,就该让 GPU 干活了。MapLibre GL JS(Mapbox GL JS v1 的开源分支,BSD 协议)把 GeoJSON 面数据三角化后交给 WebGL 渲染,几十万个面也能维持 60 帧:

const map = new maplibregl.Map({
  container: 'map',
  style: './style.json', // 底图样式文件,可指向天地图或自建瓦片
  center: [104.1, 35.8],
  zoom: 3
});

map.on('load', () => {
  map.addSource('quxian', {
    type: 'geojson',
    data: 'quxian.geojson',
    tolerance: 0,      // 关闭内部几何简化,保住边界精度
    generateId: true   // 为 feature-state 生成数值 id
  });
  map.addLayer({
    id: 'quxian-fill',
    type: 'fill',
    source: 'quxian',
    paint: {
      'fill-color': '#1f6feb',
      'fill-opacity': ['case',
        ['boolean', ['feature-state', 'hover'], false], 0.5, 0.25]
    }
  });
});

两个参数值得展开说。MapLibre 拿到 GeoJSON 后并不直接渲染,而是先用内置的 geojson-vt 在浏览器里切成矢量瓦片,切片过程默认带几何简化(tolerance 默认 0.375)。做行政区划边界展示时,相邻区县共享边界,简化不同步可能出现细缝,所以示例里把 tolerance 设为 0,简化工作交给数据预处理阶段完成。generateId: true 则是 hover 高亮的前提:feature-state 机制要求每个要素有数值型 id,行政区划数据的 adcode 是字符串属性,不会被自动当作 id 用。

代价是引入一个不小的依赖:MapLibre GL JS 压缩后 200 多 KB,还要求浏览器支持 WebGL。

三条路线对比

路线渲染载体2800 面(区县级)数万面(乡镇级)hover 交互依赖成本
SVG每要素一个 path 节点拖动明显掉帧基本不可用DOM 事件 + CSS,天然支持Leaflet 自带
Canvas单张 2D 画布流畅重绘吃力内部逐要素命中判定Leaflet 自带,一个参数
WebGLGPU 三角面片流畅流畅feature-state,需配 id引入 MapLibre,200 多 KB

选型建议

省级地图直接用 Leaflet 默认 SVG;区县级全量展示加 preferCanvas: true;乡镇级或更大规模换 MapLibre GL。不过渲染优化替代不了数据瘦身,先按NDJSON 流式处理的办法裁掉用不到的区域和属性字段,再谈渲染器。少画不需要的数据,比换什么渲染器都管用。

全国省、市、区县三级边界 GeoJSON 可以在 GeoJSON 中国按层级浏览下载,属性带 name 与 adcode 字段,三段示例代码都可以直接套用。