经纬志
首页 / 实战手册 / 数据加载

20 MB 的 GeoJSON 同步解析,主线程卡了 8 秒

先看结论

文件不大,但打开页面后转圈八秒,期间点击任何按钮都没反应。

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

现象

一份 20.4 MB、12 万个 Polygon 的行政区划 GeoJSON。 fetch 只用了 600 ms,但 L.geoJSON(data).addTo(map) 之后 页面冻结约 8 秒,Performance 面板显示主线程被一个任务占满, 出现 [Violation] 'click' handler took 8120ms。

加载完成后地图拖动也很卡,帧率从 60 掉到 12。

原因

三个阶段都在主线程:

  • JSON.parse 解析 20 MB 文本,约 300~600 ms。
  • L.geoJSON 为每个要素创建 L.Path、计算 bounds、绑定事件,12 万次对象创建约 5 秒。
  • 每帧渲染 12 万个 SVG 路径,帧率必然崩溃。

解析本身不是瓶颈,逐个创建矢量对象才是。 Leaflet 的 SVG 渲染器对超过一万个路径就无法维持 60 帧。

排查过程

  1. Performance 面板录制加载过程,看长任务(红色三角)落在解析还是渲染阶段。
  2. 在 fetch 之后、L.geoJSON 之前打时间戳,分离网络与解析耗时。
  3. 统计要素数量与顶点总数,顶点数超过 50 万就需要先简化。
  4. 把要素数从 12 万降到 5000 再测一次,若卡顿消失,就是数量问题。
const t0 = performance.now();
const res = await fetch('/data/districts.geojson');
const text = await res.text();
const t1 = performance.now();          // 网络 + 下载
const fc = JSON.parse(text);
const t2 = performance.now();          // 解析
L.geoJSON(fc).addTo(map);
const t3 = performance.now();          // 建对象
console.log('fetch', t1 - t0, 'parse', t2 - t1, 'render', t3 - t2);

解决方案

三条路按投入从低到高:先简化几何,再按视野裁剪,最后换渲染方式。

// 1. 用 mapshaper 预简化,顶点降到 10% 以内
// npx mapshaper districts.geojson -simplify 10% keep-shapes -o precision=0.0001 out.geojson

// 2. 服务端按 bbox 返回,只取视野内的要素
const b = map.getBounds();
fetch(`/api/districts?bbox=${b.getWest()},${b.getSouth()},${b.getEast()},${b.getNorth()}`);

// 3. 渲染层换成 Canvas,显著减少对象开销
L.geoJSON(fc, { renderer: L.canvas({ padding: 0.5 }) }).addTo(map);

// 4. 只做展示、不需要交互时,直接渲染成图片或矢量瓦片
// Mapbox GL JS + vector tiles 可承载 10 万级要素

必须保留全量数据时,用 requestIdleCallback 分批加入,避免单帧阻塞。

const queue = fc.features.slice();
function step(deadline) {
  while (queue.length && deadline.timeRemaining() > 2) {
    L.geoJSON(queue.shift()).addTo(layerGroup);
  }
  if (queue.length) requestIdleCallback(step);
}
requestIdleCallback(step);

验证

优化后 render 阶段应低于 800 ms,首屏可交互时间小于 2 秒; 拖动地图时帧率保持在 50 以上,Performance 面板不再出现超过 50 ms 的长任务。

console.assert(t3 - t2 < 800, '建对象仍超过 800ms');
// 帧率检查:连续拖动 3 秒,统计 requestAnimationFrame 次数
let frames = 0; const start = performance.now();
(function tick() { frames++; if (performance.now() - start < 3000) requestAnimationFrame(tick);
  else console.log('fps', frames / 3); })();

适用版本

库 / 工具版本说明
Leaflet1.9.xSVG 渲染器建议不超过 1 万路径,Canvas 可到 5 万
Mapbox GL JS2.15 / 3.x矢量瓦片方案可承载十万级要素
mapshaper0.6.x-simplify 支持按百分比或精度
引用本文

经纬志(2026)。《20 MB 的 GeoJSON 同步解析,主线程卡了 8 秒》。https://www.jingweizhi.com/cookbook/geojson-large-freeze.html(访问日期 2026-11-03)。