.md

Performance best practices

This page sums up the principles behind fast dcupl applications. The options, thresholds and code for each of them are in the SDK guide Performance optimization.

Performance principles

Initialize once, update incrementally

init() processes all models and data. Call it once at startup. Afterwards, stage changes with data.update(), data.upsert() or data.remove() and apply them with await dcupl.update(); never call init() again to apply a change. See Partial Updates.

Prefer partial updates, and batch them

A partial update reprocesses only the changed items and the attributes that depend on them. data.set() and data.reset() always trigger a full rebuild, which re-runs the processing of init() over all models, and dcupl also rebuilds fully when more than half of the models and more than half of their items changed, when a model definition changed after init() (models.set(), models.update() or models.remove()), or when the data a query reference depends on changed. See When dcupl Falls Back to a Full Update. Each dcupl.update() call has a fixed cost, so stage a batch of changes and apply it with one call.

Filter on indexed properties

References are indexed, and index: true adds an index to a property. eq and in conditions on them are index lookups; other operators evaluate the items. In an 'and' group, conditions run in order on the items that are left, so put the most selective scanning conditions first. Index-backed eq and in conditions are cheap in any position. See Query Optimization.

Flatten deep queries

Each level of a deep query such as customer.country resolves one more reference for every evaluated item. Use a derived property for paths you filter on often, keep paths to 2–3 levels, and see Data modeling for how to structure references.

Read pages, not everything

Pass start and count to a read: items({ start: 50, count: 50 }). Paging stored with applyOptions({ start, count }) works too; count() then returns the page length, and fn.metadata().currentSize the number of all matches. See Use Pagination.

Reuse and destroy lists

Create a list once and change its query instead of creating a list per search. Every dcupl.lists.create() registers a list that stays alive until you call destroy(). See List Management.

Load only what you need

Select resources with tags, leave out properties the app never reads, and render long result lists virtualized. See Memory Optimization and Large Dataset Handling.

Measure with your own data

Timings depend on your models, your data and the device. Time init(), dcupl.update() and your reads with console.time(), or read the duration of the dcupl_initialized and dcupl_updated_manually events. See Profiling and Debugging.

What holds in general

  • One dcupl.update() per batch is far faster than one dcupl.update() per change.
  • Partial updates (data.update(), upsert(), remove()) are faster than replacing a model with data.set().
  • Repeated reads of an unchanged query are served from the list's cache until the next update that affects the list.

Performance checklist

  • init() runs once, at startup
  • Changes are staged and applied with one await dcupl.update() per batch
  • Update payloads hold only the changed attributes
  • Exact-match filters use references or index: true properties
  • Frequent deep query paths are derived properties
  • Large result sets are read page by page
  • Lists are reused, and destroyed when no longer needed
  • Only the data and properties the app needs are loaded