GeoJSONcn
GeoJSONcn / 技术专栏 / 地图数据太多加载卡?按 adco…

地图数据太多加载卡?按 adcode 前缀切分 GeoJSON 分块按需加载

发布于 2026-08-12 · GeoJSONcn 技术团队 · 阅读约 12 分钟

从 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 等格式,才涉及付费服务。