fix(runtime): retry lazy component load after a failed dynamic import#6772
fix(runtime): retry lazy component load after a failed dynamic import#6772yasinkocak wants to merge 1 commit into
Conversation
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
There was a problem hiding this comment.
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.hasInitializedComponentwhen 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=Ncache-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.
| } 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)); | ||
| } |
|
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 |
|
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! |
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:
HOST_FLAGS.hasInitializedComponentis set before the load attempt ininitialize-component.tsand never cleared on failure.HOST_FLAGS.hasConnected(set unconditionally on first connect) makesconnected-callback.tsskip re-initialization on every subsequent reconnect.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?
HOST_FLAGS.hasInitializedComponentis cleared instead of staying stuck.connected-callback.tsretriesinitializeComponent()the next time the host element is reconnected (disconnect + reconnect), instead of only handling the "already has a$lazyInstance$" case.client-load-module.tsappends 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?
Testing
src/runtime/test/lazy-load-retry.spec.tsx(2 new tests): verifieshasInitializedComponentis cleared on a failed load, and that a disconnect+reconnect after a failure re-attempts initialization and succeeds once the module becomes available.npm run test.jest -- --runInBand, 64 suites / 651 tests acrosssrc/runtime+src/client) passes with no regressions.*.entry.jsrequest to simulate a blocked/flaky network request):Constructor for "web-button#undefined" was not found, element stays un-hydrated (empty shadow root).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.