Model async userland loading in app routes as a state machine#95791
Merged
Conversation
Contributor
Tests PassedCommit: 843ceb7 |
Contributor
Stats from current PR🔴 3 regressions
📊 All Metrics📖 Metrics GlossaryDev Server Metrics:
Build Metrics:
Change Thresholds:
⚡ Dev Server
📦 Dev Server (Webpack) (Legacy)📦 Dev Server (Webpack)
⚡ Production Builds
📦 Production Builds (Webpack) (Legacy)📦 Production Builds (Webpack)
📦 Bundle SizesBundle Sizes⚡ TurbopackClient Main Bundles
Server Middleware
Build DetailsBuild Manifests
📦 WebpackClient Main Bundles
Polyfills
Pages
Server Edge SSR
Middleware
Build DetailsBuild Manifests
Build Cache
🔄 Shared (bundler-independent)Runtimes
📝 Changed Files (8 files)Files with changes:
View diffsapp-route-ex..ntime.dev.jsDiff too large to display app-route-ex..time.prod.jsDiff too large to display app-route-tu..ntime.dev.jsDiff too large to display app-route-tu..time.prod.jsDiff too large to display app-route-tu..ntime.dev.jsDiff too large to display app-route-tu..time.prod.jsDiff too large to display app-route.runtime.dev.jsDiff too large to display app-route.ru..time.prod.jsDiff too large to display 📎 Tarball URLCommit: 843ceb7 |
gaearon
force-pushed
the
gaearon-async-route-modules
branch
from
July 14, 2026 20:09
8fe8ffa to
e65c7e7
Compare
gaearon
force-pushed
the
gaearon-metadata-image-tla
branch
2 times, most recently
from
July 14, 2026 20:14
6add49b to
2cc4a83
Compare
gaearon
force-pushed
the
gaearon-async-route-modules
branch
2 times, most recently
from
July 14, 2026 20:30
d0432e5 to
0f9b6c6
Compare
gaearon
force-pushed
the
gaearon-metadata-image-tla
branch
from
July 14, 2026 20:30
2cc4a83 to
1af6c3c
Compare
gaearon
force-pushed
the
gaearon-async-route-modules
branch
from
July 14, 2026 22:08
0f9b6c6 to
c20aa17
Compare
gaearon
force-pushed
the
gaearon-metadata-image-tla
branch
from
July 14, 2026 22:08
1af6c3c to
01a8521
Compare
gaearon
force-pushed
the
gaearon-async-route-modules
branch
from
July 14, 2026 22:47
c20aa17 to
928f52d
Compare
gaearon
changed the base branch from
gaearon-metadata-image-tla
to
gaearon-route-init-errors
July 14, 2026 22:49
gaearon
force-pushed
the
gaearon-async-route-modules
branch
from
July 14, 2026 23:15
928f52d to
491b4bf
Compare
gaearon
force-pushed
the
gaearon-route-init-errors
branch
from
July 14, 2026 23:15
910f6ae to
a030bef
Compare
gaearon
force-pushed
the
gaearon-async-route-modules
branch
from
July 14, 2026 23:51
491b4bf to
13023de
Compare
gaearon
marked this pull request as ready for review
July 15, 2026 00:09
The userland module of an app route was represented by independently
varying fields (a factory that's nulled after first use, a pending
promise, and init error flags), and reading exports off a still-loading
module silently yielded undefined values.
The loading is now modeled as a LazyModule, a small standalone state
machine advancing one-way on first use:
uninitialized ─(sync module)──▶ resolved
uninitialized ─(async module)─▶ pending ──▶ resolved | rejected
The states stay internal to LazyModule; consumers use one method per
consumption mode: loadIfNeeded() kicks off loading (synchronous load
and onLoad errors throw to the caller, as before, and leave the module
uninitialized), waitUntilLoaded() also waits until the module has
settled, and assertLoaded() returns the module. Errors from an async
module (top-level await) have no caller to propagate to and settle as
'rejected', which waitUntilLoaded() and assertLoaded() rethrow;
previously, a failed async module load reported the module as forever
pending, and an init error was only rethrown from ensureUserland()
while the getter returned the half-initialized module.
assertLoaded() throws while the module is pending, so any unchecked
userland access anywhere fails loudly instead of silently misbehaving.
LazyModule distinguishes async modules with instanceof Promise, so a
module exporting a function named `then` is not mistaken for one.
The option to provide the userland module as an object (instead of a
factory) is removed; the app route template is the only construction
site and always passes a factory.
gaearon
force-pushed
the
gaearon-async-route-modules
branch
from
July 15, 2026 00:14
13023de to
843ceb7
Compare
gaearon
force-pushed
the
gaearon-route-init-errors
branch
from
July 15, 2026 00:14
a030bef to
2f8a398
Compare
timneutkens
approved these changes
Jul 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Extracted from #95468, tries to address @lubieowoce feedback.
This should be a pure refactor with no publicly observable behavior changes. We want to simplify the state of how we're loading and tracking modules, and simplify signatures:
LazyModulethat hides per-module state. You can kick it (loadIfNeeded), wait for completion withwaitUntilLoaded, or peek-or-throw withassertLoaded..userlandwhile pending now throws with an invariant so we don't do it accidentally again. Should be unreachable from current callsites.userlandfactory.Added tests to cover the cases that the refactor had a risk of breaking.