Changed
Text-region fast path now fires for
{% extends %}templates — The scanner that builds the VDOM text-node position index used to process the full pre-hydration HTML, but the VDOM is rooted at[dj-root]. On templates extending a base (with<title>, meta tags, scripts outside dj-root), the scanner counted text runs in<head>and trailing<footer>/<script>siblings that the VDOM didn't have — the count mismatched, the index was discarded, and every event fell through to a full html5ever parse (~10ms on the djust.org /examples/ page). Now the scanner is restricted to the dj-root element's interior via a balanced-tag walker. Rust render drops from ~14ms → ~2.8ms on extends templates; browser E2E (production, DEBUG=False) drops from 30ms → ~25ms avg, 18ms min.See
docs/website/core-concepts/templates.md.Text-region VDOM fast path — Extends the existing text-fast-path to handle changes that differ only in a text span, even when the surrounding fragment contains tags. Computes byte-level common prefix/suffix on pre-hydration HTML; if the divergence is a single tag-free text run, locates the owning VDOM text node via a pre-built positional index (binary search on
(html_start, html_end, path, text, djust_id)entries, built once per full-parse render and kept in sync through fast-path events by shifting downstream entries by the byte delta). Patches in place and skips html5ever entirely. For a counter click inside a{% for %}loop on a 309KB page, Rust render drops from ~12ms to ~2.7ms. UTF-8 safe (snaps to char boundaries), handles<pre>/<code>/<textarea>whitespace preservation and<script>/<style>raw-text element bodies correctly, bails to full parse on entity-offset mismatches.parse_html_fragment(html, context_tag)— New public entry point indjust_vdomthat uses html5ever'sparse_fragmentwith a parent-element context. Enables parsing isolated HTML fragments with correct tokenization for context-sensitive elements (<tr>,<td>,<option>), without resetting the dj-id counter. Scaffolding for future structural-fragment fast paths.collect_vdom_text_nodesnow skips comment nodes — Previously collected<!--dj-if-->placeholders into the text-node list, shifting every subsequent ordinal by one and breaking any position-based patching. Text and comment VNodes both carrytext, so an explicitis_text()filter was needed.Partial template rendering (#737) — Per-node dependency tracking at template parse time. On re-render, only template nodes whose context variable dependencies changed are re-rendered; unchanged nodes reuse cached HTML. For a single-variable change on a page with 50 template nodes, template render drops from ~1.4ms to ~0.1ms. Changed keys are passed from Python to Rust via
set_changed_keys(), which merges across multiple sync calls.{% include %}and custom tags always re-render (wildcard dependency).{% extends %}inheritance resolution caching — Templates using{% extends %}now participate in partial rendering. Inheritance is resolved once viaOnceLock<ResolvedInheritance>on theTemplatestruct (shared viaTEMPLATE_CACHE). Final merged nodes and their deps are cached, so subsequent renders skip both chain building and static parent nodes. Combined with partial rendering, extends templates go from full re-render (~14ms Rust) to partial render of changed nodes only (~0.02ms Rust).Text-only VDOM fast path — When all changed template fragments are plain text (no HTML tags), skip both html5ever parsing and VDOM diffing entirely. The old VDOM is mutated in-place via a fragment→text-node map built on first render, and SetText patches are produced directly. For counter-style updates: parse phase drops from ~12ms to ~0.001ms.
Block flattening for partial rendering —
{% block %}nodes left by Django's template engine are flattened to expose each child as a separate fragment. This enables the text fast path to activate on pages using{% extends %}where Django resolves blocks.Faster change detection —
_snapshot_assignsuses identity + shallow fingerprints (id, length, content hash for list-of-dicts) instead ofcopy.deepcopy. Framework-internal keys (csrf_token,kwargs,temporary_assigns,DATE_FORMAT,TIME_FORMAT) and auto-generated_countkeys are excluded fromset_changed_keysto avoid spurious re-renders.Optimized VNode parser — Pre-sized attribute HashMap, eliminated redundant
to_lowercase()call, removed form element debug output.
Fixed
Derived immutable context values no longer go stale on partial re-render —
_sync_state_to_rustpreviously skipped id()-based change detection for immutable types (int/str/bool/bytes) to avoid false positives from Python's int cache, which meant derived values computed inget_context_data(e.g.completed_count = sum(...),total_count = len(...)) were never synced to Rust when their sources changed. Partial rendering would then reuse the cached HTML for template nodes depending on those values, leaving counters stale after add/toggle/delete. Fixed by tracking previous VALUES for immutable keys and comparing by equality. Regression tests intest_changed_tracking.py::TestDerivedImmutableSync.VDOM input value leak on name change — When the patcher morphs an input into a different field (e.g., wizard step 1 name → step 2 email), the old field's typed value no longer leaks into the new field. Both
morphElementandSetAttrpatches now clear.valuewhen thenameattribute changes.In-place dict mutation detection —
_snapshot_assignsnow fingerprints list contents (id + dict values hash) to detect mutations liketodo['completed'] = Truethat don't change the list's id or length. Falls back to id-only for unhashable values.Derived context value detection — When
_changed_keysis set, the sync also checks non-immutable context values by id() to catch derived values (e.g.,productsfrom_products_cache) that change via private attributes.