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 withdcupl.update() - Cached reads - Each list caches its query results, facets and aggregations until the next update that affects it
- Partial updates -
data.update(),upsert()andremove()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 rebuildPartial 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 changesWhen dcupl falls back to a full update
- A
data.set()(ordata.reset()) anywhere in the batch turns the whole update into a full rebuild. - A model definition changed after
init(): amodels.set()ormodels.update()that registers a definition (even an unchanged one), or amodels.remove()of a registered model, makes the nextdcupl.update()rebuild all data likeinit()(thedcupl_partial_updateevent reportsisPartialUpdate: 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
modelRatioThresholdanditemRatioThresholdinperformance.partialUpdates(both default0.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 (thedcupl_partial_updateevent then reportsisPartialUpdate: 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.
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 changesRemove items efficiently
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();
countmeans 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 samestart/countoptions work onquery.execute(), which takes the sameDcuplListQueryOptions. They do not apply toquery.many(), which selects byitemKeysand accepts onlyprojection. If you store paging in the list's query withquery.applyOptions({ start, count })instead,items()andcount()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:
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:
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
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
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
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 singledcupl.update()is far faster than callingdcupl.update()after each change. - Partial beats full.
data.update(),upsert()andremove()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: falseif 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: truefor frequently looked-up keys
Querying
- ✅ Filter by exact value on references or
index: trueproperties - ✅ Put the most selective condition first in
andgroups - ✅ Use
ininstead of anorgroup ofeqconditions 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 withdcupl.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:
- Measure query time:
console.time('query');
const results = list.catalog.query.items();
console.timeEnd('query');- Simplify the query: put indexed, selective conditions first and replace
orgroups ofeqconditions with oneincondition:
// Instead of { groupType: 'or', queries: [{ attribute: 'status', operator: 'eq', value: 'new' }, { ..., value: 'sale' }] }
list.catalog.query.addCondition({ attribute: 'status', operator: 'in', value: ['new', 'sale'] });- 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:
- Remove unused properties
- Load filtered data
- Clear unused data
- Destroy unused lists
Slow init
Problem: dcupl.init() takes too long
Solution:
- Reduce data size
- Remove unused models
- Disable quality validation for non-critical models (
quality: { enabled: false }on the model), or for all models withnew Dcupl({ quality: { enabled: false } }) - 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 - Profile with event timings