Skip to content

fix(runtime): retry lazy component load after a failed dynamic import#6772

Open
yasinkocak wants to merge 1 commit into
stenciljs:mainfrom
yasinkocak:fix/lazy-load-retry-after-failure
Open

fix(runtime): retry lazy component load after a failed dynamic import#6772
yasinkocak wants to merge 1 commit into
stenciljs:mainfrom
yasinkocak:fix/lazy-load-retry-after-failure

Conversation

@yasinkocak

Copy link
Copy Markdown

What is the current behavior?

GitHub Issue Number: #6771

A host element is permanently "bricked" for the rest of the page's lifetime if its lazy bundle's dynamic import() fails a single time (dropped network request, flaky mobile connection, ad-blocker, stale CDN entry, etc.) — no rendered content, no lifecycle callbacks, and no recovery, not even for a brand-new element of the same tag, until a full page reload.

Root cause:

  1. HOST_FLAGS.hasInitializedComponent is set before the load attempt in initialize-component.ts and never cleared on failure.
  2. HOST_FLAGS.hasConnected (set unconditionally on first connect) makes connected-callback.ts skip re-initialization on every subsequent reconnect.
  3. The browser's own per-document module map caches the failed import() specifier, so even a bare retry of the exact same bundle URL never reaches the network again — confirmed in a live-browser repro (see Testing below).

What is the new behavior?

  • On a failed lazy load, HOST_FLAGS.hasInitializedComponent is cleared instead of staying stuck.
  • connected-callback.ts retries initializeComponent() the next time the host element is reconnected (disconnect + reconnect), instead of only handling the "already has a $lazyInstance$" case.
  • client-load-module.ts appends a cache-busting query param (?s-retry=N) on retries, the same pattern already used for ?s-hmr=, so the retry actually reaches the network instead of being short-circuited by the browser's module map.

No public API changes. No behavior change on the happy path — a retry only ever happens after a load has actually failed.

Documentation

N/A — this is an internal runtime resilience fix, no public API or documented behavior changes.

Does this introduce a breaking change?

  • Yes
  • No

Testing

  • Added src/runtime/test/lazy-load-retry.spec.tsx (2 new tests): verifies hasInitializedComponent is cleared on a failed load, and that a disconnect+reconnect after a failure re-attempts initialization and succeeds once the module becomes available.
  • Full existing suite (npm run test.jest -- --runInBand, 64 suites / 651 tests across src/runtime + src/client) passes with no regressions.
  • End-to-end verified against a real browser (Playwright + a minimal Stencil app + a tiny Node server that drops the *.entry.js request to simulate a blocked/flaky network request):
    • Initial load blocked → Constructor for "web-button#undefined" was not found, element stays un-hydrated (empty shadow root).
    • Network "recovers", element disconnected + reconnected:
      • Before this fix: still un-hydrated, zero new network requests (browser module-map caching of the failed specifier).
      • After this fix: GET /build/p-xxx.entry.js?s-retry=1 → 200, element renders correctly (class="hydrated", populated shadow DOM) — all without a page reload.

Other information

Per CONTRIBUTING.md, issue #6771 was filed first with a full reproducible write-up before starting this PR.

A single failed dynamic import() of a lazy component's *.entry.js chunk
(dropped network request, flaky mobile connection, etc.) permanently
bricks that host element for the rest of the page's lifetime: no
rendered content, no lifecycle callbacks, and no recovery -- not even
for a brand-new element of the same tag -- until a full page reload.

Root cause: HOST_FLAGS.hasInitializedComponent is set before the load
attempt and never cleared on failure, and HOST_FLAGS.hasConnected (set
unconditionally on first connect) blocks connected-callback.ts from
ever retrying. On top of that, the browser's own per-document module
map caches the failed import() specifier, so even a bare retry of the
same bundle would never reach the network again.

This clears hasInitializedComponent when the lazy constructor isn't
found, lets connected-callback.ts retry initialization on the next
reconnect, and cache-busts the retried import() URL (the same pattern
already used for ?s-hmr=) so the retry actually reaches the network.

Fixes stenciljs#6771
Copilot AI review requested due to automatic review settings July 14, 2026 11:43
@yasinkocak yasinkocak requested a review from a team as a code owner July 14, 2026 11:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves Stencil’s runtime resilience for lazy-loaded components by allowing recovery after a transient dynamic import() failure (e.g., flaky network / blocked request), instead of leaving the host element permanently unable to initialize for the lifetime of the page.

Changes:

  • Clear HOST_FLAGS.hasInitializedComponent when a lazy-load constructor cannot be resolved, enabling future retry attempts.
  • Retry initializeComponent() on disconnect+reconnect when a prior initialization attempt failed.
  • Add an ?s-retry=N cache-busting query param on repeated lazy-bundle import attempts to avoid browser module-map caching of failed imports.
  • Add regression tests covering flag reset and reconnect-triggered retry behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
src/runtime/test/lazy-load-retry.spec.tsx Adds regression tests for failed lazy-load recovery via flag clearing + reconnect retry.
src/runtime/initialize-component.ts Clears hasInitializedComponent when constructor resolution fails so the host can retry later.
src/runtime/connected-callback.ts Re-attempts initialization on reconnect after a failed prior initialization.
src/client/client-load-module.ts Tracks failed bundle imports and appends s-retry to bust browser module-map caching on retries.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +121 to 130
} else if ((hostRef.$flags$ & HOST_FLAGS.hasInitializedComponent) === 0) {
// A previous initialization attempt for this host element failed
// (e.g. the lazy bundle's dynamic import() was rejected) and cleared
// `hasInitializedComponent` (see `initialize-component.ts`). Retry
// now that we're reconnecting, rather than leaving the element
// permanently un-upgraded for the rest of the page's lifetime.
initializeComponent(elm, hostRef, cmpMeta);
} else if (hostRef?.$onReadyPromise$) {
hostRef.$onReadyPromise$.then(() => fireConnectedCallback(hostRef.$lazyInstance$, elm));
}
@moessie

moessie commented Jul 14, 2026

Copy link
Copy Markdown

Thanks for working on this! I tested the proposed fix with a minimal reproduction by blocking the initial lazy-loaded *.entry.js request and then reconnecting the component after restoring the chunk.

With this change, the component successfully retries and renders instead of remaining stuck. This looks like a solid improvement for the recovery behavior

@t-model00291182

Copy link
Copy Markdown

We are running into the same issue in our Stencil-based component library.

A consumer recently reported this behavior to us, and after debugging we traced it back to this issue. This confirms that the problem is not only reproducible in isolated cases but can also affect real-world consumers of Stencil components.

We really appreciate the work the Stencil team is doing. A fix for this would have a meaningful impact for us and other teams building component libraries on top of Stencil.

Thanks for looking into this!

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.

4 participants