← Back to DevBytes

Server-Side Rendering with SolidJS: SSR, SSG, ISR

Server-Side Rendering with SolidJS: SSR, SSG, ISR

SolidJS has earned a reputation as one of the fastest reactive UI libraries thanks to its fine-grained reactivity model. But raw client-side rendering isn't always enough. For SEO, perceived performance, and time-to-first-byte, you need rendering strategies that produce HTML on the server. This tutorial walks through the three main server-rendering approaches in the SolidJS ecosystem: Server-Side Rendering (SSR), Static Site Generation (SSG), and Incremental Static Regeneration (ISR).

Why Server Rendering Matters

By default, a SolidJS app ships a small JavaScript bundle that hydrates an empty HTML shell in the browser. This works for dashboards and internal tools, but it falls short when:

SolidJS solves this with @solidjs/server and the meta-framework @solidjs/start, which is built on Vinxi and Nitro. Together they let you choose a rendering strategy per route.

Setting Up a SolidStart Project

The easiest way to access SSR, SSG, and ISR is through SolidStart. Create a new project with:

npm create solid@latest my-app
cd my-app
npm install

During setup, choose the "basic" template and the Nitro-powered server. Your app.config.ts file is where you configure rendering behavior globally.

import { defineConfig } from "@solidjs/start/config";

export default defineConfig({
  ssr: true,
  nitro: {
    preset: "node-server",
  },
});

With ssr: true, every route renders on the server by default. From here, you can override behavior per route.

Server-Side Rendering (SSR)

SSR means the server generates fresh HTML on every request. This is ideal for pages with frequently changing data or user-specific content. In SolidStart, SSR is the default behavior when ssr is enabled.

Creating an SSR Route

Create a route file under src/routes. SolidStart uses file-based routing, so src/routes/products.tsx maps to /products.

import { createResource, For } from "solid-js";

type Product = { id: number; name: string; price: number };

async function fetchProducts(): Promise<Product[]> {
  const res = await fetch("https://api.example.com/products");
  return res.json();
}

export default function Products() {
  const [products] = createResource(fetchProducts);

  return (
    <main>
      <h1>Products</h1>
      <ul>
        <For each={products()}>
          {(product) => (
            <li>
              {product.name} — ${product.price}
            </li>
          )}
        </For>
      </ul>
    </main>
  );
}

When a request hits /products, the server runs the component, awaits the resource, serializes the result, and streams the HTML to the browser. On the client, Solid hydrates the existing DOM instead of rebuilding it.

Server Functions for Secure Data Fetching

Fetching directly from the client exposes API endpoints and keys. SolidStart lets you define server-only functions with the "use server" directive.

// src/routes/products.tsx
"use server";

import { createResource } from "solid-js";

async function getProducts(): Promise<Product[]> {
  "use server";
  // This code runs only on the server
  const res = await fetch("https://api.example.com/products", {
    headers: { Authorization: `Bearer ${process.env.API_KEY}` },
  });
  return res.json();
}

