djust 0.4.5rc1

Pre-releaseReleased

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 in djust_vdom that uses html5ever's parse_fragment with 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_nodes now 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 carry text, so an explicit is_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 via OnceLock<ResolvedInheritance> on the Template struct (shared via TEMPLATE_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_assigns uses identity + shallow fingerprints (id, length, content hash for list-of-dicts) instead of copy.deepcopy. Framework-internal keys (csrf_token, kwargs, temporary_assigns, DATE_FORMAT, TIME_FORMAT) and auto-generated _count keys are excluded from set_changed_keys to 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_rust previously 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 in get_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 in test_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 morphElement and SetAttr patches now clear .value when the name attribute changes.

  • In-place dict mutation detection_snapshot_assigns now fingerprints list contents (id + dict values hash) to detect mutations like todo['completed'] = True that don't change the list's id or length. Falls back to id-only for unhashable values.

  • Derived context value detection — When _changed_keys is set, the sync also checks non-immutable context values by id() to catch derived values (e.g., products from _products_cache) that change via private attributes.

All releases · Atom feed