KunYu
h3-jsngeohashjavascriptspatial-indexnpm

h3-js vs ngeohash: Die richtige JavaScript-Bibliothek für räumliche Indizierung wählen

Bundle-Größen, Encoding-Benchmarks, API-Vergleich und Produktionsfallen bei h3-js und ngeohash — mit echten Messungen.

KunYu TeamMarch 25, 20269 Min. Lesezeit

Falls du dich noch nicht zwischen H3-Hexagonen und Geohash-Rechtecken entschieden hast, lies zuerst unseren H3 vs Geohash vs S2 Vergleich. Dieser Artikel behandelt nur den nächsten Schritt: welches npm-Paket du installieren solltest.

h3-js und ngeohash sind die meistgeladenen JavaScript-Bibliotheken für H3 bzw. Geohash. Wir verwenden beide in KunYus GeoHash & H3 Converter-Tool. Die Bundle-Größe unterscheidet sich um den Faktor 22, die Encoding-Geschwindigkeit um 4–6x, und die Produktionsfallen sind völlig unterschiedlich. Hier die gemessenen Zahlen und die Probleme, die wir erst nach dem Deployment entdeckt haben.

Bundle-Größe: 86 KB vs 3,8 KB

Gemessen durch Gzip-Komprimierung der ES-Module-Einstiegspunkte direkt aus node_modules:

Metrik h3-js v4.4.0 ngeohash v0.6.3
ES module (gzipped) 86 KB 3,8 KB
Gesamtpaketgröße 6,5 MB 52 KB
Runtime-Abhängigkeiten 0 0
Implementierung C → Emscripten Reines JavaScript

Der Unterschied liegt in der Implementierung. h3-js ist Ubers C-Bibliothek, kompiliert durch Emscripten: Ikosaederprojektion, hexagonale Gittertraversierung, Polygon-Clipping, alles in einer JS-Datei gebündelt, kein tree-shaking möglich. Wenn du nur { latLngToCell } importierst, werden trotzdem die vollen 464 KB ausgeliefert. ngeohash besteht aus 6 handgeschriebenen JS-Dateien — die gesamte Bibliothek ist kleiner als die Type-Definitionen von h3-js.

86 KB gzipped entspricht ungefähr der Größe von React DOM. Auf dem Server spielt das keine Rolle, aber für ein Frontend-Projekt, das nur einen einzigen Encode-Aufruf braucht, ist das ein weiteres React DOM, das du für eine einzelne Funktion auslieferst.

Encoding-Geschwindigkeit: ngeohash 4–6x schneller

Node.js v24, 100.000 Iterationen pro Operation mit 1.000 Aufwärm-Iterationen:

Operation h3-js ngeohash Unterschied
Encode (lat/lng → cell) 850K ops/sec 4,86M ops/sec ngeohash 5,7x
Decode (cell → lat/lng) 1,73M ops/sec 7,58M ops/sec ngeohash 4,4x
Neighbors (benachbarte Zellen) 614K ops/sec 465K ops/sec h3-js 1,3x
Boundary / BBox 550K ops/sec 9,29M ops/sec ngeohash 17x

Das Benchmark-Skript (als .mjs speichern und mit node ausführen):

import { latLngToCell, cellToLatLng, gridDisk, cellToBoundary } from "h3-js";
import ngeohash from "ngeohash";

const ITERATIONS = 100_000;

function bench(name, fn) {
  for (let i = 0; i < 1000; i++) fn(); // warmup
  const start = performance.now();
  for (let i = 0; i < ITERATIONS; i++) fn();
  const ms = performance.now() - start;
  const opsPerSec = Math.round((ITERATIONS / ms) * 1000);
  console.log(`${name}: ${opsPerSec.toLocaleString()} ops/sec`);
}

const h3Cell = latLngToCell(37.7749, -122.4194, 9);
const hash = ngeohash.encode(37.7749, -122.4194, 6);

bench("h3-js  encode",    () => latLngToCell(37.7749, -122.4194, 9));
bench("ngeohash encode",  () => ngeohash.encode(37.7749, -122.4194, 6));
bench("h3-js  decode",    () => cellToLatLng(h3Cell));
bench("ngeohash decode",  () => ngeohash.decode(hash));
bench("h3-js  neighbors", () => gridDisk(h3Cell, 1));
bench("ngeohash neighbors", () => ngeohash.neighbors(hash));
bench("h3-js  boundary",  () => cellToBoundary(h3Cell));
bench("ngeohash bbox",    () => ngeohash.decode_bbox(hash));

Für einfaches Encode/Decode dominiert ngeohash — reine JS-Bit-Interleaving-Operationen treffen genau den Sweet Spot von V8 JIT, kein FFI-Overhead. h3-js zahlt bei jedem Aufruf einen Marshaling-Preis: Argumente werden in den Emscripten-Heap geschrieben, die C-Funktion läuft, und Ergebnisse werden zurück nach JavaScript kopiert.

