solidjs/solid
solid-js/web (server entry) and SSR
Active contributors: Ryan Carniato, Damian Tarnawski
When solid-js is loaded under the node, deno, or worker export condition, solid-js/web resolves to the server bundle. This page covers that bundle and the underlying server reactive system.
Purpose
Solid's SSR pipeline turns a component tree into either:
- A complete HTML string (
renderToString). - A promise of a string, after waiting for resources to settle (
renderToStringAsync). - A streamed response that flushes shells first and resolves async holes via
seroval-serialized payloads (renderToStream).
Plus the same JSX surface as the browser (control-flow tags, <Portal>, <Dynamic>), reimplemented for the server.
Directory layout
packages/solid/
├── src/server/
│ ├── index.ts # re-export hub
│ ├── reactive.ts # SSR stub of the reactive runtime
│ └── rendering.ts # createComponent, control flow, createResource, lazy, escape helpers
└── web/server/
└── index.ts # solid-js/web server entry: Dynamic, Portal stub, mergeProps overrideThe packages/solid/web/server/index.ts file is the public entry; everything it exposes ultimately routes through packages/solid/src/server/.
Key abstractions
| Symbol | File | Description |
|---|---|---|
renderToString(fn, options?) |
dom-expressions/src/server.js (re-exported via web/server/index.ts's export * from "./server") |
Synchronous SSR. Runs the component, collects the output, returns a string. |
renderToStringAsync(fn, options?) |
same | Awaits all pending resources before returning a string. |
renderToStream(fn, options?) |
same | Streams a shell, then patches in async holes as resources resolve. Uses seroval to serialize Promise values, URL, Date, etc. |
ssr(template, ...nodes) / ssrElement(...) / ssrAttribute(...) / ssrHydrationKey() / ssrClassList(...) / ssrStyle(...) |
same | Compiler-emitted SSR primitives. |
escape(html) / resolveSSRNode(node) |
packages/solid/src/server/rendering.ts |
HTML-escape a string or recursively resolve a node tree to a string. |
Server createSignal, createMemo, createEffect, createRoot, createComputed, createRenderEffect, createReaction, createDeferred, createSelector, getListener, untrack, batch, on, children, createContext, useContext, getOwner, runWithOwner, equalFn, requestCallback, mapArray, indexArray, observable, from, enableExternalSource |
packages/solid/src/server/reactive.ts |
SSR stubs of the reactive runtime. |
Server createComponent, mergeProps, splitProps, For, Index, Show, Switch, Match, ErrorBoundary, Suspense, SuspenseList, createResource, resetErrorBoundaries, enableScheduling, enableHydration, startTransition, useTransition, createUniqueId, lazy, sharedConfig |
packages/solid/src/server/rendering.ts |
SSR control flow + utilities. |
Dynamic<T>(props) |
packages/solid/web/server/index.ts |
SSR version of <Dynamic>. Calls the function component or ssrElement for string components. |
Portal(props) |
packages/solid/web/server/index.ts |
Server stub: returns "". |
mergeProps (SSR override) |
packages/solid/web/server/index.ts |
Re-exports mergeProps from solid-js to override the dom-expressions server-side default. |
isServer = true / isDev = false |
packages/solid/web/server/index.ts |
Compile-time flags inverted from the browser entry. |
How it works
The server reactive system
packages/solid/src/server/reactive.ts is a parallel implementation of the reactive runtime that exists to make component code run once on the server without the bookkeeping of a tracked graph. The shape of the API is the same as the browser:
export function createSignal<T>(value: T, options?): [() => T, (v) => T] {
return [
() => value,
(v) => (value = typeof v === 'function' ? (v as (prev: T) => T)(value) : v),
];
}The getter is just a closure over value. There are no observers, no listener bookkeeping, no scheduler. createEffect, createComputed, and createRenderEffect each run the function once, capture errors via castError, and return.
Owner and cleanups are still tracked because cleanup runs at the end of renderToString. createRoot allocates an owner; cleanNode walks the tree at the end.
This means: server-side components must not rely on reactive propagation. They run top-to-bottom, observe whatever resource state happens to be available at that moment, and produce a string.
createResource on the server
createResource (packages/solid/src/server/rendering.ts) on the server has two modes selected by the ssrLoadFrom option:
"server"(default) — the fetcher runs immediately. The resultingPromise<T>is registered withsharedConfigso thatrenderToStringAsync/renderToStreamknow to wait for it. The resolved value is also collected for serialization to the client."initial"— the resource starts in the unresolved state and the client is responsible for fetching.
renderToStringAsync collects all promises returned by resources and await Promise.all(...)s them before producing the final string. renderToStream is more interesting: it emits <!--$--> placeholders for unresolved promises, flushes the shell, and writes additional <script> payloads to the stream as each promise resolves. The serialization format is provided by seroval and seroval-plugins/web.
Suspense and streaming
The server-side <Suspense> (packages/solid/src/server/rendering.ts) tracks pending resources via a SuspenseContext-like struct:
sequenceDiagram
participant SSR as renderToStream
participant Susp as <Suspense>
participant Res as createResource
participant Client as Browser
SSR->>Susp: render children
Susp->>Res: read() while pending
Res-->>Susp: throws to suspense, registers promise
Susp-->>SSR: render fallback into shell
SSR->>Client: flush shell HTML + hydration script
Note over SSR,Client: Connection stays open
Res->>Susp: promise resolves with value
Susp->>SSR: render children with resolved data
SSR->>Client: write seroval-serialized patch script
Client->>Client: replaces fallback in DOMThe 1.8 changelog (CHANGELOG.md) describes the move from bespoke promise serialization to generic seroval deduplication, which is what makes nested promises and de-duplicated resource data across Suspense boundaries work.
Control flow on the server
packages/solid/src/server/rendering.ts implements server versions of For, Index, Show, Switch, Match, ErrorBoundary, and lazy. They are simpler than the browser versions:
ForandIndexuse a singlesimpleMaphelper that just iterates synchronously and returns an array of strings.Showreturns either the children (resolving any function-as-children form) or the fallback.Switchwalks its<Match>children and returns the first whosewhenis truthy.ErrorBoundaryruns the children inside a try/catch and substitutes the fallback on error.lazy(loader)returns a component that throws the load promise so the surrounding<Suspense>waits for it.
Hydration ids
Both the browser createComponent (in packages/solid/src/render/component.ts) and the server createComponent (in packages/solid/src/server/rendering.ts) advance sharedConfig.context.id in lockstep. The base id starts as the request's hydration key (set by renderToStringAsync / renderToStream); each nested component appends a digit. The server emits these ids on <!--$id--> markers and as attribute values; the client hydrate reads them to find the matching nodes. createUniqueId() (packages/solid/src/server/rendering.ts) calls sharedConfig.getNextContextId() and returns the result.
The full id-encoding logic lives in getContextId (in packages/solid/src/render/hydration.ts and the mirror in packages/solid/src/server/rendering.ts):
function getContextId(count: number) {
const num = String(count),
len = num.length - 1;
return (
sharedConfig.context!.id + (len ? String.fromCharCode(96 + len) : '') + num
);
}The String.fromCharCode(96 + len) encodes the digit count as a letter (a, b, …) so single-digit counters do not get a prefix. This compact encoding keeps the rendered HTML small.
Key source files
| File | Purpose |
|---|---|
packages/solid/web/server/index.ts |
The public solid-js/web server entry. |
packages/solid/src/server/index.ts |
Re-export hub for the server reactive + rendering system. |
packages/solid/src/server/reactive.ts |
Stub implementations of the reactive primitives for server use. |
packages/solid/src/server/rendering.ts |
createComponent, mergeProps, splitProps, control-flow components, createResource, lazy, escape, resolveSSRNode, Suspense. |
packages/solid/web/src/server-mock.ts |
The browser-side stubs that error if SSR APIs are called from a browser-condition build (NOT the real implementation). |
Integration points
- The compiler emits
solid-js/webimports without knowing whether the consumer will resolve to the browser or server build — the export conditions inpackages/solid/web/package.jsondecide. solid-ssr(packages/solid-ssr/) consumesrenderToStringAsyncto power its static-site-generation runner. Seesolid-ssr.solid-js/web/storage(packages/solid/web/storage/src/index.ts) wraps Node'sAsyncLocalStorageso thatprovideRequestEventcan be called inside SSR without explicit context propagation. Seesolid-js/web/storage.- The
dom-expressionspackage ownsrenderToString,renderToStringAsync,renderToStream,ssr,ssrElement, etc. The SSR entry re-exports them viaexport * from "./server"inpackages/solid/web/server/index.ts.
Entry points for modification
- Adding a new SSR-only escape helper: add it to
packages/solid/src/server/rendering.tsand add the corresponding@deprecatedbrowser stub topackages/solid/web/src/server-mock.tsif it should be unsafe to call client-side. - Tweaking streaming behaviour: the renderer pipeline lives in
dom-expressions(workspace devDependency). Solid's job is to keep<Suspense>andcreateResourceintegrated withsharedConfig.load/sharedConfig.gather. Look atSuspenseinpackages/solid/src/server/rendering.tsand theserovalintegration in the workspacedom-expressions/src/server.js. - Adding a new SSR control-flow tag: mirror the browser version in
packages/solid/src/server/rendering.ts, keep the type signature identical so consumers can rely onComponent<P>working in both contexts.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.