# Performance optimization

Learn how to optimize dcupl for large datasets, complex queries, and high-frequency updates. This guide covers the performance options, indexing, partial updates, batch operations, and profiling.

## Performance overview

dcupl is designed to handle large datasets efficiently. With proper optimization:

- **Initialize once** - `init()` processes all data; afterwards, apply changes with `dcupl.update()`
- **Cached reads** - Each list caches its query results, facets and aggregations until the next update that affects it
- **Partial updates** - `data.update()`, `upsert()` and `remove()` reprocess only the changed attributes and what depends on them
- **Batched applies** - Stage many changes, then apply them with one `dcupl.update()`
- **Efficient memory usage** - With list management

## Performance options

These options go into `new Dcupl({ performance })`:

| Option (default)                      | Effect                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cache.enabled` (`true`)              | Per-list caches for query results, facets and aggregations (see [Caching](/sdk/advanced/caching))                                                                                                                                                                                                                                                                                                                                                                                                     |
| `cache.maxSize` (`64 MiB`)            | Bound, in estimated bytes, for each list's cached results and for the indices dcupl builds per model (or per scoped list) as it answers queries. Least recently used entries are dropped and rebuilt on their next use, so results are unchanged; `Infinity` removes the bound. The index bound also applies with `cache.enabled: false`. Lower it to cap memory in long-running processes                                                                                                            |
| `cache.maxProjectionSize` (`256 MiB`) | Bound, in estimated bytes per list, for the projected copies that deep facets and deep filters build (a dotted path such as `product.color` in a facet attribute, a list's query or an `fn.*` query), one per distinct set of dotted attributes. Least recently used copies are dropped and projected again on their next use; a copy larger than the bound is built and served but not kept, so raise the bound for deep facets on large lists. Results are unchanged; `Infinity` removes the bound. |
| `clone.enabled` (`true`)              | Every read returns a deep copy. `false` returns the stored items, which you must not mutate                                                                                                                                                                                                                                                                                                                                                                                                           |
| `containerBatchSize` (unset)          | How often `init()` and full rebuilds yield to the event loop: unset after every data container (one staged `data.*` call), `0` never, `N` every N containers. Fewer yields finish sooner but block the UI longer                                                                                                                                                                                                                                                                                      |
| `partialUpdates` (`0.5` and `0.5`)    | Two thresholds for the share of changed models and items: above both, `dcupl.update()` runs a full rebuild instead of a partial update (see [When dcupl Falls Back](#when-dcupl-falls-back-to-a-full-update))                                                                                                                                                                                                                                                                                         |
| `retainStagedData` (`true`)           | `false` makes `init()` drop the staged raw rows after a successful build, the same as `dcupl.seal()` (measured 15.6–21.8 % less retained heap after `init()` on a 135,880-row fixture on 2026-10-15, and 2–24 % on customer catalogs of 1,000–44,000 rows on 2026-10-16). For instances that load once and never update: afterwards every data or model write throws `DcuplSealedError`, `init()`/`update()` resolve `false`, and reads are unchanged (see [seal()](/sdk/api/dcupl#seal))             |
| `autoUpdateLists.enabled` (`false`)   | `query`-scoped lists re-run their seed query on every update that affects them                                                                                                                                                                                                                                                                                                                                                                                                                        |

Quality checks also cost time in `init()`: `new Dcupl({ quality: { enabled: false } })` turns them off for all models, and a model's own `quality: { enabled: false }` for that model.

```typescript
const dcupl = new Dcupl({
  performance: {
    clone: { enabled: false }, // your code never mutates returned items
    containerBatchSize: 0, // process synchronously; blocks the UI while init() runs
  },
});
```

The full option types are in the [Dcupl API](/sdk/api/dcupl#dcuplinitperformanceconfig).

## Indexing strategies

### Automatic indexing

dcupl builds the indices it needs for facets, aggregations and grouping on first use, so you don't need to configure anything special for most use cases. References are indexed while data is processed, so `eq` and `in` conditions on a reference are index lookups.

```typescript
const productModel: ModelDefinition = {
  key: 'Product',
  properties: [
    { key: 'category', type: 'string' }, // Filterable, facet index built on first use
    { key: 'brand', type: 'string' }, // Filterable, facet index built on first use
    { key: 'price', type: 'float' }, // Filterable + aggregatable
  ],
};
```

### Manual index property

`index: true` builds an inverse index for a property while data is processed, so `eq` and `in` conditions on it are index lookups instead of a pass over the items. The index costs processing time and memory in `init()` and `update()`, so use it for properties you often filter by exact value (SKUs, IDs, status codes):

```typescript
{
  key: 'sku',
  type: 'string',
  index: true,  // Build an inverse index for eq/in lookups
}
```

## Initialization

### Initialize once, update incrementally

`init()` processes every model and all staged data. Call it once at startup; afterwards stage changes and apply them with `await dcupl.update()`. Calling `init()` again re-processes everything.

```typescript
dcupl.models.set(productModel);
dcupl.data.set(initialProducts, { model: 'Product' });
await dcupl.init(); // Once

// Later: stage and apply changes
dcupl.data.update([{ key: 'p1', price: 150 }], { model: 'Product' });
await dcupl.update();
```

### Load critical data first

To make the app usable sooner, initialize with the data the first screen needs, then load the rest in the background. The second load replaces the model with `data.set()`, so it is a full rebuild:

```typescript
const firstPage = await fetch('/api/products?limit=100').then((r) => r.json());
dcupl.data.set(firstPage, { model: 'Product' });
await dcupl.init();
renderUI();

const allProducts = await fetch('/api/products').then((r) => r.json());
dcupl.data.set(allProducts, { model: 'Product' });
await dcupl.update();
```

## Partial updates

A partial update reprocesses only the changed attributes and the attributes that depend on them, instead of rebuilding every model. All data calls are staged: nothing changes until `await dcupl.update()` applies them.

### Full update (slow)

```typescript
// ❌ Slow: Replaces the model's data and rebuilds everything
dcupl.data.set(updatedData, { model: 'Product' });
await dcupl.update(); // data.set() turns this into a full rebuild
```

### Partial update (fast)

```typescript
// ✅ Fast: Updates only changed items
dcupl.data.update(
  [
    { key: 'p1', price: 999 }, // Only update price
    { key: 'p2', stock: 50 }, // Only update stock
  ],
  { model: 'Product' },
);
await dcupl.update(); // Apply the staged changes
```

### When dcupl falls back to a full update

- A `data.set()` (or `data.reset()`) anywhere in the batch turns the whole update into a full rebuild.
- A model definition changed after `init()`: a [`models.set()`](/sdk/api/models#set) or `models.update()` that registers a definition (even an unchanged one), or a `models.remove()` of a registered model, makes the next `dcupl.update()` rebuild all data like `init()` (the `dcupl_partial_update` event reports `isPartialUpdate: false`). The rebuild runs in the same instance, so lists created earlier serve the new definition, and data staged in the same batch is applied. Only that one update is affected; the next one follows the other rules again.
- `dcupl.update()` with nothing staged runs a full rebuild, so don't call it "just in case".
- If more than half of the models **and** more than half of their items changed, a full rebuild is cheaper and dcupl switches to it. Tune this with `modelRatioThreshold` and `itemRatioThreshold` in `performance.partialUpdates` (both default `0.5`).
- When the data a [query reference](/sdk/api/models#reference) depends on changes — the rows of the model that defines it, its target model, or a model the target reaches through its references — `dcupl.update()` falls back to a full rebuild so the reference is recomputed (the `dcupl_partial_update` event then reports `isPartialUpdate: false`). Other changes stay on the partial path.

### Partial update with specific attributes

Update only specific attributes. Every attribute you send replaces the stored value (arrays and JSON values are replaced, not merged); attributes you leave out keep their value. `data.update()` only changes existing items and ignores unknown keys; use `data.upsert()` to add items.

```typescript
dcupl.data.update([{ key: 'p1', price: 999 }], { model: 'Product' });
await dcupl.update();
```

### Batch partial updates

Update multiple items efficiently: stage all changes, then apply them with a single `dcupl.update()`. Every `dcupl.update()` call has a fixed cost (it checks every affected list and emits events), so one call per batch is far cheaper than one per item.

```typescript title="Update 1000 items"
const updates = [];
for (let i = 0; i < 1000; i++) {
  updates.push({
    key: `p${i}`,
    stock: newStock[i],
  });
}

dcupl.data.update(updates, { model: 'Product' });
await dcupl.update(); // One apply for all 1000 changes
```

### Remove items efficiently

```typescript title="Remove specific items"
dcupl.data.remove([{ key: 'p1' }, { key: 'p2' }, { key: 'p3' }], { model: 'Product' });
await dcupl.update();
```

## Query optimization

### Use indexed properties

Every property is queryable, but only `eq` and `in` conditions on references and `index: true` properties are answered from an index. Other conditions evaluate the list's items.

```typescript
// sku has index: true (see Manual Index Property): an index lookup
productList.catalog.query.addCondition({
  attribute: 'sku',
  operator: 'eq',
  value: 'LPT-001',
});

// Range operators such as gte always evaluate the list's items
productList.catalog.query.addCondition({
  attribute: 'price',
  operator: 'gte',
  value: 100,
});
```

### Put the most selective condition first

In an `and` group, each condition only evaluates the items the previous conditions left. Put the condition that matches the fewest items first, ideally one that is answered from an index:

```typescript
productList.catalog.query.addGroup({
  groupKey: 'filters',
  groupType: 'and',
  queries: [
    { attribute: 'sku', operator: 'eq', value: 'LPT-001' }, // Index lookup, one item
    { attribute: 'inStock', operator: 'eq', value: true }, // Evaluates only what is left
  ],
});
```

### Limit deep queries

Each level of a dotted path resolves one more reference for every evaluated item, so the cost grows with the depth. A list that queries through other models is also invalidated whenever any model on the path changes.

```typescript
// ⚠️ Slower: 3-level deep query
orderList.catalog.query.addCondition({
  attribute: 'customer.address.city',
  operator: 'eq',
  value: 'San Francisco',
});

// ✅ Faster: Use derived properties
// Customer model: derive the city from the address reference
{ key: 'city', type: 'string', derive: { localReference: 'address', remoteProperty: 'city' } }

// Order model: derive it again from the customer reference
{
  key: 'customerCity',
  type: 'string',
  derive: {
    localReference: 'customer',
    remoteProperty: 'city', // The Customer's derived city
  },
}

// Then query directly
orderList.catalog.query.addCondition({
  attribute: 'customerCity',
  operator: 'eq',
  value: 'San Francisco',
});
```

### Optimize query groups

Every condition in an `or` group is evaluated against all items the group receives, and the results are merged. For several values of one attribute, use one `in` condition instead: it is answered from the index when there is one, and with a single pass over the items when there isn't.

```typescript
// ⚠️ Slower: one evaluation per value
productList.catalog.query.addGroup({
  groupKey: 'category',
  groupType: 'or',
  queries: [
    { attribute: 'category', operator: 'eq', value: 'Electronics' },
    { attribute: 'category', operator: 'eq', value: 'Computers' },
    { attribute: 'category', operator: 'eq', value: 'Phones' },
  ],
});

// ✅ Faster: one condition, same result
productList.catalog.query.addCondition({
  attribute: 'category',
  operator: 'in',
  value: ['Electronics', 'Computers', 'Phones'],
});
```

### Use pagination

For large result sets, request a page instead of the whole result. Pass paging as a **read option**:
`start` (0-based index) and `count` (maximum items) are passed to the read itself,
so the list holds the full filtered result and each call slices it.

```typescript
const PAGE_SIZE = 50;
const productList = dcupl.lists.create({ modelKey: 'Product' });

// First page
const page1 = productList.catalog.query.items({ start: 0, count: PAGE_SIZE });

// Any page — no cursor to advance, just compute the offset
function getPage(page: number) {
  return productList.catalog.query.items({
    start: page * PAGE_SIZE,
    count: PAGE_SIZE,
  });
}

// Total number of matching items, independent of paging
const total = productList.catalog.query.count();
const pageCount = Math.ceil(total / PAGE_SIZE);
```

Because the page is resolved at read time, filters and paging compose without re-creating the
list — apply a query, then read page 0 again:

```typescript
productList.catalog.query.apply({
  attribute: 'category',
  operator: 'eq',
  value: 'Electronics',
});

