.md

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)
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)
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())
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.

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.

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.

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

{
  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.

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:

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)

// ❌ 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)

// ✅ 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() 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 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.

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.

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

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.

// 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:

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.

// ⚠️ 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.

// ⚠️ 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.

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:

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.

// ✅ 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.

// ❌ 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:

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

// Clean up
tempList.destroy();

List pooling

For frequently created/destroyed lists:

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:

// ❌ 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:

// ❌ 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:

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:

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:

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

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:

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

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

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

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

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

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

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 for the matching rules.

Example 5: inventory management

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:
console.time('query');
const results = list.catalog.query.items();
console.timeEnd('query');
  1. Simplify the query: put indexed, selective conditions first and replace or groups of eq conditions with one in condition:
// Instead of { groupType: 'or', queries: [{ attribute: 'status', operator: 'eq', value: 'new' }, { ..., value: 'sale' }] }
list.catalog.query.addCondition({ attribute: 'status', operator: 'in', value: ['new', 'sale'] });
  1. Use derived properties for deep queries

Slow updates

Problem: Updates are slow

Solution: Use partial updates:

// ❌ 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