前端一次画 2800 个区县面就卡?SVG、Canvas、WebGL 三条渲染路线怎么选
--- 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 自带,一个参数 |
| WebGL | GPU 三角面片 | 流畅 | 流畅 | feature-state,需配 id | 引入 MapLibre,200 多 KB |
选型建议
省级地图直接用 Leaflet 默认 SVG;区县级全量展示加 preferCanvas: true;乡镇级或更大规模换 MapLibre GL。不过渲染优化替代不了数据瘦身,先按NDJSON 流式处理的办法裁掉用不到的区域和属性字段,再谈渲染器。少画不需要的数据,比换什么渲染器都管用。
全国省、市、区县三级边界 GeoJSON 可以在 GeoJSON 中国按层级浏览下载,属性带 name 与 adcode 字段,三段示例代码都可以直接套用。