const firstPage = productList.catalog.query.items({ start: 0, count: PAGE_SIZE });
const filteredTotal = productList.catalog.query.count();
```

> `count` means two different things here: as an **option** it is the page size, as a **method**
> (`query.count()`) it is the number of matching items. The same `start`/`count` options work on
> `query.execute()`, which takes the same `DcuplListQueryOptions`. They do **not** apply to
> `query.many()`, which selects by `itemKeys` and accepts only `projection`. If you store paging
> in the list's query with `query.applyOptions({ start, count })` instead, `items()` and `count()`
> both return only that page while those options are applied (`query.clear()` removes them).

### Filter in dcupl, not on the result

Let dcupl filter instead of filtering the returned array: `items()` followed by `Array.filter()` copies every item first (with cloning on), skips the indexes, and the filtered result isn't cached.

```typescript
// ✅ Indexed where possible, cached per query
productList.catalog.query.addCondition({ attribute: 'category', operator: 'eq', value: 'Electronics' });
const electronics = productList.catalog.query.items();

// ❌ Copies every product, then filters in JavaScript
const filtered = productList.catalog.query.items().filter((p) => p.category === 'Electronics');
```

## List management

### Reuse lists

Every `lists.create()` registers a list that stays, with its caches, until you destroy it, and `dcupl.update()` checks every registered list. A list created per call therefore piles up.

```typescript
// ❌ Bad: Create new list for each query
function getProducts(category) {
  const list = dcupl.lists.create({ modelKey: 'Product' });
  list.catalog.query.addCondition({
    attribute: 'category',
    operator: 'eq',
    value: category,
  });
  return list.catalog.query.items();
}

// ✅ Good: Reuse list
const productList = dcupl.lists.create({ modelKey: 'Product' });

function getProducts(category) {
  productList.catalog.query.clear();
  productList.catalog.query.addCondition({
    attribute: 'category',
    operator: 'eq',
    value: category,
  });
  return productList.catalog.query.items();
}
```

### Destroy lists

Destroy lists when no longer needed:

```typescript
const tempList = dcupl.lists.create({ modelKey: 'Product' });
// ... use list

// Clean up
tempList.destroy();
```

### List pooling

For frequently created/destroyed lists:

```typescript
class ListPool {
  private pool: Map<string, DcuplList[]> = new Map();

  get(modelKey: string): DcuplList {
    const pool = this.pool.get(modelKey) || [];
    return pool.pop() || dcupl.lists.create({ modelKey });
  }

  release(list: DcuplList) {
    list.catalog.query.clear();
    const modelKey = list.modelKey;
    const pool = this.pool.get(modelKey) || [];
    pool.push(list);
    this.pool.set(modelKey, pool);
  }
}

const pool = new ListPool();
const list = pool.get('Product');
// ... use list
pool.release(list);
```

## Memory optimization

### Limit data loading

Load only needed data:

```typescript
// ❌ Bad: Load all data
const allProducts = await fetch('/api/products').then((r) => r.json());
dcupl.data.set(allProducts, { model: 'Product' });

// ✅ Good: Load filtered data
const activeProducts = await fetch('/api/products?active=true').then((r) => r.json());
dcupl.data.set(activeProducts, { model: 'Product' });
```

### Remove unused properties

Don't define properties you won't use. dcupl stores only the attributes the model declares (unless the model uses `autoGenerateProperties`), so every declared property costs memory on every item:

```typescript
// ❌ Bad: Include everything
properties: [
  { key: 'id', type: 'string' },
  { key: 'name', type: 'string' },
  { key: 'legacyField1', type: 'string' },
  { key: 'legacyField2', type: 'string' },
  { key: 'internalNote', type: 'string' },
  // ...
];

// ✅ Good: Only needed properties
properties: [
  { key: 'id', type: 'string' },
  { key: 'name', type: 'string' },
  { key: 'price', type: 'float' },
];
```

### Clear unused data

Remove data that's no longer needed:

```typescript title="Remove specific items"
dcupl.data.remove([{ key: 'old1' }, { key: 'old2' }, { key: 'old3' }], { model: 'Product' });
await dcupl.update();

// Clear all data for a model
dcupl.data.reset({ model: 'Product' });
await dcupl.update();
```

## Large dataset handling

### Load data in chunks

When an API returns the data in pages, stage every page with `upsert()` and call `init()` once at the end. Staged data is processed in one pass, so the chunks cost one processing run instead of one per chunk:

```typescript
async function loadLargeDataset() {
  const chunkSize = 10000;
  let offset = 0;

  while (true) {
    const chunk = await fetch(`/api/products?offset=${offset}&limit=${chunkSize}`).then((r) => r.json());

    if (chunk.length === 0) break;

    // upsert adds to the staged data - set() would replace the previous chunks
    dcupl.data.upsert(chunk, { model: 'Product' });

    offset += chunkSize;
  }

  await dcupl.init();
}
```

### Virtualization

For rendering large lists:

```tsx title="Use virtual scrolling libraries"
import { FixedSizeList } from 'react-window';

