How to Run Python GeoPandas and Shapely in the Browser with Pyodide: No Server Required
GeoLibre enables full GeoPandas and Shapely vector processing directly in the browser using Pyodide, eliminating any server dependency while maintaining identical results to the native FastAPI side-car.
Running Python geospatial libraries in the browser has historically required a back-end server. The opengeos/GeoLibre project breaks this constraint by embedding Pyodide—a WebAssembly-based Python runtime—directly into its web build. This article explains the complete architecture, from shared Python modules to Web Worker execution, so you can implement the same pattern in your own applications.
Shared Python Code: One Source of Truth
The foundation of GeoLibre's browser-side processing is vector_ops.py, a pure-Python module containing all core geometry functions. Located at backend/geolibre_server/geolibre_server/vector_ops.py, this file serves both the native FastAPI side-car and the Pyodide browser engine.
# backend/geolibre_server/geolibre_server/vector_ops.py
# Core functions for buffer, intersection, union, dissolve, etc.
# Used identically by server and browser builds
By maintaining a single implementation, GeoLibre guarantees that vector operations produce identical results regardless of execution environment.
Build-Time Integration: Vite Copy Plugin
To make the Python module available in the browser, a custom Vite plugin copies vector_ops.py into the frontend bundle during build:
// apps/geolibre-desktop/vite-plugins/copy-vector-ops.ts
// Copies vector_ops.py to src/lib/pyodide/vector_ops.generated.py
This transformation happens automatically, ensuring the browser always has the latest shared logic without manual file synchronization.
Pyodide Runtime Configuration
The Pyodide runtime URL is configurable via environment variable, defaulting to the public jsDelivr CDN:
// apps/geolibre-desktop/src/lib/pyodide/pyodide-config.ts
const PYODIDE_VERSION = "0.25.0"; // or specified version
const indexURL = import.meta.env.VITE_PYODIDE_INDEX_URL
?? `https://cdn.jsdelivr.net/pyodide/v${PYODIDE_VERSION}/full/`;
Key configuration points:
- First load cost: The Pyodide WASM payload downloads once (~tens of MB)
- Subsequent operations: Reuse the warmed-up interpreter for near-instant execution
- Self-hosting option: Point
VITE_PYODIDE_INDEX_URLto your own mirror for air-gapped environments
Web Worker Execution Architecture
A dedicated Web Worker at public/pyodide/pyodide-worker.js manages the Pyodide lifecycle:
// apps/geolibre-desktop/src/lib/pyodide/pyodide-worker.js
// Loads Pyodide, installs geopandas, imports vector_ops.generated.py
// Executes tools over JSON-serialized message passing
The worker pattern prevents UI blocking during heavy geometry operations. It loads Pyodide from the configured CDN, installs the geopandas package (which includes Shapely), then imports the copied vector_ops.generated.py module.
UI Integration: Automatic Engine Selection
The VectorToolsDialog component automatically selects the appropriate engine based on build configuration:
// apps/geolibre-desktop/src/components/processing/VectorToolsDialog.tsx
// IS_MAS_BUILD true → automatically uses Pyodide engine
// Otherwise → connects to FastAPI side-car
When Pyodide is active, the pyodide-vector-loader handles parameter marshaling, progress streaming, and result conversion:
// apps/geolibre-desktop/src/lib/pyodide/pyodide-vector-loader.ts
interface VectorToolParams {
tool: string;
layerId: string;
params: Record<string, any>;
onProgress?: (percent: number) => void;
}
export async function runVectorToolInPyodide(
params: VectorToolParams
): Promise<GeoJSON.FeatureCollection> {
// Forwards to worker, awaits result, streams progress
}
Complete Usage Examples
From the Application UI
import { runVectorToolInPyodide } from "./pyodide/pyodide-vector-loader";
async function bufferLayer(layerId: string, distance: number) {
const result = await runVectorToolInPyodide({
tool: "buffer",
layerId,
params: { distance, resolution: 16 },
onProgress: (p) => console.log(`Progress: ${p}%`),
});
// result is GeoJSON FeatureCollection ready for map display
addGeoJsonLayer(result);
}
Direct Pyodide in Custom Console
const pyodide = await loadPyodide({
indexURL: "https://cdn.jsdelivr.net/pyodide/v0.25.0/full/",
});
await pyodide.loadPackage(["geopandas", "shapely"]);
const geojson = pyodide.runPython(`
import geopandas as gpd
from shapely.geometry import Point
# Create sample data
gdf = gpd.GeoDataFrame(
{"name": ["A", "B"]},
geometry=[Point(0, 0), Point(1, 1)],
crs="EPSG:4326"
)
# Reproject and buffer
gdf_utm = gdf.to_crs("EPSG:32633")
buffered = gdf_utm.buffer(1000) # 1km buffer in meters
# Return as GeoJSON
gdf_utm.set_geometry(buffered).to_json()
`);
const featureCollection = JSON.parse(geojson);
File Reference Table
| Component | Path | Purpose |
|---|---|---|
| Shared geometry logic | backend/geolibre_server/geolibre_server/vector_ops.py |
Single source of truth for all vector operations |
| Build-time copy plugin | apps/geolibre-desktop/vite-plugins/copy-vector-ops.ts |
Copies Python module into frontend bundle |
| Runtime configuration | apps/geolibre-desktop/src/lib/pyodide/pyodide-config.ts |
CDN URL and version management |
| Web Worker | apps/geolibre-desktop/public/pyodide/pyodide-worker.js |
Pyodide lifecycle and package installation |
| UI bridge | apps/geolibre-desktop/src/lib/pyodide/pyodide-vector-loader.ts |
Parameter passing and progress streaming |
| Engine selection | apps/geolibre-desktop/src/components/processing/VectorToolsDialog.tsx |
Automatic Pyodide/side-car routing |
Performance Considerations
- Initial load: 10-30 MB WASM download depending on Pyodide version and packages
- Package caching:
geopandasand dependencies download once, cached by browser - Memory limits: Constrained by browser tab (typically 2-4 GB for modern browsers)
- Suitable workloads: Vector operations on datasets up to ~100k features depending on complexity
For larger datasets, the FastAPI side-car remains available as a fallback option.
Summary
- Single Python module:
vector_ops.pyensures identical results across server and browser - Build-time copying: Vite plugin integrates Python code into the frontend bundle
- Configurable runtime: Environment variable controls Pyodide CDN or mirror URL
- Worker isolation: Web Worker prevents UI blocking during geometry operations
- Automatic fallback: UI components transparently select browser or server engine
- Zero server dependency: Full GeoPandas/Shapely capabilities available offline after initial load
Frequently Asked Questions
What file size should users expect when loading Pyodide for the first time?
The initial download is approximately 10-30 MB depending on the Pyodide version and which packages are loaded. GeoLibre loads geopandas which includes Shapely and its dependencies. This payload caches in the browser, making subsequent visits nearly instant.
How does GeoLibre guarantee the same results between browser and server processing?
Both environments execute the identical Python code from backend/geolibre_server/geolibre_server/vector_ops.py. A Vite plugin copies this file into the frontend at build time, ensuring the browser and FastAPI side-car always run the same implementation.
Can Pyodide handle the same dataset sizes as a native Python environment?
Browser memory constraints limit Pyodide to smaller datasets than a dedicated server—typically tens to hundreds of thousands of features depending on geometry complexity. GeoLibre provides automatic fallback to the FastAPI side-car when server processing is available.
Is internet connectivity required after the initial Pyodide load?
Only if self-hosting is not configured. By default, GeoLibde uses the jsDelivr CDN. Setting VITE_PYODIDE_INDEX_URL to a local mirror enables fully offline operation after the first download.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →