KunYu
h3-jsngeohashjavascriptspatial-indexnpm

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

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

KunYu TeamMarch 25, 202610 min de leitura

Se você ainda não decidiu entre hexágonos H3 e retângulos Geohash, leia primeiro nossa comparação H3 vs Geohash vs S2. Este artigo é apenas sobre o próximo passo: qual pacote npm instalar.

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

Tamanho do Bundle: 86 KB vs 3.8 KB

Medido compactando com gzip os entry points do ES module diretamente de node_modules:

Métrica h3-js v4.4.0 ngeohash v0.6.3
ES module (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 Pure JavaScript

A diferença se resume à implementação. h3-js é a biblioteca C do Uber compilada via Emscripten — projeção icosaédrica, travessia de grade hexagonal, clipping de polígonos, tudo embutido num único arquivo JS, sem possibilidade de tree-shaking. Importar apenas { latLngToCell } ainda envia os 464 KB completos. ngeohash são 6 arquivos JS escritos à mão, e a biblioteca inteira é menor que as definições de tipo 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 precisa de apenas uma chamada de encode, é outro React DOM que você está enviando para uma única função.

Velocidade de Codificação

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

Operação h3-js ngeohash Diferença
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
Vizinhos (células adjacentes) 614K ops/sec 465K ops/sec h3-js 1.3x
Boundary / BBox 550K ops/sec 9.29M ops/sec ngeohash 17x

O script de benchmark (salve como .mjs e execute 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 encode/decode simples, ngeohash domina — operações puras de bit-interleaving em JS estão exatamente no ponto forte do JIT do V8, sem overhead de FFI. h3-js paga custo de marshaling em cada chamada: os argumentos vão para o heap do Emscripten, a função C executa, os resultados são copiados de volta para o JavaScript.

Mas h3-js se destaca na busca de vizinhos. gridDisk percorre a grade icosaédrica num loop C compacto, e a eficiência do algoritmo compilado supera o custo de serialização.

A linha de boundary não é uma comparação justa: cellToBoundary calcula 6 coordenadas de vértice para um hexágono, decode_bbox apenas retorna 4 números. Mas essas são as operações que você realmente chamaria em produção.

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

Comparação de API

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

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

Boundary 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 retorna [minLat, minLon, maxLat, maxLon] — sul, oeste, norte, leste. Não é a ordem [west, south, east, north] usada pelo GeoJSON, nem a ordem que o LatLngBounds do Leaflet espera. Erre isso e você terá uma bounding box do outro lado do planeta, sem nenhum erro visível, apenas resultados completamente errados. Sempre desestruture explicitamente:

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

h3-js já vem com suas próprias definições de tipo TypeScript; ngeohash requer a instalação separada de @types/ngeohash.

Armadilhas em Produção

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

Essa foi a que mais nos pegou. IDs de células H3 são inteiros de 64 bits, excedendo o limite de inteiros seguros do JavaScript (2^53 − 1), então h3-js os retorna como strings hexadecimais: "8928308280fffff".

O problema aparece na camada de API. Se qualquer parte do seu backend escreve IDs H3 como números JSON em vez de strings, JSON.parse trunca silenciosamente o valor — você obtém um número válido mas completamente diferente, sem erro, e as consultas retornam resultados vazios. Passamos uma sessão inteira de debugging com dados que simplesmente não batiam entre serviços antes de rastrear o problema até a serialização.

A correção é simples mas precisa ser completa: colunas de string no banco de dados, campos de string no schema da sua API, tipos string nas interfaces TypeScript. Sem exceções. Não assuma "o valor desse ID não é 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 são limitados a 52 bits (o limite de inteiro seguro do float64 IEEE 754). Passe bitDepth acima de 52 e a precisão é silenciosamente perdida — nenhum erro é lançado.

O Redis também usa internamente inteiros Geohash de 52 bits, então isso se alinha com o backend mais comum. Mas se você está portando lógica de uma biblioteca Python ou Java que usa inteiros Geohash de 64 bits, os valores não vão bater. É difícil de debugar porque os resultados "parecem próximos", com diferença apenas nos últimos bits.

ngeohash e Outras Bibliotecas Geohash Nem Sempre Concordam

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

Encontramos isso ao construir a ferramenta Geohash do 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 abrangendo centenas de metros, o impacto prático é mínimo. Mas se você precisa que Node.js e Python/Redis concordem exatamente, escreva 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 seu projeto ainda está na v3, atualizar para a v4 significa que quase todos os nomes de função mudam:

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

A v4 também mudou o tratamento de erros — entrada inválida lança exceção em vez de retornar 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 busca e substituição é mais limpo do que depender do wrapper legado 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 Redis usa Geohash internamente — comportamento compatível
Busca por proximidade no frontend ngeohash 3.8 KB vs 86 KB
Heatmaps hexagonais / Deck.gl h3-js Deck.gl suporta nativamente H3HexagonLayer
Agregação global (área de célula consistente) h3-js A área das células H3 varia <3% entre latitudes
IDs legíveis em logs ngeohash "9q8yyk" ganha de "8928308280fffff"
Cobertura de polígonos / multi-resolução h3-js polygonToCells, compactCells, hierarquia pai/filho
Processamento em lote no servidor Qualquer um Ambos rápidos o suficiente — escolha pelo sistema de índice, não pela biblioteca

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

Para experimentar a codificação Geohash e H3 no navegador, use a ferramenta de conversão GeoHash & H3 do KunYu — cole coordenadas, veja o ID da célula e visualize os limites no 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, h3-js usa Emscripten para produzir JavaScript puro em vez de .wasm. A vantagem: nenhum arquivo wasm separado para carregar, funciona em todos os navegadores modernos. A desvantagem: o tamanho do bundle (86 KB gzipped) e o overhead de chamada são ambos maiores do que wasm nativo seria. A comunidade discutiu um build wasm, mas não há plano oficial.

O ngeohash tem exports ESM?

Não tem exports ESM nativos — o pacote é apenas CommonJS. Bundlers de frontend (Vite, webpack) fazem a conversão CJS → ESM automaticamente, então import ngeohash from "ngeohash" funciona normalmente em projetos com bundle. Mas se você está rodando um projeto Node.js puramente ESM com "type": "module", vai precisar de createRequire ou de um fork da comunidade. A biblioteca tem mais de 100K downloads semanais no npm mas não recebe uma publicação desde 2020.

Posso usar ambas as bibliotecas no mesmo projeto?

Sim, sem conflitos. Um padrão comum: o backend usa H3 para agregação e análise espacial, o frontend usa ngeohash para verificações leves de geofence, e eles trocam seus respectivos IDs de célula pela API. Os dois sistemas de índice funcionam independentemente — nenhuma conversão entre eles é necessária.