// Create the list once, not on every render
const productList = dcupl.lists.create({ modelKey: 'Product' });

function ProductList() {
  const products = productList.catalog.query.items();

  return (
    <FixedSizeList height={600} itemCount={products.length} itemSize={50} width="100%">
      {({ index, style }) => <div style={style}>{products[index].name}</div>}
    </FixedSizeList>
  );
}
```

## Profiling and debugging

### Measure init performance

```typescript
console.time('init');
await dcupl.init();
console.timeEnd('init');
```

### Timings from events

The events dcupl emits for its methods carry a `duration` in milliseconds. Subscribe with `dcupl.on()` for instance-wide events and `list.on()` for a list's queries and functions:

```typescript
dcupl.on((msg) => {
  if (msg.type === 'dcupl_initialized' || msg.type === 'dcupl_updated_manually') {
    console.log(msg.type, msg.duration);
  }
});

productList.on((msg) => {
  if (msg.type === 'query_execute' || msg.type === 'fn_facets') {
    console.log(msg.type, msg.duration);
  }
});
```

### Measure query performance

```typescript
const list = dcupl.lists.create({ modelKey: 'Product' });

console.time('query');
list.catalog.query.addCondition({
  attribute: 'category',
  operator: 'eq',
  value: 'Electronics',
});
const results = list.catalog.query.items();
console.timeEnd('query');
```

### Measure update performance

```typescript
console.time('update');
dcupl.data.update([{ key: 'p1', price: 999 }], { model: 'Product' });
await dcupl.update(); // The work happens here - data calls only stage changes
console.timeEnd('update');
```

## Practical examples

### Example 1: e-commerce product catalog

```typescript title="Catalog model and queries"
const productModel: ModelDefinition = {
  key: 'Product',
  properties: [
    { key: 'sku', type: 'string', index: true },
    { key: 'name', type: 'string' },
    { key: 'brand', type: 'string' },
    { key: 'price', type: 'float' },
    { key: 'inStock', type: 'boolean' },

    // Derived for faster queries; index: true makes eq/in on it index lookups
    {
      key: 'categoryName',
      type: 'string',
      index: true,
      derive: {
        localReference: 'category',
        remoteProperty: 'name',
      },
    },

    // Display only
    { key: 'description', type: 'string' },
    { key: 'images', type: 'json' },
  ],
  references: [{ key: 'category', type: 'singleValued', model: 'Category' }],
};

// Reuse list
const productList = dcupl.lists.create({ modelKey: 'Product' });
const PAGE_SIZE = 50;

// Fast queries
function searchProducts(filters, page = 0) {
  productList.catalog.query.clear();

  if (filters.category) {
    productList.catalog.query.addCondition({
      attribute: 'category', // Reference - matched by key, answered from its index
      operator: 'eq',
      value: filters.category,
    });
  }

  if (filters.inStock) {
    productList.catalog.query.addCondition({
      attribute: 'inStock',
      operator: 'eq',
      value: true,
    });
  }

  return productList.catalog.query.items({ start: page * PAGE_SIZE, count: PAGE_SIZE });
}

// Efficient updates
// data.update matches items by their key, not by sku
async function updatePrice(productKey, newPrice) {
  dcupl.data.update([{ key: productKey, price: newPrice }], { model: 'Product' });
  await dcupl.update();
}
```

### Example 2: real-time dashboard

```typescript title="Optimized for frequent updates"
const metricsModel: ModelDefinition = {
  key: 'Metric',
  properties: [
    { key: 'timestamp', type: 'date' },
    { key: 'type', type: 'string' },
    { key: 'value', type: 'float' },
  ],
};

dcupl.models.set(metricsModel);
await dcupl.init();

// Fast aggregation
const metricsList = dcupl.lists.create({ modelKey: 'Metric' });

