地图数据太多加载卡?按 adcode 前缀切分 GeoJSON 分块按需加载
从 geojsoncn.com 这类站点做完 GeoJSON下载,接回前端最常见的一个坎就是"数据太大"。单个省几十 MB 还忍得,一旦想把全国区县甚至乡镇街道都挂上去,JSON.parse 的那一下就能让页面白屏两三秒,滚动缩放更是掉帧掉得没法看。这事的解法不是死磕渲染,而是先把数据"切成块",让浏览器只加载眼下需要的部分。这篇就讲我怎么用行政区划代码(adcode)的分层前缀做分块、按需加载,顺便把踩过的坑列清楚。
一、为什么全量加载会卡:问题在下行带宽和解析
先别急着优化渲染。拿一份全国区县边界算一笔账:5 万多个乡镇级要素,压缩前可能接近上百 MB。浏览器要做的不是"画一下"那么轻——它得先整包下载、再整包 JSON.parse,然后 Leaflet 才会开始建图层。这三个环节在大数据下每步都可能卡上秒级。
| 环节 | 全量时的问题 | 分块后 |
|---|---|---|
| 下载 | 上百 MB 整包传输,弱网要等很久 | 只拉当前所在区县那一小块,几十 KB |
| 解析 | JSON.parse 同步执行,阻塞主线程 | 每块小文件,解析几乎无感 |
| 渲染 | 一次建几千个 feature,事件大量堆积 | 当前视野内几百个,流畅 |
| 交互 | 缩放平移总在重算全部要素 | 只重算当前可见块,操作跟手 |
所以核心思路是:把一个大 GeoJSON 预先按"能下钻的行政层级"切成若干独立的小文件,前端只在用户定位到某个层级或区域时才 fetch 那一块。 这样就算这份 GeoJSON下载回来是全省甚至全国全量,页面实际加载的成本也被压到了单次、小块、几十 KB 的量级。
二、用 adcode 的前缀做天然的分块键
行政区划代码是分层的:6 位 adcode 里,前 2 位是省、前 4 位是市,6 位就是区县。用 520000(贵州)这种前缀做键,天然能把数据组织成"省目录 → 市目录 → 区县文件"的树,根本不用自己设计分片规则:
// 前端按需加载:先拿省代码,需要时再下钻到市、区县
const CODE = { '贵州省': '520000', '贵阳市': '520100', '云岩区': '520103' };
async function loadUnit(adcode) {
const res = await fetch(`/data/${adcode}.geojson`);
const fc = await res.json(); // 每一片都小,解析快
if (!fc.features) return; // 归一化兜底
return fc;
}
配合 Leaflet 的下钻,点一个市就 fetch 它下面各区的文件;放大地图再命中更细的乡镇片。整个过程零后端,静态目录 + 一个 JSON 清单就够了,这跟我之前写过的离线预览用的是同一套"下载 → 按需装配"的思路。
三、切块脚本:按首 2/4/6 位归组
生成端也好写:读整份 GeoJSON,把每个 feature.properties.adcode(或 code)的前缀提出来,按前缀归组各自落盘。以区县→地市为例:
const path = require('path');
const fs = require('fs');
function shard(fc, outDir) {
const groups = {};
for (const f of fc.features) {
const code = String(f.properties.adcode || f.properties.code);
const pre = code.slice(0, 4); // 地市前缀
(groups[pre] ||= []).push(f);
}
for (const [pre, feats] of Object.entries(groups)) {
fs.writeFileSync(path.join(outDir, `${pre}.geojson`),
JSON.stringify({ type: 'FeatureCollection', features: feats }));
}
}
想得更细,4 位地市文件还能再按 6 位拆到区县;乡镇街道就再往下拆。切得越小,单个文件越轻,前端按需加载的收益越明显——这也是很多生产系统里"一份 GeoJSON下载 + 一套切块脚本"的标准组合。完整的分层切瓦片思路,Github 上有人用 tippecanoe 的 -f 参数切矢量瓦片做同款策略,值得参考。
四、按需加载要配一套"已加载缓存"
直接每次 fetch 会重复下载——用户在同一市里来回拖动,别重复请求同一个文件。用一个 Map 当缓存,命中就直接用内存里的对象:
const cache = new Map();
async function loadCached(code) {
if (cache.has(code)) return cache.get(code);
const fc = await (await fetch(`/data/${code}.geojson`)).json();
cache.set(code, fc);
return fc;
}
这一套伺候下来,弱网环境的首屏也能做到"只花了加载当前省市的那一次请求",体验跟整包拖拽完全不是一个量级。真到了全国国界这么大体量的场景,更该考虑把边界切完直接走PMTiles 单文件 + MapLibre那种瓦片管线,而不是用 GeoJSON 硬扛。
五、三个容易踩的坑
1. adcode 不是连续编号,别用 parseInt 前 4 位。 地市代码有跳号,直接 String(code).slice(0,4) 比数值运算稳,避免 0100 这类被转丢前导零。我之前用 Number() 一次把 520100 切成 5201,文件对不上,排查了半小时。
2. 没带 adcode 的要素会落进 undefined 组。 有些第三方拿到的 GeoJSON 只有 name 没有 code,切块时先兜一个缺省目录,否则整组 feature 会静默丢失。更稳的是先做一遍几何与字段校验再切。
3. "坐标系的坑在分块后照旧"。 切块只是把文件变小,不解决投影问题。若数据是 CGCS2000 高斯平面投影,叠到 WGS84 底图照样偏出几百米;分块前务必先统一到 4326,相关转换可回看坐标系那篇。
这套"adcode 前缀分块 + 前端按需加载 + Map 缓存"的做法,成本几乎为零,却能把大数据量地图的首屏从"白屏三秒"拉回"秒开"。更多的数据下载、切分与格式细节,详见geojsoncn 主站。本站边界数据预览免费,若要按需导出区县边界成 GeoJSON、PNG 或 SVG 等格式,才涉及付费服务。