Aber h3-js holt bei der Nachbarsuche auf. gridDisk traversiert das ikosaedrische Gitter in einer engen C-Schleife, und die Effizienz des kompilierten Algorithmus überwiegt die Serialisierungskosten.

Die Boundary-Zeile ist kein fairer Vergleich: cellToBoundary berechnet 6 Vertex-Koordinaten für ein Hexagon, decode_bbox gibt nur 4 Zahlen zurück. Aber das sind die tatsächlichen Operationen, die du in der Produktion aufrufen würdest.

Encoding-Geschwindigkeit ist kein Entscheidungskriterium. h3-js kodiert eine Koordinate in 1,2 Mikrosekunden. Selbst bei 10.000 Punkten pro Request bist du unter 12 ms. Erst ab Millionen-Batches wird der Unterschied relevant.

API-Vergleich

Encode und Decode

import { latLngToCell, cellToLatLng } from "h3-js";
import ngeohash from "ngeohash";

// H3: resolution 9 ≈ 0.11 km²
const h3Cell = latLngToCell(37.7749, -122.4194, 9);
// → "8928308280fffff"

// Geohash: precision 6 ≈ 0.74 km²
const hash = ngeohash.encode(37.7749, -122.4194, 6);
// → "9q8yyk"

// Decode
const [lat, lng] = cellToLatLng(h3Cell);
const { latitude, longitude } = ngeohash.decode(hash);

Neighbors

import { gridDisk } from "h3-js";

// H3: center + 6 hexagonal neighbors
const h3Neighbors = gridDisk(h3Cell, 1);

// Geohash: 8 neighbors (N, NE, E, SE, S, SW, W, NW)
const ghNeighbors = ngeohash.neighbors(hash);

Cell Boundary

import { cellToBoundary } from "h3-js";

// H3: 6 [lat, lng] vertices
const h3Boundary = cellToBoundary(h3Cell);

// Geohash: bounding box
const bbox = ngeohash.decode_bbox(hash);
// → [minLat, minLon, maxLat, maxLon]

decode_bbox gibt [minLat, minLon, maxLat, maxLon] zurück — Süd, West, Nord, Ost. Nicht die [west, south, east, north]-Reihenfolge, die GeoJSON verwendet, und nicht die Reihenfolge, die Leaflets LatLngBounds erwartet. Wenn du das verwechselst, bekommst du eine Bounding Box auf der anderen Seite des Planeten. Kein Fehler, einfach komplett falsche Ergebnisse. Immer explizit destrukturieren:

const [minLat, minLon, maxLat, maxLon] = ngeohash.decode_bbox(hash);

h3-js liefert eigene TypeScript-Type-Definitionen mit; ngeohash erfordert eine separate Installation von @types/ngeohash.

4 Produktionsfallen

H3 Cell IDs werden durch JSON-Serialisierung stillschweigend abgeschnitten

Dieses Problem hat uns am härtesten getroffen. H3 Cell IDs sind 64-Bit-Integer, die JavaScripts sicheren Integer-Bereich (2^53 − 1) überschreiten, weshalb h3-js sie als Hex-Strings zurückgibt: "8928308280fffff".

Das Problem zeigt sich auf der API-Ebene. Wenn irgendein Teil deines Backends H3 IDs als JSON-Zahlen statt als Strings schreibt, schneidet JSON.parse den Wert stillschweigend ab — du bekommst eine gültige, aber völlig andere Zahl, keinen Fehler, und Abfragen liefern leere Ergebnisse. Wir haben eine komplette Debugging-Session damit verbracht, dass Daten einfach nicht zwischen Services zusammenpassten, bevor wir es auf die Serialisierung zurückführen konnten.

Die Lösung ist einfach, muss aber konsequent durchgezogen werden: String-Spalten in der Datenbank, String-Felder in deinem API-Schema, String-Typen in TypeScript-Interfaces. Keine Ausnahmen. Geh nicht davon aus, dass „der Wert dieser ID gar nicht so groß ist."

ngeohash Integer-Encoding auf 52 Bits begrenzt

encode_int und decode_int verwenden Integer statt Base32-Strings, sind aber auf 52 Bits begrenzt (das IEEE 754 float64 Safe-Integer-Limit). Wenn du bitDepth über 52 setzt, geht Präzision verloren, ohne dass ein Fehler geworfen wird.

Redis verwendet intern ebenfalls 52-Bit-Geohash-Integer, insofern passt das zum häufigsten Backend. Aber wenn du Logik von einer Python- oder Java-Bibliothek portierst, die 64-Bit-Geohash-Integer verwendet, stimmen die Werte nicht überein, und es ist schwer zu debuggen, weil die Ergebnisse „fast richtig aussehen" — nur die letzten paar Bits unterscheiden sich.

ngeohash und andere Geohash-Bibliotheken stimmen nicht immer überein

Verschiedene Geohash-Implementierungen können für dieselbe Koordinate unterschiedliche Ergebnisse liefern. Die Ursache ist Gleitkomma-Rundung: Wenn eine Koordinate genau auf der Grenze zwischen zwei Zellen liegt, können verschiedene Bibliotheken auf unterschiedliche Nachbarn runden.

