Skip to content

feat: elide bare async-render components (ship only when async render isn't the sole client signal) #474

Description

@vivek7405

Problem

Follow-up refinement to #469 / #470 (PR #470). #470 made async render() a blanket interactivity signal in the elision analyser, so every async-render component ships its module to the browser and re-fetches on hydration. That is over-conservative: a bare async-render component (an async render() with no other client signal) renders identical first-paint HTML with or without JS, so it can be elided like any display-only component, saving a module download plus a redundant hydration RPC + DB hit for zero observable change. This is a common shape in content / docs apps (fetch-and-display leaves).

Today's elision note ("an async-render component is always shipped, never elided") should be corrected.

Design / approach

Refined rule. async render() is interactive iff the component also has another signal: an @event, a non-state reactive prop, a signal / reactive import, a lifecycle hook, a <slot>, renderFallback(), cross-module observation, static shadow = true, or a transitively-reachable interactive dep / child. A bare light-DOM async-display component with none of those becomes elidable. Implementation: drop async render from the standalone-signal short-circuit in component-elision.js analyzeComponentSource, and add static shadow = true as a ship signal.

Why it is safe (grounded in the existing machinery).

  • Nested interactive child is already fenced by the elision fixpoint's IMPORT RULE (component-elision.js:817-839): a shipping interactive child forces its importer to ship, so a bare async parent that imports / registers an interactive child still ships.
  • Prop-drilling (fetch in page/layout, drill props down) is orthogonal and unchanged: the page is server-only; the receiving component already ships if it has a non-state reactive prop and elides if state-only. The refinement only touches components that fetch their OWN data.
  • Cross-module observation (whenDefined / :defined / instanceof) already forces shipping.
  • The differential-elision guard (render elision on vs off, assert identical post-hydration DOM + behavior) is the backstop and fails CI on any wrong-elide.

The one genuinely new risk: shadow DOM. Declarative Shadow DOM attaches only during HTML PARSING. A STREAMED shadow component arrives via a JS replaceWith, so its DSD would not attach without the module running attachShadow, and the analyser is context-free (can't tell if a component will be streamed). Safe carve-out: static shadow = true always ships.

The one behavior change no guard can catch (deliberate). Eliding removes the stale-while-revalidate refresh on hydration (a re-fetch ~200ms after SSR). Moot for request-stable data; only matters if the author wanted fresh-on-load data. The opt-out already exists (renderFallback() is a ship signal, as is a changeable prop or a signal); consider also an explicit static refresh = true. Document it.

Relationship to #472 (deferred hydration seed): for a bare component, eliding ships NOTHING, which strictly dominates the seed (which still ships the module to read a cached value). The seed stays relevant only for async components that ship for other reasons. #472 stays out of scope.

Acceptance criteria

  • Differential-elision cases (render on vs off, identical observable behavior): bare async ELIDED (zero application JS, network-probed) · async+@event ships · async+signal ships · async+non-state prop ships · async+renderFallback ships · async+static shadow ships · async-importing-an-interactive-child ships (import rule) · async parent + child registered elsewhere → parent elided, child still upgrades and works.
  • An elided bare async component renders correctly JS-off AND JS-on with no console error (e2e + the JS-off probe).
  • Counterfactual: each "ships" case fails to ship when its signal is removed.
  • The lifecycle-coverage / elision SSOT guard updated.
  • Docs corrected (root AGENTS.md, packages/core/AGENTS.md, agent-docs/components.md, the data-fetching doc page): drop the "async render is always shipped / never elided" note; document the shadow carve-out and the renderFallback / static refresh opt-out.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

  • Status
    Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions