HypStack github.com/hyparam/hyparquet

HypStack / hyparquet

hyparquet artwork from the project repository.

hyparquet

Pure-JavaScript Apache Parquet parser·v1.26·MIT

Your data is already sitting in object storage as Parquet, but reading it from JavaScript usually means standing up a query service or pulling in a native library that only runs on the server. hyparquet removes that step: it parses Parquet’s columnar layout in pure JS and fetches only the row groups and columns you ask for over HTTP range requests, so a page can read a multi-gigabyte file in S3 by downloading a few megabytes and never touching a backend.

It carries no dependencies and handles the whole format, every type, encoding, and compression codec, which is the part most lightweight readers skip and the part that decides whether a real file actually opens. The same code runs unchanged in the browser and in Node, so the reader you write against a local file is the reader you ship against a bucket.

$ npm install hyparquet

// read a local file in Node
import { asyncBufferFromFile, parquetReadObjects } from 'hyparquet'

const file = await asyncBufferFromFile('example.parquet')
const rows = await parquetReadObjects({ file })

1 What it does

hyparquet reads Apache Parquet files in JavaScript, with no native code and no dependencies. Parquet stores data by column and in row groups, with a footer describing the layout, so a reader that understands the format can seek to exactly the bytes it needs rather than scanning the file front to back. hyparquet uses that structure directly: give it a file handle and a range of rows or a list of columns, and it reads the footer, works out which row groups and pages overlap your request, and decodes only those.

Because the file handle is just an object with a byteLength and a slice(start, end) method, the source can be anything. A local file, an ArrayBuffer in memory, or a URL served with HTTP range support. When the source is a URL, those slices become range requests, so querying a file in cloud storage from the browser costs only the bytes the query touches.

2 Quickstart

Read a remote Parquet file straight from the browser, asking for two columns and a slice of rows. Only the matching pages are fetched over range requests:

// from a CDN, no build step
const { asyncBufferFromUrl, parquetReadObjects } =
  await import('https://cdn.jsdelivr.net/npm/hyparquet/src/hyparquet.min.js')

const url = 'https://hyperparam-public.s3.amazonaws.com/bunnies.parquet'
const file = await asyncBufferFromUrl({ url })

const data = await parquetReadObjects({
  file,
  columns: ['Breed Name', 'Lifespan'],
  rowStart: 10,
  rowEnd: 20,
})

For a private file you can pass request headers and a known byte length, which lets hyparquet skip the initial size probe:

const file = await asyncBufferFromUrl({
  url: 'https://s3.hyperparam.app/wiki_en.parquet',
  byteLength: 415958713,
  requestInit: { headers: { Authorization: 'Bearer my_token' } },
})

3 Features

  • Zero dependencies. The published package pulls in nothing else, so it stays small and there is no transitive supply chain to audit.
  • The whole format. Every Parquet physical and logical type, every encoding (plain, dictionary, RLE, delta, byte-stream-split), and every compression codec, so real-world files open instead of erroring on an unsupported page.
  • HTTP range requests. Reads the footer and just the overlapping row groups and pages, so a query against a file in S3 downloads kilobytes or megabytes, not the whole object.
  • Browser and Node. The same ES module runs in both. Use asyncBufferFromFile on Node, asyncBufferFromUrl in the browser, or hand it any object with byteLength and slice.
  • Streaming callbacks. parquetRead takes onChunk and onPage callbacks so you can process column data as it decodes rather than buffering an entire result set.
  • Metadata without the data. parquetMetadataAsync and parquetSchema read the footer alone, so you can inspect schema, row counts, and statistics before deciding what to fetch.
  • Typed. Ships TypeScript definitions for the full API.

4 API

The two entry points differ in how they hand back data. parquetReadObjects resolves to an array of row objects, which is the convenient default. parquetRead is the lower-level call that drives the same work through callbacks for streaming and custom output.

parquetReadObjects(options)

Reads rows and resolves to Record<string, any>[]. Accepts file plus optional columns, rowStart, and rowEnd to narrow what is fetched and decoded.

parquetRead(options)

The core reader. Takes the same file and selection, plus onComplete, onChunk, onPage, and rowFormat to receive data as it decodes. Resolves once reading finishes.

parquetMetadataAsync(file) & parquetSchema(metadata)

Read and interpret the Parquet footer on its own. Use them to inspect the schema, row-group boundaries, and column statistics without downloading any data pages.

asyncBufferFromUrl(options) & asyncBufferFromFile(path)

Build the file handle hyparquet reads from. asyncBufferFromUrl wraps a URL with range requests (and optional requestInit headers and byteLength); asyncBufferFromFile wraps a local file on Node. Either returns an object with byteLength and an async slice, the same shape you can implement yourself for any other source.

5 Benchmarks

Time to first data reading wiki_en.parquet in the browser, the metric that decides whether a page feels instant. hyparquet is a ~10 KB module, so it loads, does a HEAD and two GET range requests, and returns rows in about 155 ms. DuckDB-wasm has to download and instantiate a 2.4 MB WebAssembly extension first, then walks a long sequential chain of growing fetches, so first data lands at about 3.5 s even though it is far more powerful once warm.

Network waterfall reading wiki_en.parquet in the browser. hyparquet returns first data at 155 milliseconds after a HEAD and two GET range requests. DuckDB-wasm downloads a 2.4 MB WebAssembly extension, then a long sequential chain of growing GETs, reaching first data at 3466 milliseconds.
ReaderBundle (gzip)First dataRuntimeDependencies
hyparquet~10 KB~155 mspure JSnone
DuckDB-wasm~8 MB~3.5 sWebAssemblybundled engine

hyparquet does one thing, read the bytes you ask for, and the small footprint is exactly why first rows arrive an order of magnitude sooner. When you need joins, aggregations, and full SQL over those rows, pair it with squirreling, a 13 kb streaming SQL engine that reads through hyparquet over the same range requests, so you add query power without giving up the fast start or pulling in a multi-megabyte engine.

Source on GitHub README & docs npm