Wir sind darauf gestoßen, als wir KunYus Geohash-Tool gebaut haben — die Frontend-Ausgabe von ngeohash und die Backend-Python-geohash-Bibliothek waren gelegentlich nicht einig. Das passiert selten, ist aber real. Für GPS-Koordinaten (~3 Meter Genauigkeit), die in Zellen von mehreren hundert Metern kodiert werden, ist die praktische Auswirkung minimal. Aber wenn Node.js und Python/Redis exakt übereinstimmen müssen, schreib Integrationstests mit Koordinaten auf bekannten Zellgrenzen. Redis' GEOHASH-Befehl ist die sicherste Referenzimplementierung für systemübergreifende Konsistenz.

h3-js v3 → v4: Jede Funktion wurde umbenannt

Wenn dein Projekt noch auf v3 läuft, bedeutet ein Upgrade auf v4, dass sich fast jeder Funktionsname ändert:

v3 v4
geoToH3 latLngToCell
h3ToGeo cellToLatLng
kRing gridDisk
hexRing gridRingUnsafe
compact compactCells

v4 hat auch die Fehlerbehandlung geändert — ungültige Eingaben werfen jetzt eine Exception statt null zurückzugeben. Es gibt eine Kompatibilitätsschicht unter h3-js/legacy, aber das Fehlerverhalten folgt der v4-Semantik. Eine einmalige Find-and-Replace-Migration ist sauberer als eine langfristige Abhängigkeit vom Legacy-Wrapper. Die vollständige Umbenennungstabelle findest du im v3→v4 Migrationsguide.

GeoHash-Konverter

Konvertieren Sie zwischen GeoHash, H3 und Plus Codes.

Try it now

Entscheidungstabelle

Szenario Wahl Kurzgrund
Redis GEO / Elasticsearch ngeohash Redis verwendet intern Geohash — passendes Verhalten
Frontend-Proximity-Suche ngeohash 3,8 KB vs 86 KB
Hexagonale Heatmaps / Deck.gl h3-js Deck.gl unterstützt nativ H3HexagonLayer
Globale Aggregation (konsistente Zellfläche) h3-js H3-Zellfläche variiert über alle Breitengrade um <3 %
Menschenlesbare IDs in Logs ngeohash "9q8yyk" schlägt "8928308280fffff"
Polygon-Abdeckung / Multi-Resolution h3-js polygonToCells, compactCells, Parent/Child-Hierarchie
Serverseitige Batch-Verarbeitung Beide Beide schnell genug — wähle anhand des Indexsystems, nicht der Bibliothek

S2 in JavaScript: lieber nicht. s2-geometry wurde seit 8 Jahren nicht aktualisiert, nodes2ts ist umfangreicher, hat aber eine kleine Community. Wenn dein Projekt S2 braucht, ist der pragmatische Ansatz ein Python/Go-Microservice mit der offiziellen Bibliothek, den du aus deiner Node.js-App heraus aufrufst.

Um Geohash- und H3-Encoding im Browser auszuprobieren, nutze KunYus GeoHash & H3 Converter-Tool — Koordinaten einfügen, die Cell ID sehen und Grenzen auf einer Karte visualisieren.

GeoHash-Konverter

Konvertieren Sie zwischen GeoHash, H3 und Plus Codes.

Try it now

FAQ

Verwendet h3-js WebAssembly?

Nein. Obwohl h3-js aus C kompiliert wird, nutzt es Emscripten, um reines JavaScript statt .wasm zu erzeugen. Der Vorteil: Keine separate wasm-Datei zum Laden, funktioniert in allen modernen Browsern. Der Nachteil: Bundle-Größe (86 KB gzipped) und Call-Overhead sind beide höher als bei nativem wasm. Die Community hat einen wasm-Build diskutiert, aber es gibt keinen offiziellen Plan.

Hat ngeohash ESM-Exports?

Keine nativen ESM-Exports — das Paket ist rein CommonJS. Frontend-Bundler (Vite, webpack) übernehmen die CJS → ESM-Konvertierung automatisch, sodass import ngeohash from "ngeohash" in gebündelten Projekten problemlos funktioniert. Aber wenn du ein reines ESM-Node.js-Projekt mit "type": "module" betreibst, brauchst du createRequire oder einen Community-Fork. Die Bibliothek hat über 100K wöchentliche npm-Downloads, wurde aber seit 2020 nicht mehr veröffentlicht.

Kann ich beide Bibliotheken im selben Projekt verwenden?

Ja, keine Konflikte. Ein gängiges Muster: Das Backend nutzt H3 für räumliche Aggregation und Analysen, das Frontend nutzt ngeohash für leichtgewichtige Geofence-Prüfungen, und beide tauschen ihre jeweiligen Cell IDs über die API aus. Die beiden Indexsysteme laufen unabhängig voneinander — keine Konvertierung zwischen ihnen nötig.