Skip to main content

Renderer Deep Dive

The Renderer (Renderer.tsx) is the isomorphic engine that bridges React and the Server response. It is the most specialized class in the server package, performing Component Orchestration and Payload Pruning to ensure the initial HTML payload is both minimal and fully hydrated.

The render budget

A render is abandoned after abortAfterMSeconds. The default is scaled to the flush mode, because the deadline does not guard the same thing in both:

Flush modeDefaultThe deadline covers
onShellReady (default)5000 msthe shell and the drain below it
onAllReady (experimental.singleRootIslands)10000 msevery boundary of the complete page

Set it per project in archibald.json, or per call site:

{
"cli": {
"render": {
"abortAfterMSeconds": 8000
}
}
}
const renderer = new Renderer({ dataClient: DataClient.getInstance(), abortAfterMSeconds: 8000 });

The deadline stays armed across the drain, in both modes. The stream settles at the flush, which is not the end of the render as soon as the tree contains Suspense boundaries — single-root islands wrap every island in one — so a deadline disarmed at the flush would leave the drain unguarded and one hung island could hold the response open indefinitely. A deadline reached during the drain is reported as a timeout fallback rather than served as truncated markup.

It has to fit the request timeouts below it

The render budget and the request timeouts inside it are configured independently but only work as a pair: a render is worth starting only if the requests inside it fail early enough to leave time to render the page without them. Worst case for one boundary is every attempt of one read timing out — with the default single retry, 2 × read.server — and the render still needs room afterwards, so:

abortAfterMSeconds >= (attempts + 1) × api.timeout.read.server

CoreServer checks this at boot and logs a warning when it does not hold. It is a warning, not a refusal to boot: a violated budget makes SSR degrade, not misbehave. See request timeouts and retries for the values on the other side of that relation.

Reporting a render

RouterContextInterface carries matchedPattern — the absolute pattern of the deepest <Route> that matched (/:language/product/:code), not the requested path. It is the label to report per-page metrics under, because it has the cardinality of the route table rather than of the traffic:

const context = {} as RouterContextInterface;
const markup = await renderer.render(
<StaticRouter location={initialLocation} context={context}>
<App />
</StaticRouter>
);

await this.prometheusService.observe({ path: context.matchedPattern ?? initialLocation, status: appClient.httpStatus }, duration, ObserveType.RENDER);

context.matches holds the matched chain behind it. Both are set on the static (server) context once the routes have matched, and stay unset for a request that redirected before matching.

Failure handling and the client side fallback

SSR never fails the request. When the render throws, the stream errors, or the render exceeds abortAfterMSeconds, the Renderer sets hydrate = false and returns empty markup — the page is served and the client renders it from scratch.

Because the request still looks successful, the fallback has to be detected explicitly. An empty string is not a failure signal — a legitimately empty render produces one too. Use the structural API instead:

const renderer = new Renderer({
dataClient: DataClient.getInstance(),
// Called with the cause before the fallback is returned. Register this to log and report the
// failure with the request context your project has — it replaces archibald's own
// `Logger.error`, which is uncorrelated and hard to trace back to a request.
onRenderError: (error) => Logger.error(`SSR fallback for ${initialLocation} (${error.reason}): ${error.stack}`)
});

// Either branch on the result…
const { markup, error, fallback } = await renderer.renderResult(app);

// …or keep `render()` and ask afterwards.
const markup = await renderer.render(app);
if (renderer.hasFallenBack()) {
const error = renderer.getRenderError();
}

Every failure arrives as a RenderError carrying the original as cause and a reason:

reasonRaised when
renderA component threw during SSR, or React reported a shell error.
streamThe response stream errored while being drained.
timeoutThe render exceeded abortAfterMSeconds (RenderTimeoutError, a RenderError).

Every RenderError also carries elapsedMs, the wall-clock time the render got before giving up — a render that failed 4.9s into a 5s budget is a different problem from one that threw immediately.

To maintain a clean and modular documentation structure, the rendering process is broken down into the following specialized areas:

1. Component & Asset Orchestration

Detailed guide on the three-phase asset processing lifecycle, including build-time mapping, SSR discovery flow (the report() mechanism), and critical asset inlining.

2. Real-World Integration Example

A technical analysis of how the RenderService in the shop template orchestrates the framework's renderer, manages state hydration, and injects third-party snippets.


Connection Points

  • @archibald/cli (Rspack): Produces the loadable.json manifest used for discovery.
  • @archibald/storage (DataClient): Holds the data and messages that the Renderer prunes before serialization.
  • @archibald/core (CaptureProvider): Provides the React context bridge between the components and the Renderer.
  • @archibald/storefront (Loadable): The UI-layer components that trigger the discovery reports.