What we learned when a user tried to load a massive GML file in a browser.
Pangram verdict · v3.3
We believe that this document is fully AI-generated
AI likelihood · overall
AIArticle text · 1,824 words · 6 segments analyzed
The Problem That Started This A few weeks ago, a GeoDataViewer user wrote in asking why a large GML file wouldn’t load in their browser. The file — a geological map from a national survey — was roughly 1 GB in size and contained hundreds of thousands of mapped features. They had downloaded open data, unzipped it, dragged it into a web map, and nothing happened. This is not a bug report. This is a format problem. And it’s a problem far bigger than this one user. Every day, GIS professionals, geological surveyors, urban planners, and hobbyists hit the same wall: they acquire perfectly good open data, try to view it in a modern web browser, and the browser just … gives up. The solution exists. It has existed for over a decade. But most end users have never heard of it. This post is about why vector tiles are the answer, why partial solutions fall short, and why the gap between “it works in my desktop GIS” and “it works in my browser” remains the industry’s most underappreciated UX problem.
Part I: Why Browsers Can’t Handle Large Vector Datasets The browser is not QGIS. It is not ArcGIS Pro. It is a sandboxed runtime with a strict memory budget, a single-threaded (ish) DOM, and no access to your disk. Here is what happens when you drag a ~1 GB GML file into a web map: Step 1: Memory saturation The browser must read the entire file into memory. GML — like all XML-derived formats — is notoriously verbose. A ~1 GB GML file decompresses to roughly a gigabyte of XML text. Parse that into a DOM tree, and you are looking at 3–5 GB of peak memory for a naive parser. Even a streaming parser that outputs GeoJSON must materialize that data into an in-memory data structure. The resulting GeoJSON still requires the browser to hold the full dataset in memory before rendering can begin. Chrome on a typical laptop with 8 GB of RAM will crash, swap to death, or throw an Out of Memory error. Step 2: Parsing bottleneck XML parsing in JavaScript is slow. DOMParser is not optimized for multi-hundred-megabyte documents. The browser’s main thread blocks for tens of seconds — or minutes — while it tries to build a document tree.
There is no yield, no progress bar, just a frozen tab and a spinning cursor. Step 3: Rendering collapse Even if you somehow get the data into memory as GeoJSON, rendering hundreds of thousands of features is itself a challenge. Every feature becomes a DOM element in the canvas rendering pipeline. Every pan or zoom re-evaluates visibility, reprojects coordinates (if you forgot to pre-reproject — a second blocking operation), and redraws. Without tiling, the renderer must consider every feature on every frame. Frame rate drops below 1 FPS. The user gives up. Why This Affects Every Flat Format This is not a GML problem. It is a problem with every non-tiled vector format:
FormatBrowser IssueMemory for large datasetsGMLXML parsing is slow and memory-hungry; no native browser support~3–5 GB (DOM)Shapefile (.shp)Requires binary parser; must be fully loaded before rendering~600 MB–1 GB (uncompressed)GeoJSONDe facto standard, but no streaming; entire file must parse before first draw~700 MB+GeoJSON Lines (.geojsonl)Streamable line-by-line, but still must be held in memory as a single layer~700 MB (reduced peak during load)GeoParquetColumnar, efficient for analytics; requires WASM or DuckDB in browser; no native rendering supportDepends on query — better, but needs complex tooling Every single one of these formats shares the same fundamental limitation: the client must load the full dataset before it can render anything. ”But Desktop GIS Can Handle It” — No, It Can’t A common reaction at this point is: “Well, browsers are weak. I’ll just use QGIS or ArcGIS — they’re built for this.” The uncomfortable truth: they struggle too. Loading a ~1 GB GML file into QGIS can take 5–10 minutes on a modern laptop. During that time, the application is frozen — no pan, no zoom, no attribute table, no progress bar beyond a vague “Loading…” in the status bar.
ArcGIS Pro fares better on feature count, but its memory footprint for hundreds of thousands of features with full geometry sits at 1.5–2.5 GB — and that is before you open the attribute table, apply a symbology classification, or try to export a map. If your machine has 8 GB of RAM, you are now in swap territory. This is not a browser-vs-desktop issue. It is a data model issue. When a format requires loading all geometry into memory before any operation — whether that operation is “render to screen” or “run a spatial query” — the bottleneck is the same. Desktop GIS has a larger budget (more RAM, direct disk access, native threading), but it does not have a different architecture. The full dataset still lands in memory. The difference is degrees. A desktop application might take 5 minutes instead of crashing. But the user is still waiting. And the moment they need to share that data with a colleague on the web, they hit the browser wall anyway.
Part II: The Vector Tile Solution Vector tiles break this cycle with a deceptively simple insight: don’t load everything. Load only what’s visible at the current zoom level. A vector tile is a pre-tiled, pre-clipped chunk of data — typically 256×256 or 512×512 pixels at a given zoom level — encoded in a compact binary format (Mapbox Vector Tile / MVT, PBF). The browser requests tiles on demand as the user pans and zooms. Each tile is typically 10–100 KB. A session that views a national dataset may fetch only a few hundred tiles — a few megabytes total — instead of the full GeoJSON payload. How It Works Raw Data (GML, Shapefile, GeoJSON) │ ▼ [ETL / Conversion Pipeline] │ ├── Reproject to a web-friendly CRS ├── Simplify geometry (Douglas-Peucker, Visvalingam) ├── Clip to tile boundaries ├── Encode as MVT (Protocol Buffers) └── Write tile pyramid (MBTiles / PMTiles / directory) │ ▼ Tile Server / Storage
│ ├── Static files on CDN or S3 ├── PMTiles (single file archive) └── Tile server (Tegola, Martin, TileServer GL) │ ▼ Client (MapLibre GL / Mapbox GL / Leaflet) │ ├── Request tiles at viewport + zoom ├── Decode MVT → WebGL geometries ├── Style on the fly (paint, fill, labels) └── Render at 60 FPS Why It Works Clipping. Each tile contains only the features (or feature pieces) that intersect its bounding box. A feature that spans 100 km is divided across dozens of tiles — but at zoom 10, each tile might contain only a few simplified segments. Simplification. At low zoom levels, geometry is aggressively simplified. The 1,000-vertex coastline of a peninsula becomes a 10-vertex line. The simplification is invisible to the user because the pixels are too small to notice — but it reduces tile size by orders of magnitude. Zoom-level selection. Features can be filtered by zoom level. Minor roads appear only at zoom 12+. Municipal boundaries appear at zoom 8+. The renderer never receives data it cannot display. Demand-driven loading. The browser loads exactly the tiles for the current viewport. A user examining central Stockholm loads tiles for that area — not the entire country. Real-World Numbers For the type of dataset described above (hundreds of thousands of features, ~1 GB GML):
ApproachSizeMemoryLoad TimePan/ZoomRaw GML drag-and-drop~1 GB3–5 GB (crash)Minutes (blocked)N/AGeoJSON (pre-converted)~750 MB~750 MB~10 seconds parse~5 FPSGeoJSON + geojson-vt~750
MB~750 MB~10 seconds parse~30 FPSVector tiles (MVT)~50–150 MB total<100 MB (viewport only)<1 second initial60 FPS The vector tile pyramid for a national geological map, at zoom levels 0–14 with appropriate simplification, compresses to roughly 100 MB of MVT data — and most sessions will never download more than 10 MB of that.
Part III: Why geojson-vt Isn’t Enough MapLibre GL and Mapbox GL ship with a built-in feature called geojson-vt — a JavaScript library that performs client-side vector tiling. You pass it a GeoJSON object, and it generates tiles in the browser, on the fly. This sounds like a silver bullet. It is not. The Fundamental Problem geojson-vt does in-browser what should be done in a preprocessing step. It must:
Load the entire GeoJSON into memory (all ~750 MB) Build an internal tile index — a quadtree over all features Generate tiles on demand during pan/zoom
Step 1 is the killer. You still need to hold the full dataset in memory. The GeoJSON must be fully parsed and stored before any tiling can begin. For a ~1 GB GML → GeoJSON pipeline, the browser is still consuming nearly a gigabyte of RAM before rendering a single polygon. What geojson-vt Is Good For geojson-vt is excellent for small to medium datasets — say, up to 50,000 features or 100 MB of GeoJSON. It eliminates the need for a tile pipeline during prototyping. For a city’s worth of buildings or a single county’s roads, it works perfectly. Where It Breaks Beyond that threshold, you hit:
Memory ceiling: The browser cannot allocate ~750 MB for a single JavaScript object. Even if it succeeds, the garbage collector thrashes, causing visible hitches. Parse-time blocking: Building the quadtree from hundreds of thousands of features takes 2–5 seconds on a fast machine, freezing the UI. No simplification: geojson-vt clips but does not simplify. At low zoom levels, tiles contain unnecessarily detailed geometry.
No reprojection: If your data is in a projected CRS, you must reproject each coordinate in the browser — a blocking O(n) operation over tens of millions of points.
In short: geojson-vt is a workaround for the absence of a tile pipeline, not a replacement for one. It trades server-side precomputation for client-side compute and memory — a trade that becomes untenable at scale.
Part IV: The Vector Tile Pipeline, End to End What does it actually take to serve a large geological map as vector tiles? Here is the pipeline, with real performance numbers. Step 1: Parse and Reproject (GML → GeoJSONL) The GML file is first converted to GeoJSONL using a streaming parser. This is the only step that touches the full ~1 GB file:
MetricValueInput~1 GB GMLFeaturesHundreds of thousandsCoordinatesTens of millionsParse time~65 secondsPeak memory~19 MB (streaming)Output~750 MB GeoJSONLReprojectionFrom projected CRS to EPSG:4326 Step 2: Generate Vector Tiles (GeoJSONL → MVT) The GeoJSONL is fed into Tippecanoe (or a similar tile generator) to produce an MBTiles or PMTiles archive: tippecanoe -o output.pmtiles \ --layer=features \ --maximum-zoom=14 \ --minimum-zoom=5 \ --simplification=10 \ --coalesce-smallest-tiles \ input.geojsonl Key parameters:
Zoom range 5–14: Zoom levels below 5 don’t need detailed geology. Zoom levels above 14 hit diminishing returns for this data scale. Simplification: Douglas-Peucker with tolerance 10 meters preserves visual fidelity while reducing tile sizes by 60–80%. Dropping-dot: Tippecanoe removes features that are too small to see at each zoom level — a free compression win.
Step 3: Deploy and Serve PMTiles (a single-file tile archive) is placed on any static file server or CDN.