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_priceThis 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 0Use 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
# OutputsKeep 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.