// gte/lte on a date property match Date objects or timestamps, not ISO strings
function getMetrics(type: string, startDate: Date, endDate: Date) {
  metricsList.catalog.query.clear();
  metricsList.catalog.query.addGroup({
    groupKey: 'filters',
    groupType: 'and',
    queries: [
      { attribute: 'type', operator: 'eq', value: type },
      { attribute: 'timestamp', operator: 'gte', value: startDate.getTime() },
      { attribute: 'timestamp', operator: 'lte', value: endDate.getTime() },
    ],
  });

  return metricsList.catalog.fn.aggregate({
    attribute: 'value',
    types: ['sum', 'avg', 'min', 'max'],
  });
}

// Efficient real-time updates: buffer incoming metrics,
// add them with upsert and apply them in one update per second
type Metric = { key: string; timestamp: Date; type: string; value: number };
const pending: Metric[] = [];

function addMetric(metric: Metric) {
  pending.push(metric);
}

setInterval(async () => {
  if (pending.length === 0) return;
  dcupl.data.upsert(pending.splice(0), { model: 'Metric' });
  await dcupl.update();
}, 1000);
```

### Example 3: order management

```typescript
const orderModel: ModelDefinition = {
  key: 'Order',
  properties: [
    { key: 'orderId', type: 'string', index: true },
    { key: 'status', type: 'string' },
    { key: 'total', type: 'float' },
    { key: 'createdAt', type: 'date' },

    // Derived for fast queries; index: true makes eq/in on it index lookups
    {
      key: 'customerEmail',
      type: 'string',
      index: true,
      derive: {
        localReference: 'customer',
        remoteProperty: 'email',
      },
    },
  ],
  references: [{ key: 'customer', type: 'singleValued', model: 'Customer' }],
};

// Efficient status updates
async function updateOrderStatus(orderKey, newStatus) {
  dcupl.data.update([{ key: orderKey, status: newStatus }], { model: 'Order' });
  await dcupl.update();
}

// Fast customer order lookup
const orderList = dcupl.lists.create({ modelKey: 'Order' });

function getCustomerOrders(email) {
  orderList.catalog.query.clear();
  orderList.catalog.query.addCondition({
    attribute: 'customerEmail', // Derived + indexed: an index lookup, no reference resolution
    operator: 'eq',
    value: email,
  });
  return orderList.catalog.query.items();
}
```

### Example 4: search performance

```typescript title="Optimized search model"
const articleModel: ModelDefinition = {
  key: 'Article',
  properties: [
    { key: 'title', type: 'string' },
    { key: 'slug', type: 'string', index: true },
    { key: 'tags', type: 'Array<string>' },
    { key: 'authorName', type: 'string' },
    { key: 'status', type: 'string' },
    { key: 'publishedAt', type: 'date' },

    // Content fields
    { key: 'content', type: 'string' },
    { key: 'excerpt', type: 'string' },
  ],
};

const articleList = dcupl.lists.create({ modelKey: 'Article' });

const escapeRegex = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

function searchArticles(query: string) {
  articleList.catalog.query.clear();

  // Conditions on different attributes are combined with AND,
  // so group them with groupType 'or'.
  // '/term/' = substring search; regexFlags 'i' = case-insensitive
  const pattern = `/${escapeRegex(query)}/`;
  articleList.catalog.query.addGroup({
    groupKey: 'search',
    groupType: 'or',
    queries: [
      { attribute: 'title', operator: 'find', value: pattern, options: { regexFlags: 'i' } },
      { attribute: 'tags', operator: 'find', value: pattern, options: { regexFlags: 'i' } },
      { attribute: 'authorName', operator: 'find', value: pattern, options: { regexFlags: 'i' } },
    ],
  });

  return articleList.catalog.query.items({ start: 0, count: 20 });
}
```

`find` evaluates every item of the list, so text search cost grows with the list. See [Text search](/sdk/queries/query-search) for the matching rules.

### Example 5: inventory management

```typescript
const inventoryModel: ModelDefinition = {
  key: 'Inventory',
  properties: [
    { key: 'sku', type: 'string', index: true },
    { key: 'warehouse', type: 'string' },
    { key: 'location', type: 'string' },
    { key: 'quantity', type: 'int' },
    { key: 'reorderPoint', type: 'int' },
    { key: 'lastUpdated', type: 'date' },
  ],
};

// Batch stock updates
async function updateStock(updates) {
  const data = updates.map((u) => ({
    key: u.key, // The item key - data.update ignores keys it doesn't know
    quantity: u.quantity,
    lastUpdated: new Date().toISOString(),
  }));

  dcupl.data.update(data, { model: 'Inventory' });
  await dcupl.update();
}

