Performance
Citry provides three explicit rendering optimizations:
simple = Truerenders a presentation component without its own instance, hooks or browser identity.Constmarks an individual value that will not change, so template work depending only on that value can be prepared once.pure = Truemarks an entire component class whose body is deterministic and side-effect-free, so equal occurrences within one root render can reuse the settled body strings.
Start without them, measure a real repeated-render workload, and choose the contract that fits the component.
Skip independent setup with simple components
Use simple = True for a presentation component that only needs to turn inputs and optional default content into HTML:
from citry import Component
class StatusLabel(Component):
simple = True
class Kwargs:
text: str
template = """
<span class="status">{{ text }}</span>
"""
The surrounding component owns the output. Inputs and data callbacks remain live on every invocation; equal inputs are not required. Citry rejects declarations or calls that need an independent instance, including component hooks, own JS/CSS, named outlets and component-level client bindings. Use a static template_data(kwargs, slots) method if data needs preparation.
The Simple components guide explains the supported content, inheritance and error rules. This changes the component's contract, so it is useful only where independent identity and hooks are unnecessary.
| Choice | What it avoids | What your code promises |
|---|---|---|
simple = True | Independent component setup and ownership records | The component fits the restricted presentation contract |
Const(value) | Repeating template work based only on that value | The marked value will not change |
pure = True | Repeating safe body work for equal data within one root render | The template is deterministic and side-effect-free |
You can combine simple and pure declarations when both contracts apply. The data callback still runs. Simple bodies with default outlets remain live.
Reuse stable values with Const
Use Const when the same component input appears across many renders and never changes. Citry can then finish the template work that depends on that input once and reuse the result.
This is useful for repeated rows with the same label, components with stable layout choices, and application-wide presentation settings. It is a focused rendering optimization, not a general cache for component output.
Const(...) describes one value. For a whole component body, see Reuse a pure component body.
Mark a stable input
Wrap the value at the point where you pass it to a component:
from citry import Component, Const
class Metric(Component):
class Kwargs:
label: str
value: str
template = """
<p>
<strong>{{ label }}</strong>
<span>{{ value }}</span>
</p>
"""
rows = [
Metric(label=Const("Status"), value="Ready"),
Metric(label=Const("Status"), value="Waiting"),
]
The label is the same for both rows, so Citry can reuse the rendered <strong> content. Each value remains ordinary input and renders normally.
Const is a promise from your code. Citry does not watch the value for later changes, so treat the marked value as read-only.
What Citry can reuse
Citry precomputes a template part when every value needed by that part is constant:
{{ expression }}becomes reusable escaped text.- A
<c-if>chain keeps only its selected branch. - A
<c-for>loop that produces only text can be unrolled once, up to 1,000 iterations. - Constant attribute expressions become reusable attribute text unless an installed extension needs to process the final attributes.
Child component tags and slot or fill content stay live. They may create new components or depend on the template that supplied the content. Constant expressions inside those live areas can still be precomputed.
Keep template expressions free of side effects. Citry may evaluate a constant expression while preparing a branch, even when that branch is not selected in the current render.
Let template literals be constant automatically
Values written directly on a component tag cannot vary between renders, so Citry marks them for you:
<c-Grid columns="3" compact="" />
<c-Grid c-columns="1 + 2" c-breakpoints="[480, 900]" />
The same applies to an expression attribute with no variable references. Citry also forwards the optimization through a direct expression attribute when all of that expression's variables are known constant.
Citry evaluates an expression before marking its complete result as the child input. In c-total="add(1, 2)", the arguments remain ordinary integers. When every referenced variable, including add, is known constant, Citry marks the evaluated result at the child-input root.
Make a default constant
Mark a typed default when the omitted value should receive the optimization:
from citry import Component, Const
class Grid(Component):
class Kwargs:
columns: int = Const(3)
template = """
<div c-style="{'--columns': columns}">
{{ columns }} columns
</div>
"""
Grid() uses the constant default. Grid(columns=4) receives an ordinary dynamic value unless the caller passes Const(4).
Mark stable output from a custom callback
Citry consumes the markers it adds automatically and an explicit Const(...) at the root of each component input or output. Those values reach component kwargs, data callbacks, hooks, and template expressions as ordinary Python values. Citry keeps the optimization metadata separately, so identity checks, type() checks, JSON serialization, path operations, and standard-library APIs work normally for the values Citry prepares.
The base template_data method returns the component kwargs. Citry knows that this mapping preserves each name and value, so the earlier Metric and Grid examples keep their known const inputs without an override.
A custom template_data callback can run arbitrary Python, so Citry does not infer constness from equal values. It does preserve a marked input when the final output has the same key and is the exact same ordinary object:
def template_data(self, kwargs, slots):
return {
"label": kwargs.label,
}
If the caller supplied label=Const(value) and the input schema preserves value's identity, this direct pass-through keeps its promise when the final output still has that identity. Citry compares after the output schema and data hooks. The same rule applies to return kwargs: each same-key input keeps its known constness when both schema stages retained the recorded object.
Renamed or replaced outputs are dynamic unless the callback makes a new promise. Mark them only when the result will stay stable:
def template_data(self, kwargs, slots):
return {
"heading": Const(kwargs.label),
"value": Const(kwargs.value.strip()),
}
Citry uses Python's is identity test. Normal singleton and interning behavior therefore applies: a recomputed immutable value can count as the same object when Python reuses its identity.
Schemas that validate, coerce, or otherwise transform data do not inherit the input's optimization by name alone. A same-key field can keep it by retaining the exact input object; otherwise the final named field needs an explicit Const(...) promise. Citry normalizes marked defaults and factories on its generated dataclasses, but it does not assume how arbitrary schema-owned factories behave.
Citry consumes each output marker before a template expression reads its value.
Use marker helpers before rendering
is_const and const_value remain useful when your own code handles a manually marked value before passing it to Citry:
from citry import Const, const_value, is_const
marked = Const("status")
if is_const(marked):
plain = const_value(marked)
The manual marker is a Python proxy until Citry consumes it. Like other proxy objects, it cannot preserve identity with the wrapped object, and an API that requires an actual built-in value may reject it. Call const_value() before handing such a value directly to that API.
Citry recursively unwraps a value only when its outermost object is Const: Const([Const(1)]) reaches component code as [1]. This cleanup follows exact builtin containers. An ordinary container is left alone, so [Const(1)] still contains a marker. Unwrap a manually nested marker at the point where your code uses it:
from operator import add
from citry import Const, const_value
items = [Const(1)]
total = add(const_value(items[0]), 2)
Calling add(items[0], 2) would pass the proxy to add. This also applies to a marker inserted into an ordinary container by component or extension code.
Root markers supplied together in one normalization operation are converted together, preserving aliases between those roots. When marked and unmarked fields share a graph that must be rebuilt, the marked value can become a separate cleaned graph; the unmarked original stays unchanged. Citry does not inspect attributes or contents inside custom objects. Marker cycles encountered beneath an outer Const raise ValueError, and a marked graph that requires rebuilding a cyclic tuple or frozenset may also be rejected. A custom unhashable object that cannot form a stable cache key renders normally.
Do not mark a one-shot generator. Precomputing can consume it, leaving later work with an exhausted iterator. Use a stable list or tuple instead.
Extensions can also keep attribute processing live by implementing the on_attrs_resolved hook. This preserves the extension's chance to inspect or change the final attributes.
Choose values that will repeat
The optimization cache keeps the 512 most recently used combinations on each Citry instance. Citry.clear empties it.
Marking Const(user.id) when nearly every user has a different ID creates many entries with little or no reuse. Prefer values such as fixed labels, small layout choices, and stable configuration that recur across many renders.
Different types remain different cache inputs. Const(True) and Const(1) do not share an entry, even though Python considers those values equal.
Reuse a pure component body
When a small component appears many times with repeated data, it can opt into render-local body memoization:
from citry import Component
class StatusIcon(Component):
pure = True
class Kwargs:
state: str
template = """
<span c-class="state">{{ state }}</span>
"""
This is a class-level promise: rendering the template body must be a deterministic, side-effect-free function of its template variables. Citry still creates each ordinary component instance, runs its data and lifecycle hooks, and gives it a fresh render ID. A component also declared simple keeps the simple contract described above. Within one root render, a later equal body can reuse the first body's immutable strings and transparent control-flow shape. When a body also renders a child or a slot, that live content still renders again while safe work beside it can be reused. The memo is discarded when the root render ends.
Do not declare a component pure when its template expressions mutate state, consume one-shot iterators, read ambient values not present in template data, or rely on a per-element extension hook running for every occurrence. Body items that create child components, slot or ownership records, or i18n capture remain live even when safe sibling items are reused. A subclass must state pure = True again because it can add new behavior.
Purity pays only when equal instances repeat within the same tree. A component that appears once, or whose inputs are unique every time, should remain on the ordinary path. Use Const(...) when only selected values are stable; use pure = True only when the complete body satisfies the stronger promise.
Related pages
- Cache rendered output for reusing a complete rendered subtree.
- Rendering for the full render and serialization process.