Skip to content

Smoother reloads: flash-free stylesheet swap, debounced reload, reload only changed pages - #50

Merged
dannote merged 3 commits into
masterfrom
smoother-reloads
Oct 3, 2026
Merged

dannote merged 3 commits into
masterfrom
smoother-reloads

Conversation

@dannote

@dannote dannote commented Oct 3, 2026

Copy link
Copy Markdown
Member

Stacked on #49. Makes dev reloads less disruptive for server-rendered sites (Phoenix pages, Astral), after looking at how Vite and Astro handle the same cases.

Changes

  • Stylesheet swap without a flash (as Vite does). The client adds a second <link> for the updated stylesheet and removes the previous one once the new one has loaded or failed, instead of changing href in place.
  • Debounced reload (as Vite does). Every reload goes through one pageReload(), so a save that produces several messages reloads once.
  • Reload only the pages whose HTML changed. A change in reload_dirs used to reload every open page.

How page revalidation works

Volt does not know which pages a template or content file feeds. Neither Vite nor Astro tracks that either: Vite scopes reloads to the edited .html page, and Astro reloads everything.

  1. Each successful HTML response that passes through Volt.DevServer gets an entity tag of its HTML, in the etag header and in data-volt-etag on the client <script>.
  2. On a change in a reload directory the watcher broadcasts a document update instead of a full reload.
  3. The client requests its own page again with If-None-Match. The server renders it, as it would for a reload, and answers 304 when the HTML is the same.
  4. Only another answer reloads the page.

No server state or dependency graph is involved. Pages that already load the dev client (site generators inject it themselves) are tagged on their existing tag, so Astral 0.5.1 works without changes. Pages without the tag, or whose HTML differs on every render, reload as before.

Volt.HMR.document_update/2 sends the update from other packages; Volt.HMR.Document documents the contract for servers that send HTML without going through Volt.DevServer.

Verification

  • mix test: 754 passed. Credo, the architecture check and Dialyzer pass locally.
  • Browser tests (--include integration, not run in CI): 29 passed, including a new one where editing another page's content does not reload the open page and editing its own content reloads it once.
  • Run against a copy of a real Astral site with three pages "open" (simulated by a script doing the conditional request): editing a post reloaded only that post; a whitespace-only edit and a HEEx comment in the layout reloaded nothing; a shared component edit reloaded only the page using it.
  • Not run locally: the type-aware JS lint step (tsgolint missing on my machine).

Also in this PR

  • The overlay browser fixture still used the pre-0.19.0 overlay API and failed, which stopped the client fixture run before the stylesheet tests. It is rewritten for the current API.

Not included

  • The page being edited still does a full reload; patching the DOM in place is a separate design.
  • Asset files in a reload directory under the asset root still take the asset path and reload fully.

Both follow Vite's dev client.

A stylesheet update changed the href of the existing <link>, which
leaves the page unstyled until the new stylesheet arrives. The client
now inserts a second <link> and removes the previous one when the new
one loads or fails.

Each message that needs a reload called location.reload() directly.
They now go through one debounced pageReload(), so a save that produces
several such messages reloads once.

The overlay browser fixture still used the pre-0.19.0 overlay API and
failed, which stopped the client fixture run before the stylesheet
tests. It is rewritten for the current API.
A change in reload_dirs asked every open page for a full reload, whether
or not the file affects it. Volt does not know which pages a template or
content file feeds, and neither Vite nor Astro tracks that either: Vite
scopes reloads to the edited .html page, and Astro reloads everything.

Pages now carry an entity tag of the HTML they were rendered with. On a
change the watcher broadcasts a "document" update; the client requests
its page again with If-None-Match, and the dev server, which renders it
anyway, answers 304 when the HTML is the same. Only another answer
reloads the page. No server state or dependency graph is involved.

Volt.DevServer does this for the HTML responses it adds its client to.
Volt.HMR.Document describes the contract for servers that inject the
client themselves, and Volt.HMR.document_update/2 sends the update.
Pages without the tag reload as before.
Site generators such as Astral add Volt's client <script> to the pages
they render, and the dev server left those pages alone. They now get
the same entity tag, on their existing tag, and the same 304 answer, so
they take part in document revalidation without any change of their
own.
@dannote
dannote changed the base branch from investigate-agent-issues to master October 3, 2026 11:26
@dannote
dannote merged commit d1d6343 into master Oct 3, 2026
2 checks passed
@dannote dannote mentioned this pull request Oct 3, 2026
@dannote
dannote deleted the smoother-reloads 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