Open-Source Wikis

/

Solid

/

Packages

/

solid-js/web (server entry) and SSR

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 override

The 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 resulting Promise<T> is registered with sharedConfig so that renderToStringAsync / renderToStream know 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 DOM

The 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:

  • For and Index use a single simpleMap helper that just iterates synchronously and returns an array of strings.
  • Show returns either the children (resolving any function-as-children form) or the fallback.
  • Switch walks its <Match> children and returns the first whose when is truthy.
  • ErrorBoundary runs 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/web imports without knowing whether the consumer will resolve to the browser or server build — the export conditions in packages/solid/web/package.json decide.
  • solid-ssr (packages/solid-ssr/) consumes renderToStringAsync to power its static-site-generation runner. See solid-ssr.
  • solid-js/web/storage (packages/solid/web/storage/src/index.ts) wraps Node's AsyncLocalStorage so that provideRequestEvent can be called inside SSR without explicit context propagation. See solid-js/web/storage.
  • The dom-expressions package owns renderToString, renderToStringAsync, renderToStream, ssr, ssrElement, etc. The SSR entry re-exports them via export * from "./server" in packages/solid/web/server/index.ts.

Entry points for modification

  • Adding a new SSR-only escape helper: add it to packages/solid/src/server/rendering.ts and add the corresponding @deprecated browser stub to packages/solid/web/src/server-mock.ts if 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> and createResource integrated with sharedConfig.load / sharedConfig.gather. Look at Suspense in packages/solid/src/server/rendering.ts and the seroval integration in the workspace dom-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 on Component<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.

solid-js/web (server entry) and SSR – Solid wiki | Factory