KunYu
h3-jsngeohashjavascriptspatial-indexnpm

h3-js vs ngeohash: Escolher a Biblioteca JavaScript de Índice Espacial Certa

Tamanho do bundle, benchmarks de codificação, comparação de API e problemas em produção para h3-js e ngeohash — com medições reais.

KunYu TeamMarch 25, 202610 min de leitura

Se ainda não decidiste entre hexágonos H3 e retângulos Geohash, lê primeiro a nossa comparação H3 vs Geohash vs S2. Este artigo trata apenas do passo seguinte: qual pacote npm instalar.

h3-js e ngeohash são as bibliotecas JavaScript mais descarregadas para H3 e Geohash, respetivamente. Usamos ambas na ferramenta de conversão GeoHash & H3 da KunYu. O tamanho do bundle difere por 22x, a velocidade de codificação por 4–6x, e os problemas em produção são completamente diferentes. Abaixo estão os números medidos e os problemas que só encontrámos depois de colocar em produção.

Tamanho do Bundle: 86 KB vs 3,8 KB

Medido por gzip dos pontos de entrada do módulo ES diretamente a partir de node_modules:

Métrica h3-js v4.4.0 ngeohash v0.6.3
Módulo ES (gzipped) 86 KB 3,8 KB
Tamanho total do pacote 6,5 MB 52 KB
Dependências de runtime 0 0
Implementação C → Emscripten JavaScript puro

A diferença resume-se à implementação. O h3-js é a biblioteca C da Uber compilada através do Emscripten: projeção icosaédrica, travessia de grelha hexagonal, recorte de polígonos, tudo incorporado num único ficheiro JS, sem possibilidade de tree-shaking. Importar apenas { latLngToCell } envia na mesma os 464 KB completos. O ngeohash são 6 ficheiros JS escritos à mão — a biblioteca inteira é mais pequena do que as definições de tipos do h3-js.

86 KB gzipped é aproximadamente o tamanho do React DOM. Não faz diferença no servidor, mas para um projeto frontend que só precisa de uma chamada de codificação, é outro React DOM que estás a enviar para uma única função.

Velocidade de Codificação: ngeohash 4–6x Mais Rápido

Node.js v24, 100.000 iterações por operação com 1.000 iterações de aquecimento:

Operação h3-js ngeohash Diferença
Codificar (lat/lng → célula) 850K ops/seg 4,86M ops/seg ngeohash 5,7x
Descodificar (célula → lat/lng) 1,73M ops/seg 7,58M ops/seg ngeohash 4,4x
Vizinhos (células adjacentes) 614K ops/seg 465K ops/seg h3-js 1,3x
Fronteira / BBox 550K ops/seg 9,29M ops/seg ngeohash 17x

O script de benchmark (gravar como .mjs e executar com node):

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));

Para codificação/descodificação simples, o ngeohash domina — as operações de intercalação de bits em JavaScript puro encaixam perfeitamente no sweet spot do JIT do V8, sem overhead de FFI. O h3-js paga custo de marshaling em cada chamada: os argumentos entram no heap do Emscripten, a função C executa, os resultados são copiados de volta para JavaScript.

Mas o h3-js ganha na pesquisa de vizinhos. O gridDisk percorre a grelha icosaédrica num ciclo C compacto, e a eficiência do algoritmo compilado supera o custo de serialização.

A linha da fronteira não é uma comparação justa: cellToBoundary calcula 6 coordenadas de vértice para um hexágono, decode_bbox apenas devolve 4 números. Mas são estas as operações que efetivamente chamarias em produção.

A menos que estejas a processar milhões de pontos por pedido, a velocidade de codificação não vai decidir qual biblioteca usar. O h3-js codifica uma coordenada em 1,2 microssegundos — rápido o suficiente.

Comparação de API

Codificar e Descodificar

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);

Vizinhos

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);

Fronteira da Célula

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 devolve [minLat, minLon, maxLat, maxLon] — sul, oeste, norte, este. Não é a ordem [west, south, east, north] usada pelo GeoJSON, nem a ordem que o LatLngBounds do Leaflet espera. Se te enganares, obténs um bounding box do outro lado do planeta, sem qualquer erro visível. Desestrutura sempre explicitamente:

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

O h3-js inclui as suas próprias definições de tipos TypeScript; o ngeohash requer a instalação separada de @types/ngeohash.

Problemas em Produção

IDs de Célula H3 Silenciosamente Truncados pela Serialização JSON

Este foi o que mais nos apanhou. Os IDs de célula H3 são inteiros de 64 bits, excedendo o intervalo de inteiros seguros do JavaScript (2^53 − 1), por isso o h3-js devolve-os como strings hexadecimais: "8928308280fffff".

O problema manifesta-se na camada da API. Se qualquer parte do teu backend escrever IDs H3 como números JSON em vez de strings, JSON.parse trunca silenciosamente o valor — obtens um número válido mas completamente diferente, sem erro, e as consultas devolvem resultados vazios. Passámos uma sessão de depuração inteira com dados que simplesmente não correspondiam entre serviços até rastrearmos o problema até à serialização.

