Skip to content

Patch changed pages in place (opt-in morph) - #51

Merged
dannote merged 1 commit into
masterfrom
document-morph
Oct 3, 2026
Merged

dannote merged 1 commit into
masterfrom
document-morph

Conversation

@dannote

@dannote dannote commented Oct 3, 2026

Copy link
Copy Markdown
Member

Stacked on #50. After #50, a page whose server-rendered HTML changed still does a full reload. This adds an opt-in mode where the dev client patches the page instead, so scroll position, focus and JavaScript state survive edits to text and classes.

Configuration

config :volt, :server, morph: true
config :volt, :server, morph: [preserve: "[data-island]"]
Value Meaning
false (default) A changed page reloads.
true A changed page is patched in place.
[preserve: selector] Patched in place, except elements matching the CSS selector, which are left alone with everything inside them.

The setting is emitted as data-volt-morph on the client <script>, which is what the client reads, so a framework can also set it per page.

How it works

The client already has the new HTML at this point, from the revalidation request added in #50. With morph it parses that HTML and patches <body> with idiomorph, vendored into the dev client the same way lit-html is. It then stores the new entity tag and dispatches volt:document-updated on document.

It falls back to a reload when patching would not be safe:

  • the scripts the page runs differ (data blocks such as application/json are patched);
  • stylesheet links or <style> elements differ;
  • the server answers with an error;
  • a preserved element was added or removed, or the attributes the server renders for it changed.

Verification

  • mix test: 755 passed. mix lint passes in full locally.

  • Browser tests (--include integration, not run in CI): 30 passed, with seven new tests for the patching rules and an end-to-end test where a page is patched twice and keeps its state.

  • Tried in a real browser on a copy of an Astral site (unmodified Astral 0.5.1, one config line added), on a page with a Vue island:

    Edit Reloaded JS state Scroll Island
    Add a sentence to the post no kept kept still mounted and reactive
    Add a class in the layout no kept kept still mounted
    Add an inline script to the layout yes (fallback) lost reset remounted

Limits

  • Only the contents of <body> and the title are patched. Attributes on <html> and <body> and the rest of <head> stay as they were until the next reload.
  • A change to a preserved element's server-rendered attributes (island props) reloads the page; remounting just that element needs a hook in the framework.
  • Scripts that attach behaviour to elements do not see newly inserted elements unless they listen for volt:document-updated.
  • idiomorph adds about 53 KB unminified to the dev client, whether or not a project opts in.
  • Neither Vite nor Astro patches server-rendered HTML in place, so there is no reference behaviour for the edge cases.

A page whose HTML changed still reloaded. The client already has the new
HTML at that point, from revalidating the page, so with :morph it
patches the body with idiomorph instead: only nodes that differ change,
and scroll position, focus and JavaScript state survive an edit to text
or classes.

Patching cannot re-run scripts or reload stylesheets, so the client
falls back to a reload when the scripts the page runs or its
stylesheets differ, or when the server answers with an error.
morph: [preserve: selector] names elements owned by client code, such
as mounted components. They are never touched, and the page reloads if
what the server renders for them changes.

The client dispatches volt:document-updated after a patch. Only the
body's contents and the title are patched.

Opt-in, because a server has to vouch that its pages tolerate patching.
Neither Vite nor Astro does this.
@dannote
dannote changed the base branch from smoother-reloads to master October 3, 2026 11:26
@dannote
dannote merged commit 1e81a18 into master Oct 3, 2026
2 checks passed
@dannote dannote mentioned this pull request Oct 3, 2026
@dannote
dannote deleted the document-morph branch October 3, 2026 13:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant