Tekko

Language

Get in Touch →

Usually respond within 24 hours

Back to BlogArchitecture

Client-Side Analytics: DuckDB-Wasm, Arrow, and Parquet

7 min read
DuckDBWebAssemblyApache ArrowPerformanceData Engineering
Client-Side Analytics: DuckDB-Wasm, Arrow, and Parquet

For years, the standard architecture for analytical dashboards followed a predictable pattern: a frontend sends a request, a backend translates it into SQL, a data warehouse executes the query, and the results are serialized back to the client. While this works, it introduces significant latency, high compute costs for the provider, and potential privacy concerns as data travels across the wire.

The emergence of WebAssembly (Wasm) has fundamentally shifted this paradigm. We can now move the analytical engine itself into the browser. By combining DuckDB-Wasm, Apache Arrow, and Parquet, we can build client-side applications capable of querying multi-gigabyte datasets with sub-second response times, all without a dedicated backend database.

The Architectural Shift: Moving Compute to the Edge

Traditional client-side data handling usually involves fetching JSON blobs and iterating over them with JavaScript. This approach falls apart once you hit a few thousand rows. JavaScript’s garbage collector struggles, and the memory overhead of objects is massive.

To handle millions of rows in the browser, we need three things:

  1. Efficient Storage: A format that doesn't require loading the whole file into memory (Parquet).
  2. Efficient Transport: A memory format that avoids serialization overhead (Apache Arrow).
  3. Efficient Execution: A database engine that utilizes modern CPU features like SIMD (DuckDB-Wasm).

Why Parquet and Arrow are the Perfect Pair

To understand why this stack is so performant, we have to look at how data is structured.

Parquet: The Storage Layer

Parquet is a columnar storage format. Unlike CSV or JSON, which store data row-by-row, Parquet stores it column-by-column. If you have a table with 50 columns but your query only needs two, DuckDB-Wasm can use HTTP Range requests to fetch only the specific bytes representing those two columns. This drastically reduces the amount of data transferred over the network.

Apache Arrow: The Memory Layer

If Parquet is how data lives on disk, Arrow is how it lives in memory. Arrow defines a standardized, language-independent columnar memory layout. The magic happens when DuckDB-Wasm processes data: it produces Arrow-formatted results. Because modern browsers and JavaScript libraries (like Arquero or even basic d3-array) can interface with Arrow, we achieve "zero-copy" or near-zero-copy data transfer. We aren't wasting CPU cycles transforming strings into numbers or vice-versa.

Implementing DuckDB-Wasm

Setting up DuckDB-Wasm requires a bit more than a simple <script> tag because it relies heavily on Web Workers and WebAssembly.

Initialization

First, you need to instantiate the database. It is highly recommended to run DuckDB in a Web Worker to ensure the main UI thread remains responsive during heavy computations.

import * as duckdb from '@duckdb/duckdb-wasm'; const JSDELIVR_BUNDLES = duckdb.getJsDelivrBundles(); const bundle = await duckdb.selectBundle(JSDELIVR_BUNDLES); const worker = new Worker(bundle.mainWorker); const logger = new duckdb.ConsoleLogger(); const db = new duckdb.AsyncDuckDB(logger, worker); await db.instantiate(bundle.mainModule, bundle.pthreadWorker);

Querying Remote Parquet Files

One of the most powerful features of DuckDB-Wasm is its ability to query remote files via HTTP. You don't need to download the 2GB Parquet file to the user's machine. You simply register the URL.

const conn = await db.connect(); // Register a remote Parquet file await db.registerFileURL( 'sales_data.parquet', 'https://example-bucket.s3.amazonaws.com/large_dataset.parquet', duckdb.DuckDBDataProtocol.HTTP, false ); // Query it directly const res = await conn.query(` SELECT region, SUM(sales) as total_sales FROM 'sales_data.parquet' GROUP BY region ORDER BY total_sales DESC `); console.log(res.toArray()); await conn.close();

