Performance

Performance Patterns

Performance Patterns

Grid model authors normally write formulas, not execution plans. A few authoring patterns make large models easier for Grid to store, recalculate, and explain.

Prefer Repeated Shapes

Repeated spreadsheet patterns are cheaper than thousands of unrelated formulas. When you fill a formula down or across, keep the shape regular:

Pattern Example
Constant fills A1:Z1000 = 0
Linear sequences B2 = B1 + 5, copied down
Running sums B2 = A2 + B1, copied down
Moving windows B10 = SUM(A1:A10), copied down
Coordinate grids A1 = ROW_INDEX + COL_INDEX
Same-row blends D1 = A1 * 0.5 + B1 * 0.3 + C1 * 0.2

Regular ranges preserve the same formula semantics while giving Grid a compact representation of the repeated work.

Keep Dependencies Narrow

Prefer formulas that reference the smallest range that actually matters.

# Better when only this month's rows matter
monthly_total = SUM(B2:B32)
 
# More expensive and less precise
monthly_total = SUM(B:B)

Use full-column or full-row references only when the model genuinely needs the whole column or row.

Isolate Expensive Work

Give expensive formulas their own named bindings, then reference those names from downstream formulas:

raw_quotes ~= HTTP_JSON(url) DEFAULT {}
latest_price = raw_quotes.price DEFAULT 0
portfolio_value = shares * latest_price

This makes the model easier to inspect, lets you add fallbacks at the boundary, and avoids repeating expensive calls.

Use Lazy Bindings Deliberately

~= delays evaluation until the binding is read. It is useful for external calls, large matrix work, or optional outputs:

scenario_detail ~= RUN_SCENARIO(inputs) DEFAULT {}
summary = scenario_detail.total DEFAULT 0

Use eager = for values that should always stay current, and lazy ~= for work that is expensive or rarely viewed.

Keep Terminal Collection Pipelines Together

When only the final summary is part of the model, express the complete transformation as one binding:

positive_total = values |> MAP(value => value * 2) |> FILTER(value => value > 0) |> SUM()
positive_count = values |> FILTER(value => value > threshold) |> COUNT()

Grid can execute eligible homogeneous numeric MAP/FILTER stages in authored order without publishing each intermediate collection. A named intermediate is still appropriate when people or other formulas need to inspect or reuse it; publication is then an intentional semantic boundary and may require materialization.

Use a named predicate lambda inside FILTER. _ in a pipe call denotes the whole piped value, as in SORT(_, -1); the legacy FILTER(_, _ > 0) spelling should not be used in new models.

Direct numeric scalar captures such as threshold remain ordinary reactive dependencies. A changed capture replays the complete pipeline; sparse source edits can update a filtered COUNT or COUNTA from changed pages only while the capture is unchanged. SUM and AVERAGE deliberately retain a source-ordered full fold so floating-point results cannot change through unsafe reassociation. These are transparent optimizations: unsupported operations or computed captures, mixed values, dense edits, and observable empty results use exact fallback execution.

gridctl cell explain <model> <symbol> reports whether the physical collection-pipeline plan was selected, its ordered stages and capture count, or a bounded reason such as published_intermediate, unsupported_capture, or unsupported_stage. These decisions explain performance without changing formula meaning.

See collections.md for the complete language contract.

Let Frame Plans Stay Declarative

Frame and relational pipelines carry a versioned physical plan with row estimates and a reason for every decision. Grid chooses hash, membership, scalar-lookup, or bounded nested-loop joins; when both input sizes are known, it hashes the smaller legal side while preserving authored result order. A left-only predicate after an inner join may move before the join, and an immediate simple projection lets the join copy only the requested columns. Missing statistics use a deterministic compatibility plan.

These choices do not change source semantics. Keep filters and projections visible as ordinary pipeline stages instead of manually materializing intermediate Frames. Explain output records cardinalities, build side, algorithm, pushdowns, and late-materialized columns when investigating a slow model.

Split Big Models Into Sections

Readable sections help both people and Grid:

# Inputs
 
# Normalized data
 
# Calculations
 
# Outputs

Keep input normalization close to the raw inputs, group formulas by purpose, and name key intermediate values. The result is easier to review and easier to debug when an output surprises you.

Watch For Common Slow Shapes

Shape Better habit
Repeating the same external call in many bindings Put the call in one named binding and reference it
Referencing huge ranges when a small range is enough Use bounded ranges
Mixing many unrelated formulas in one dense area Group related calculations together
Deep chains without named checkpoints Name important intermediate results
External calls without fallback values Use DEFAULT, IFERROR, or WITH ... ELSE
Publishing every stage of a terminal-only numeric collection pipeline Keep the MAP/FILTER chain and final aggregate in one binding

Performance-friendly models are usually the same models people find easiest to read: regular shapes, named boundaries, bounded ranges, and explicit fallbacks.