export default function Products() {
  const [products] = createResource(getProducts);
  return (
    <ul>
      {products()?.map((p) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  );
}

The server function is automatically serialized and called over an RPC endpoint. The client never sees the API key.

Static Site Generation (SSG)

SSG pre-renders pages at build time. The output is plain HTML files you can serve from any CDN. This gives you the best possible performance for content that rarely changes: blog posts, marketing pages, documentation.

Enabling SSG for a Route

In SolidStart, you can prerender specific routes using the prerender option in your route module's routeData or via the global config. The simplest approach is global prerendering in app.config.ts:

import { defineConfig } from "@solidjs/start/config";

export default defineConfig({
  ssr: true,
  nitro: {
    prerender: {
      crawlLinks: true,
      routes: ["/", "/about", "/blog"],
    },
  },
});

With crawlLinks: true, Nitro starts at the listed routes and follows internal links to discover more pages. Each discovered page is rendered to a static HTML file in the .output/public directory.

Dynamic SSG with Route Parameters

For dynamic routes like /blog/[slug], you need to tell the prerenderer which slugs exist. Use the getStaticPaths pattern by exporting a route data function:

// src/routes/blog/[slug].tsx
import { useParams } from "@solidjs/router";

export const routeData = async () => {
  "use server";
  const posts = await fetch("https://api.example.com/posts").then((r) =>
    r.json()
  );
  return posts;
};

export default function BlogPost() {
  const post = useParams();
  return <article><h1>{post.slug}</h1></article>;
}

Then list the dynamic paths explicitly in the config:

nitro: {
  prerender: {
    crawlLinks: true,
    routes: ["/blog"],
  },
}

Nitro crawls from /blog, finds each post link, and renders every /blog/[slug] page at build time.

Deploying SSG Output

After running npm run build, the static output lives in .output/public. Deploy it to any static host:

npm run build
npx serve .output/public

Common targets include Vercel, Netlify, Cloudflare Pages, GitHub Pages, and S3 with CloudFront. No Node server is required.

Incremental Static Regeneration (ISR)

ISR combines the speed of SSG with the freshness of SSR. Pages are generated statically, but the server can regenerate them in the background when a request comes in after a specified interval. This is perfect for content that updates periodically but doesn't need to be real-time: product catalogs, news homepages, leaderboards.

How ISR Works in SolidStart

SolidStart's Nitro layer supports ISR through route rules. You define a swr (stale-while-revalidate) time in seconds. The first request serves the cached static page. After the interval expires, the next request triggers a background regeneration while still serving the stale version immediately.

import { defineConfig } from "@solidjs/start/config";

export default defineConfig({
  ssr: true,
  nitro: {
    routeRules: {
      "/products/**": { swr: 60 },        // regenerate every 60 seconds
      "/blog/**": { prerender: true },    // fully static
      "/dashboard/**": { ssr: true },     // always server-rendered
    },
  },
});

This configuration gives you three strategies in one app: ISR for products, SSG for the blog, and SSR for the dashboard.

ISR with On-Demand Revalidation

Sometimes you want to regenerate a page immediately when data changes, rather than waiting for the interval. Nitro supports this through cache invalidation endpoints. Create a server route that clears the cache for a specific path:

// src/routes/api/revalidate.ts
export default defineEventHandler(async (event) => {
  const query = getQuery(event);
  const path = query.path as string;

  if (!path) {
    throw createError({ statusCode: 400, message: "Path required" });
  }

  // Purge the cached page
  await useStorage("cache").remove(`nitro:routes:${path}`);
  return { revalidated: true, path };
});

When your CMS publishes a new article, it can POST to /api/revalidate?path=/blog/my-post to trigger an immediate regeneration. The next visitor gets the fresh content.

Streaming and Suspense

One of SolidJS's standout SSR features is streaming. Instead of waiting for all data to load before sending any HTML, Solid streams the shell immediately and fills in async content as it resolves. Wrap slow sections in <Suspense>:

import { Suspense } from "solid-js";

export default function Page() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<p>Loading stats...</p>}>
        <Stats />
      </Suspense>
      <Suspense fallback={<p>Loading activity...</p>}>
        <Activity />
      </Suspense>
    </div>
  );
}

The browser receives the heading and fallbacks instantly. As each resource resolves on the server, Solid streams the replacement HTML into the document. This dramatically improves perceived performance.

Best Practices

Conclusion

SolidJS gives you a flexible, high-performance toolkit for server rendering. SSR handles dynamic, user-specific content with fresh HTML on every request. SSG pre-renders static pages at build time for maximum speed and deployability. ISR bridges the gap by regenerating static pages in the background on a schedule or on demand. With SolidStart and Nitro, you can mix all three strategies in a single application, choosing the right approach for each route. By understanding the tradeoffs and applying best practices around caching, streaming, and server functions, you can build applications that are fast for users, friendly to search engines, and maintainable for developers.

— Ad —

Google AdSense will appear here after approval

← Back to all articles