In this example, DuckDB-Wasm sends specific Range requests to S3, fetching only the metadata (footer) to understand the file structure, and then only the columns required for the SUM and GROUP BY operations.

Performance Optimization Techniques

As a senior engineer, you know that simply using a fast tool isn't enough; you have to use it correctly. Here are the key levers for optimizing client-side analytics.

1. HTTP Range Requests and CORS

For the remote querying to work, your storage provider (S3, GCS, Azure) must support HTTP Range requests and have CORS configured to allow the Range header. Without this, DuckDB-Wasm might fall back to downloading the entire file, defeating the purpose of using Parquet.

2. Schema Projection and Filter Pushdown

DuckDB is an analytical (OLAP) engine. It excels at "wide" tables where you only need a few columns. Ensure your SQL queries are explicit. Avoid SELECT *. By selecting only the columns you need, you minimize the I/O bottleneck.

3. Data Partitioning

If your dataset is massive (e.g., 50GB+), even Parquet's columnar nature might not be enough for a smooth browser experience. Consider partitioning your Parquet files by a common filter key (like year or customer_id). The client can then register only the relevant partition, reducing the metadata overhead.

Handling Memory Constraints

Browsers impose limits on how much memory a Wasm instance can allocate (typically 2GB to 4GB depending on the browser and hardware). If you are working with extremely large datasets, keep these strategies in mind:

  • Streaming Results: Use conn.send() instead of conn.query() to receive results as a stream of Arrow record batches rather than one giant materialized table.
  • Persistence: DuckDB-Wasm can use IndexedDB to persist data locally. If a user frequently queries the same dataset, you can store it in the browser's persistent storage to avoid repeated network fetches.

The User Experience: Latency vs. Feedback

While DuckDB-Wasm is fast, "fast" is relative. Querying 10 million rows might take 200ms of compute time but 800ms of network latency for range requests.

  • Optimistic UI: Show the structure of the dashboard immediately.
  • Progressive Loading: Since DuckDB returns Arrow batches, you can start rendering the first 100 rows of a table while the engine is still calculating the aggregate for the remaining million rows.
  • Worker Offloading: Always perform the query in a worker. If you run it on the main thread, the browser's "spinner" will freeze, creating a jarring experience for the user.

Real-World Use Case: Privacy-Preserving Analytics

Consider a healthcare application where researchers need to analyze patient outcomes. Moving sensitive data to a central server for analysis creates a massive security surface area.

By using DuckDB-Wasm, you can send an encrypted Parquet file to the researcher's browser. The decryption and the query happen entirely in the client's local memory. The raw data never touches your analytics server in an unencrypted state, and you don't have to manage a complex, high-compliance database backend for ad-hoc queries.

When NOT to Use This Stack

Despite its power, client-side analytics isn't a silver bullet. Avoid this approach if:

  • The data changes constantly: DuckDB-Wasm is optimized for read-heavy analytical workloads. If you have a high-frequency write stream, a traditional database is better.
  • Low-end mobile devices: While Wasm is performant, it still requires CPU and RAM. A budget smartphone might struggle with complex joins on large datasets that a laptop would handle with ease.
  • Proprietary Logic: If your query logic contains intellectual property you don't want exposed, remember that any SQL sent to DuckDB-Wasm is visible in the client's source code/network tab.

Conclusion

Combining DuckDB-Wasm, Apache Arrow, and Parquet represents a significant milestone in web architecture. It allows us to build "thick" analytical clients that are faster, cheaper to run, and more private than their server-side counterparts.

To get started, evaluate your current data egress costs and dashboard latency. If you find yourself fetching large JSON payloads or waiting on slow SQL middle-layers, it's time to experiment with moving that compute to the browser. Start by converting a single slow report to a Parquet file, serve it from a CDN, and use DuckDB-Wasm to query it. The performance gains will likely speak for themselves.