npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tinloof/tanstack-wsr

v0.2.0

Published

Worker-Side Rendering (WSR) for TanStack Start: render selected routes inside a service worker — instant, offline, zero-flash document loads.

Readme

@tinloof/tanstack-wsr

Render TanStack Start routes inside a service worker with just one route flag:

export const Route = createFileRoute("/dashboard")({
  wsr: true, // ← rendered on-device by the service worker
  loader: () => readLocalData(),
  component: Dashboard,
});

Example

examples/recipes — a local-first recipes app

Demo link

Install

npm i @tinloof/tanstack-wsr

Setup

1 — Add the vite plugin (generates /sw.js from your getRouter; no worker file to write):

// vite.config.ts
import { tanstackWsr } from "@tinloof/tanstack-wsr/vite";

export default defineConfig({
  plugins: [tanstackWsr(), tanstackStart()],
});

2 — Register the generated service worker once in your root document:

// src/routes/__root.tsx
import { WsrRegister } from "@tinloof/tanstack-wsr/react";

// inside <body>
<WsrRegister hot={import.meta.hot} />;

3 — Let getRouter take a history (the worker seeds one per request):

export function getRouter(history?: RouterHistory) {
  return createRouter({ routeTree, ...(history ? { history } : {}) });
}

Then flag any route with wsr: true. Done.

Marking routes

wsr is a normal route option, so it works on any eager route API — file-based or code-based, including the root:

// code-based child route
const dashboard = createRoute({
  getParentRoute: () => layoutRoute,
  path: "/dashboard",
  wsr: true,
});

// the root route — opts the whole app in
const rootRoute = createRootRoute({ wsr: true });

Use an eager definition — wsr is ignored in .lazy files (createLazyFileRoute/createLazyRoute), since the gate reads it before the lazy chunk loads.

Nested routes

A document load renders its whole matched chain (root → layouts → leaf) in one environment, so wsr inherits down the tree: marking a route opts in that route and everything under it. Given this tree:

root
├─ /                    (index)
├─ /posts               (parent / layout)
│  ├─ /posts/           (index)
│  └─ /posts/$id
└─ /settings            (layout)
   └─ /settings/profile

each wsr: true placement opts in these pages (✓ = rendered in the worker, – = served by the origin):

| wsr: true on… | / | /posts | /posts/$id | /settings/profile | | -------------------- | :-: | :------: | :----------: | :-----------------: | | root | ✓ | ✓ | ✓ | ✓ | | /posts (parent) | – | ✓ | ✓ | – | | /posts/ (index) | – | ✓ | – | – | | /posts/$id | – | – | ✓ | – | | /settings (layout) | – | – | – | ✓ |

Note the /posts/ index row: it's a leaf sibling of /posts/$id, so marking it opts in only the /posts page, not the detail pages — mark the index (not the parent) to scope wsr to just that page. Inheritance has no opt-out: once a parent or the root is marked, you can't exclude a descendant — mark the specific routes you want instead.

Two consequences:

  • Ancestors run in the worker. When a chain is worker-rendered, every ancestor's loader and beforeLoad runs there — keep them worker-safe (createServerFn is fine, it becomes an RPC).
  • 404s follow the root. Unmatched paths render in the worker only if the root is marked; otherwise they fall through to the origin.

Worker routes

The mirror of TanStack server routes, but the handlers run in the service worker. Declare them with the worker route option — the route's path is the identity (no name, no codegen; the router is the dispatch table). This lets a single stateful client — a local-first / sync-engine client (Zero, Replicache, …) — live in the worker, with reads and writes both going through it. Because a wsr render reads that same client, a hard-load render reflects exactly what the user just did: one client, one store, no cross-client round-trip, no stale flash.

// src/routes/api/todos.ts
import { createFileRoute } from "@tanstack/react-router";
import { getClient } from "@/client"; // your single in-worker client

export const Route = createFileRoute("/api/todos")({
  worker: {
    handlers: {
      GET: async () => Response.json(await getClient().list()),
      POST: async ({ request }) => {
        await getClient().add(await request.json());
        return new Response(null, { status: 204 });
      },
    },
  },
  // optional: a `server` handler for the same method is the no-worker
  // (first-load / SSR) fallback — see TanStack server routes.
});

Call them with workerFetch(path, init), which resolves in the right place:

  • in the worker (e.g. a wsr route's loader during a hard load) → runs the matched handler directly (a worker can't fetch itself);
  • on the main thread (client navigation, event handlers) → fetches the path, which the worker intercepts — the same path it renders on;
  • on the origin server (SSR, no worker yet) → a 503, so the caller can fall back (e.g. render an empty shell until the worker takes over).
import { workerFetch } from "@tinloof/tanstack-wsr/worker-fetch";

export const Route = createFileRoute("/")({
  wsr: true,
  loader: async () => {
    const res = await workerFetch("/api/todos"); // worker on hard load, fetch on nav
    return { todos: res.ok ? await res.json() : [] };
  },
  component: Todos,
});

// a mutation, from an event handler
await workerFetch("/api/todos", { method: "POST", body: JSON.stringify({ title }) });

Notes:

  • Handlers are keyed by HTTP method (like server routes); model operations RESTfully (POST/PATCH/DELETE) or put an action in the body.
  • The worker must be registered and controlling the page for main-thread calls (it is, right after it renders a document).
  • To push live updates to pages (e.g. when synced data changes), call broadcastToClients(message) from a handler and have pages listen on navigator.serviceWorker to router.invalidate().

API

  • @tinloof/tanstack-wsr/vitetanstackWsr({ router?, entry? }) — the Vite plugin (router defaults to ./src/router, must export getRouter).
  • @tinloof/tanstack-wsr/react<WsrRegister hot? /> — registers /sw.js; pass import.meta.hot for the dev auto-update bridge.
  • @tinloof/tanstack-wsr/worker-fetchworkerFetch(path, init) and broadcastToClients(message); the worker route option declares the handlers — see Worker routes.

How it works

One route tree, one router. The service worker only acts on a document (hard) load; in-app navigation stays a normal SPA transition.

 Hard load / reload / offline open
        │
        ▼
 ┌───────────────┐   any route in the matched chain has `wsr: true`?
 │ service worker│──────────────┬──────────────────────────┐
 └───────────────┘              │ yes                       │ no
                                ▼                            ▼
                  run the loaders + render         fetch(request) →
                  in-worker → return HTML →           origin handles it
                  hydrate via Start's bundle

 In-app navigation (<Link> / history) → the client router renders it;
 the service worker is not involved.

The gate is loader-free (pure path match) and chain-aware — if any route in the matched chain is wsr (see Nested routes), the whole document renders in the worker; otherwise the request passes through untouched, so a non-wsr route's loader never runs there. wsr pages reuse Start's client entry for hydration — no extra bundle.

The worker bundle is compiled with TanStack Start's own client transform, so it's a browser-grade environment: createServerFn becomes a network RPC (its body and secrets stay on the origin, never in /sw.js), and you can call it from a wsr loader or an event handler just like anywhere else in your app.

Edits update the worker automatically in dev. Service workers can't be hot-swapped, so the plugin rebuilds /sw.js on every request and watches your source; on save it signals the page through the import.meta.hot you pass to <WsrRegister>, which pulls the new worker — it installs, takes control, and the page reloads with it. Editing a wsr route or loader just shows up, with no manual service-worker unregistering.

Contributing

See CONTRIBUTING.md — dev setup and how releases are published to npm.

License

MIT