A correção é simples mas tem de ser rigorosa: colunas de string na base de dados, campos de string no esquema da API, tipos string nas interfaces TypeScript. Sem exceções. Não assumir que "o valor deste ID não é assim tão grande."

Codificação Inteira do ngeohash Limitada a 52 Bits

encode_int e decode_int usam inteiros em vez de strings Base32, mas estão limitados a 52 bits (o limite de inteiro seguro float64 IEEE 754). Se passares bitDepth acima de 52, a precisão perde-se silenciosamente — sem erro lançado.

O Redis também usa internamente inteiros Geohash de 52 bits, por isso isto está alinhado com o backend mais comum. Mas se estiveres a portar lógica de uma biblioteca Python ou Java que usa inteiros Geohash de 64 bits, os valores não vão corresponder. É difícil de depurar porque os resultados "parecem próximos", apenas os últimos bits diferem.

ngeohash e Outras Bibliotecas Geohash Nem Sempre Concordam

Diferentes implementações de Geohash podem produzir resultados diferentes para a mesma coordenada. A causa é o arredondamento de vírgula flutuante: quando uma coordenada cai exatamente na fronteira entre duas células, diferentes bibliotecas podem arredondar para vizinhos diferentes.

Encontrámos este problema ao construir a ferramenta Geohash da KunYu — a saída do ngeohash no frontend e a biblioteca Python geohash no backend ocasionalmente discordavam. É raro, mas real. Para coordenadas GPS (~3 metros de precisão) codificadas em células que abrangem centenas de metros, o impacto prático é mínimo. Mas se precisas que Node.js e Python/Redis concordem exatamente, escreve testes de integração usando coordenadas em fronteiras de células conhecidas. O comando GEOHASH do Redis é a implementação de referência mais segura para consistência entre sistemas.

h3-js v3 → v4: Todas as Funções Foram Renomeadas

Se o teu projeto ainda está na v3, atualizar para a v4 significa que praticamente todos os nomes de função mudam:

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

A v4 também alterou o tratamento de erros — entrada inválida lança exceção em vez de devolver null. Existe uma camada de compatibilidade em h3-js/legacy, mas o comportamento de erros segue a semântica da v4. Uma migração única com find-and-replace é mais limpa do que depender do wrapper legacy a longo prazo. Tabela completa de renomeações no guia de migração v3→v4.

Conversor GeoHash

Converta entre GeoHash, H3 e Plus Codes.

Try it now

Qual Usar

Cenário Escolha Motivo em uma linha
Redis GEO / Elasticsearch ngeohash O Redis usa Geohash internamente — comportamento correspondente
Pesquisa de proximidade no frontend ngeohash 3,8 KB vs 86 KB
Mapas de calor hexagonais / Deck.gl h3-js O Deck.gl suporta nativamente H3HexagonLayer
Agregação global (área de célula consistente) h3-js A área de célula H3 varia <3% entre latitudes
IDs legíveis por humanos em logs ngeohash "9q8yyk" ganha a "8928308280fffff"
Cobertura de polígonos / multi-resolução h3-js polygonToCells, compactCells, hierarquia pai/filho
Processamento em lote no servidor Qualquer um Ambos suficientemente rápidos — escolhe com base no sistema de índice, não na biblioteca

O S2 no ecossistema JavaScript está em mau estado: s2-geometry não é atualizado há 8 anos, nodes2ts é mais completo mas tem uma comunidade pequena. Se o teu projeto precisa de S2, a abordagem prática é um microserviço Python/Go a executar a biblioteca oficial, chamado a partir da tua aplicação Node.js.

Para experimentar a codificação Geohash e H3 no browser, usa a ferramenta de conversão GeoHash & H3 da KunYu — cola coordenadas, vê o ID da célula e visualiza fronteiras num mapa.

Conversor GeoHash

Converta entre GeoHash, H3 e Plus Codes.

Try it now

FAQ

O h3-js usa WebAssembly?

Não. Apesar de ser compilado a partir de C, o h3-js usa Emscripten para produzir JavaScript simples em vez de .wasm. A vantagem: não há ficheiro wasm separado para carregar, funciona em todos os browsers modernos. A desvantagem: o tamanho do bundle (86 KB gzipped) e o overhead de chamada são ambos superiores ao que wasm nativo seria. A comunidade tem discutido uma build wasm, mas não há plano oficial.

O ngeohash tem exports ESM?

Não tem exports ESM nativos — o pacote é apenas CommonJS. Os bundlers de frontend (Vite, webpack) tratam da conversão CJS → ESM automaticamente, por isso import ngeohash from "ngeohash" funciona sem problemas em projetos com bundle. Mas se estiveres a executar um projeto Node.js puramente ESM com "type": "module", vais precisar de createRequire ou de um fork da comunidade. A biblioteca tem mais de 100K downloads semanais no npm, mas não é publicada desde 2020.

Posso usar ambas as bibliotecas no mesmo projeto?

Sim, sem conflitos. Um padrão comum: o backend usa H3 para agregação espacial e analytics, o frontend usa ngeohash para verificações de geofence leves, e trocam os respetivos IDs de célula através da API. Os dois sistemas de índice funcionam de forma independente — não é necessária conversão entre eles.