// Fast low-stock query
const inventoryList = dcupl.lists.create({ modelKey: 'Inventory' });

function getLowStock() {
  inventoryList.catalog.query.clear();
  inventoryList.catalog.query.addCondition({
    attribute: 'quantity',
    operator: 'lte',
    value: 10,
  });
  return inventoryList.catalog.query.items();
}
```

## Performance benchmarks

Timings depend on your models (references, derived and expression properties), your data and the device, so measure with your own data using the profiling snippets above. As a reference point: measured with 1 million items, indexed queries took 25–65 ms, facets about 2.7 s. What holds in general:

- **One `dcupl.update()` per batch.** Staging 100 changes and applying them with a single `dcupl.update()` is far faster than calling `dcupl.update()` after each change.
- **Partial beats full.** `data.update()`, `upsert()` and `remove()` reprocess only the changed attributes; `data.set()` rebuilds the whole model and everything that depends on it.
- **Repeated reads are cached.** Re-reading a list's result for an unchanged query skips query evaluation; what remains is the per-read deep copy, which you can turn off with `performance.clone.enabled: false` if you never mutate returned items.

## Performance checklist

### Model setup

- ✅ Use derived properties for common deep queries
- ✅ Remove unused properties
- ✅ Use appropriate data types
- ✅ Use `index: true` for frequently looked-up keys

### Querying

- ✅ Filter by exact value on references or `index: true` properties
- ✅ Put the most selective condition first in `and` groups
- ✅ Use `in` instead of an `or` group of `eq` conditions on one attribute
- ✅ Avoid deep queries (use derived properties)
- ✅ Use pagination for large results
- ✅ Reuse lists instead of creating new ones
- ✅ Keep query groups simple

### Updates

- ✅ Call `init()` once, then apply changes with `dcupl.update()`
- ✅ Use partial updates (`data.update()`, `upsert()`, `remove()`)
- ✅ Stage a batch of changes, then call `await dcupl.update()` once
- ✅ Avoid full `init()` after updates

### Memory

- ✅ Load only needed data
- ✅ Remove unused data
- ✅ Destroy unused lists
- ✅ Use virtualization for large lists

## Troubleshooting

### Slow queries

**Problem:** Queries take too long

**Solution:**

1. Measure query time:

```typescript
console.time('query');
const results = list.catalog.query.items();
console.timeEnd('query');
```

2. Simplify the query: put indexed, selective conditions first and replace `or` groups of `eq` conditions with one `in` condition:

```typescript
// Instead of { groupType: 'or', queries: [{ attribute: 'status', operator: 'eq', value: 'new' }, { ..., value: 'sale' }] }
list.catalog.query.addCondition({ attribute: 'status', operator: 'in', value: ['new', 'sale'] });
```

3. Use derived properties for deep queries

### Slow updates

**Problem:** Updates are slow

**Solution:** Use partial updates:

```typescript
// ❌ Slow: full rebuild
dcupl.data.set(allData, { model: 'Product' });
await dcupl.update();

// ✅ Fast
dcupl.data.update([{ key: 'p1', price: 999 }], { model: 'Product' });
await dcupl.update();
```

### High memory usage

**Problem:** Memory usage is too high

**Solution:**

1. Remove unused properties
2. Load filtered data
3. Clear unused data
4. Destroy unused lists

### Slow init

**Problem:** `dcupl.init()` takes too long

**Solution:**

1. Reduce data size
2. Remove unused models
3. Disable quality validation for non-critical models (`quality: { enabled: false }` on the model), or for all models with `new Dcupl({ quality: { enabled: false } })`
4. Try `performance: { containerBatchSize: 0 }`: `init()` no longer yields to the event loop between data containers, so it finishes sooner but blocks the UI while it runs
5. Profile with [event timings](#timings-from-events)

## Next steps

- [Caching](/sdk/advanced/caching) — Caching strategies
- [Data management](/sdk/core-concepts/data-management) — Loading and updating data
- [Lists](/sdk/core-concepts/lists) — Working with lists
- [Queries](/sdk/queries/query-filter) — Query optimization
- [Models](/sdk/core-concepts/models) — Property indexing
- [Dcupl API](/sdk/api/dcupl) — All constructor options
