Performance and large datasets
Bound the rows, the marks and the work per interaction. Measure the complete report on the devices your users have.
Understand the current execution model
For synchronous datasets, each linked view applies predicates and derives its own data. Worker queries filter and aggregate before returning bounded rows; scatter renderers reduce mark DOM cost. These solve separate parts of the workload. Ordinary table paging still does not turn in-memory sorting into a query.
The worker explorer separates source records, matching records, returned rows and drawn points, and downloads observations with requested/active renderer state. Bounded scatter rows are the first matching IDs, not a representative sample.
Aggregate for the question
- Fetch a bounded time window and only the columns that the report needs.
- Aggregate time series to a useful interval before sending data to the browser.
- Limit category cardinality with a documented top-N rule and an “Other” bucket when appropriate.
- Page raw detail rows on the server when the full result is too large for the browser.
LineChart, AreaChart and BarChart sum valid numeric values at repeated x/series coordinates. This is not a mean or calendar bucketing: separate timestamps stay separate coordinates. Histogram with value="amount" sums a measure into date buckets. Precompute non-additive statistics or use an explicit aggregate encoding in a spec; Studio requires unique line coordinates.
Preserve fields used by shared predicates. If one chart publishes region but a pre-aggregated series has no region field, that series cannot respond correctly to the selection. Filter raw rows before aggregating, or keep the aggregated view independent with selection={false}.
import { LineChart, useLinkedRows, useSourceId } from '@quartile/react';
// date is already a normalized calendar day.
type Order = { date: string; region: string; amount: number };
export function RevenueTrend({ rows }: { rows: Order[] }) {
const source = useSourceId();
const { rows: visible } = useLinkedRows(rows, { source });
const byDay = new Map<string, number>();
for (const row of visible) {
byDay.set(row.date, (byDay.get(row.date) ?? 0) + row.amount);
}
const daily = [...byDay].map(([date, revenue]) => ({ date, revenue }));
// Display-only: filtering was applied before aggregation above.
return <LineChart data={daily} x="date" y="revenue" selection={false}
aria-label="Revenue per day for the selected orders" />;
}Move filtering to the server deliberately
Selection.onChange exposes the current predicates. An application may translate an allowlisted subset into a parameterized query and fetch updated report data. Remote query translation remains an application integration. The optional DuckDB adapter runs local worker queries.
- Debounce network requests, cancel stale ones and keep the last valid report visible while loading.
- Validate field names, operators and values on the server. Apply tenant authorization independently.
- For true crossfilter behavior, compute each view without that view's own predicate. One shared filtered response cannot recover excluded categories.
- Use
selection={false}for displays that already receive fully filtered results, and make selection publishing explicit through application controls.
Profile the interaction
| Measure | Include |
|---|---|
| Initial report | Download size, parsing, schema inference, first chart and first usable table. |
| Selection response | Brush a range, select a category, clear all filters; include every linked view. |
| Rendering cost | Returned rows and drawn marks, active renderer, DOM count, commit, layout, paint opportunity and keyboard tooltip response. |
| Representative conditions | Real category counts, missing values, narrow screens and slower hardware. |
Use a production build and record browser, device, dataset shape and dashboard composition with any timing. A filter microbenchmark alone does not establish a browser rendering limit. Query duration includes queueing, transfer and decoding; a requestAnimationFrame paint opportunity is not GPU completion.
Memoize expensive derived datasets when profiling justifies it and keep rows stable between changes. React documents useMemo as a performance optimization, not a correctness mechanism.