feat(router): blockSSR#8793
Conversation
🦋 Changeset detectedLatest commit: d57a664 The changes in this PR will be included in the next version bump. This PR includes changesets to release 5 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
Pull request overview
Adds a new blockSSR option to Qwik Router route loaders so SSR can either await loaders by default (allowing redirect/error to short-circuit) or opt out and run loaders in the background without affecting the page response.
Changes:
- Introduces
LoaderOptions.blockSSR(defaulttrue) and propagates it through loader internals. - Updates loader execution to detach response controls for
blockSSR: falseloaders and adds middleware/unit/e2e/docs coverage. - Adds a changeset announcing the new router feature.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/qwik/src/core/shared/serdes/serialize.ts | Adds a TODO comment related to async serialization behavior. |
| packages/qwik-router/src/runtime/src/types.ts | Adds blockSSR to loader options and internal loader shape. |
| packages/qwik-router/src/runtime/src/route-loaders.ts | Implements response-detaching for background loaders and threads blockSSR into server capture. |
| packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.unit.ts | Adds unit tests covering blocking vs background loader SSR behavior. |
| packages/qwik-router/src/middleware/request-handler/resolve-request-handlers-core.ts | Changes loader middleware to start loaders and await only blocking loaders before SSR. |
| packages/docs/src/routes/docs/(qwikrouter)/route-loader/index.mdx | Documents default SSR-blocking behavior and the new “background loaders” option. |
| e2e/qwik-e2e/tests/qwikrouter/loaders.e2e.ts | Adds an e2e regression test ensuring unread background loaders don’t fail SSR. |
| e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/loaders/non-blocking/index.tsx | Adds an e2e fixture route that throws from a background loader without reading .value. |
| .changeset/lazy-route-loaders.md | Declares a minor release for @qwik.dev/router to ship blockSSR. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Comparing the two shapes for deferred/streamed loader data — a Capability comparison
The one thing only bare promises can express: dynamic cardinalityA search page: fast ID query, slow per-item hydration, and you want each result to pop in individually as its own fetch settles. // bare-promise fields — one streamed unit PER ITEM, count known only at runtime
export const useSearch = routeLoader$(async (ev) => {
const ids = await searchIds(ev.query.get('q')); // fast, critical, envelope-capable
return { ids, items: ids.map((id) => fetchItem(id)) }; // N bare promises, N = runtime
});
// render: each card reveals as ITS OWN promise settles
{data.value.ids.map((id, i) => (
<Await key={id} promise={data.value.items[i]} fallback={<SkeletonCard />}>
{(item) => <ProductCard item={item} />}
</Await>
))}Why // Attempt 1 — one deferred loader with Promise.all: loses per-item streaming
export const useItems = routeLoader$(async (ev) => {
const ids = await ev.resolveValue(useSearchIds);
return Promise.all(ids.map(fetchItem)); // signal resolves when the SLOWEST item does
}, { defer: true }); // → the whole grid pops at once, no per-item pop-in
// Attempt 2 — one loader per item: impossible
// loaders are static module-level exports; you can't declare useItem_0 … useItem_N
// when N depends on the request. One loader = one signal; there is no per-instance
// parameterization of a routeLoader$.The unit of deferral for a |
@qwik.dev/core
@qwik.dev/router
eslint-plugin-qwik
create-qwik
@qwik.dev/optimizer
@qwik.dev/devtools
commit: |
built with Refined Cloudflare Pages Action⚡ Cloudflare Pages Deployment
|
| allBlockSSRLoaders = ( | ||
| allBlockSSRLoaders ? allBlockSSRLoaders.then(() => promise) : promise | ||
| ) as Promise<void>; |
There was a problem hiding this comment.
this way, the first error in route order will throw even if there are multiple errors.
Also, the loads all start at the same time so the .then() chaining doesn't cause a waterfall
| '@qwik.dev/router': minor | ||
| --- | ||
|
|
||
| route loaders gain a `blockSSR` option (default `true`); set `blockSSR: false` to run a loader in the background without blocking SSR |
|
In the PR it mentions the recommended setting for loaders is Should that be the default instead? Or less radically, could there be a global setting to change the default to |
Routeloaders block SSR by default and can be opted-out.