Skip to content

Compare page renders in each tab's websocket process - #54

Merged
dannote merged 3 commits into
masterfrom
tab-documents
Oct 5, 2026
Merged

dannote merged 3 commits into
masterfrom
tab-documents

Conversation

@dannote

@dannote dannote commented Oct 3, 2026

Copy link
Copy Markdown
Member

Builds on the page revalidation and opt-in morph from 0.20.0. A page found out what changed by requesting itself again and diffing the answer against its live DOM, which scripts have modified since. That left three gaps: attributes on <html>/<body>, metadata in <head>, and elements owned by client code (an island's props), which could only be handled by reloading.

Idea

Each open page already has a websocket process that lives as long as the page. With a new :document option and :morph, that process keeps the HTML the server last rendered for its page. On a document update it renders the page again and compares the two server renders, then sends the page only what applies to it:

  • nothing, when its HTML is the same;
  • a reload, when scripts or stylesheets differ or preserved elements were added or removed;
  • otherwise the new HTML to patch, plus what the server changed outside the body's contents.
plug Volt.DevServer, document: {MySite, :render_document, []}

The function is called with the page's path, followed by the listed arguments, and returns {:ok, html} or :error.

What the comparison enables

  • Root attributes. Only the attributes the server set, changed or removed on <html> and <body> are applied. One a script set, such as a theme, is in neither render and stays.
  • Head. Metadata and links (not scripts, stylesheets or the title) that are gone are removed, and new ones added.
  • Owned elements. For a preserved element whose attributes the server changed, the client sets them and dispatches a cancelable volt:element-update event. Its owner re-renders and calls preventDefault(); the page reloads only if nothing handles it.
  • No request per page per save. Unchanged pages hear nothing; changed pages get the HTML over the socket.

The page starts from the HTML it was served: the dev server keeps it until the page connects, so opening a page costs no extra render. At most 64 pages per session are kept, for pages that never connect. A page that was served other HTML than the server can reproduce falls back to revalidating itself, as in 0.20.0.

Verification

  • mix test: 770 passed. mix lint passes.
  • Browser tests (--include integration, not run in CI): 33 passed, including an end-to-end test of the push flow: no request from the page, an owned element re-rendered by its owner, root and head changes applied, a script-set attribute kept.
  • Tried in a real browser on a copy of an Astral site with the companion Astral branch: changing a post's title and description, and renaming a heading that feeds a Vue island, updated the page with no reload and no request from the page; a second tab on another post received nothing.

Open points

  • :document and volt:element-update are new public API between Volt and frameworks.
  • The body is still patched with idiomorph from whole HTML; the server does not send patch operations.
  • The 64-page cap is arbitrary, and kept HTML is cleared only when the session ends or the cap is reached.
  • One unrelated-looking test failed once in an early full-suite run and did not recur in later runs; I could not identify it.

A page found out what changed by requesting itself again and diffing the
answer against its live DOM, which scripts have modified since. So a
change to an owned element's attributes, such as an island's props,
could only be handled by reloading.

Each open page already has a websocket process that lives as long as
the page. With the new :document option, a function that renders a
page's HTML, and :morph, that process keeps the HTML the server last
rendered for its page. On a document update it renders the page again
and compares the two server renders, then sends the page only what
applies to it: nothing, a reload, or the new HTML with the owned
elements whose attributes the server changed.

For those elements the client sets the attributes and dispatches a
cancelable volt:element-update event; the page reloads only if nothing
handles it. Pages that were served other HTML than the server can
reproduce fall back to revalidating themselves.
The comparison of two server renders now also names the attributes the
server changed on <html> and <body>, and the metadata and links it
changed in <head>. The client applies only those, so an attribute a
script set, such as a theme, stays; a blind copy from the new document
would remove it.

A page's websocket process rendered the page when it connected, to have
something to compare with. The dev server now keeps the HTML of each
page it serves until the page connects, so opening a page costs no
extra render. At most 64 pages per session are kept, for pages that
never connect.

A browser test covers the whole push flow: no request from the page,
an owned element re-rendered by its owner, and root and head changes
applied.
@dannote
dannote merged commit 3e5beee into master Oct 5, 2026
2 checks passed
@dannote dannote mentioned this pull request Oct 5, 2026
@dannote
dannote deleted the tab-documents branch October 5, 2026 13:51
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