Velox: What If React Had a Lighter Sibling?
A small, readable React framework on plain Vite, still in progress, and the lessons it is teaching me.

Next.js is a remarkable piece of engineering, and much of my work is built on it, including this portfolio. But full-stack React has picked up a lot of moving parts along the way: server components, compiler transforms, several layers of caching that are not always easy to see into. One day I got curious. What if React had a lighter sibling? Something with the parts I actually reach for, server rendering, data loading and caching, but small and plain enough to read from top to bottom.
That curiosity became Velox, a small, readable React framework built on plain Vite. It is still a work in progress, and I fully intend to finish it. This article is about the idea, what already works, and what building it has taught me so far.
The idea: nothing hidden
Velox keeps every piece explicit:
- Plain Vite for both development and production builds. No custom bundler and no compiler magic.
- Explicit
loader()functions for data, one per page. - One cache you can inspect and purge, instead of several hidden ones.
- A standard Node.js server.
- No React Server Components. Pages render on the server, then hydrate in the browser, the model most React developers already understand.
The goal is not to compete with Next.js on features. It is to show how much of what people need fits in something small enough to understand completely.
What already works
Routing from files
Pages are files in src/pages. Square brackets make dynamic routes, such as [slug] and [...rest], and a _layout.tsx in any folder wraps every page below it. The nearest _error.tsx renders when something fails. API routes live alongside pages.
Loaders, streaming and deferred data
Each page can export a loader that runs on the server. Pages stream by default using React's streaming renderer, so the browser gets the page shell as soon as it is ready. A slow field can be returned as a promise without waiting for it, and it streams in when it resolves:
export async function loader({ params }: LoaderContext) {
return {
post: await getPost(params.slug), // needed for the first paint
comments: getComments(params.slug), // streams in later
};
}
export default function Post({ post, comments }: PageProps<typeof loader>) {
return (
<>
<h1>{post.title}</h1>
<Await promise={comments} fallback={<p>Loading comments...</p>}>
{(list) => list.map((c) => <p key={c.id}>{c.body}</p>)}
</Await>
</>
);
}
Loaders never reach the browser. When Velox builds the client bundle, it removes the loader, along with every import and helper that only the loader used, so database clients and secrets stay on the server.
Caching you can see
A page opts into incremental static regeneration with a single line:
export const revalidate = 60;
The first request renders and caches the full HTML. Requests within the window are served from memory, and after it, visitors get the old copy while a fresh one renders in the background. Every response says which happened through an X-Cache header (MISS, HIT or STALE), and a single endpoint purges a path on demand. You can always tell what the cache is doing.
Server functions
Mutations are written once, in any *.server.ts file, and wrapped in serverFn(). A component can then call them like a normal typed function, or a plain HTML form can post to them and work without any JavaScript. Each one can validate its input before the handler runs.
export const addComment = serverFn(
async ({ slug, text }: { slug: string; text: string }) => {
await db.comments.insert({ slug, text });
return { ok: true };
}
);
Type-safe links
Velox generates route types from your pages, so a typo in a link's address is a compile error, not a broken link in production.
What building a framework teaches you
The most valuable part of Velox so far has been finding out why frameworks make the choices they do. Some of those lessons came from bugs I am glad I caught early.
Caching can leak data between users
This was the most important one. In an early version, an ISR page's loader still received the incoming request. That sounds harmless, until you realise a loader that read cookies could render one visitor's personal page, have it cached, and then serve it to everyone else. Cached HTML is shared by all visitors, so now ISR loaders get no request at all, and per-user data has to live on pages without caching. It is easy to read about this kind of bug. Designing a cache myself is what made me truly understand it.
Caches need limits
An unlimited cache keyed by URL means anyone can grow your server's memory by requesting random addresses. The cache is now capped at 1,000 entries, and only successful responses are ever cached, never errors or 404 pages.
Server endpoints are an attack surface
The endpoint that runs server functions only runs functions that were explicitly registered through serverFn() in a server file. It rejects cross-site requests to block CSRF, caps request bodies at 1 MB, and only accepts JSON and form data. None of that is glamorous, and all of it is necessary.
Hydration is subtle
For a while, the browser's first render produced nothing, so React threw away the server's HTML and rebuilt the page from scratch. The page still appeared, but the whole point of server rendering was quietly lost. The fix was making sure the page is fully loaded before hydration starts. It taught me to check that the server's work is actually being reused, not just that the page appears.
Where it is going
Velox is usable today, but it has not had its first release yet, and while it is below version 1.0, its API may still change. The cache lives in process memory, so it is per server instance for now. I intend to finish it: complete the core, stabilize the API and publish a first release. Lcore went the same way, from curiosity to running in production.
Velox and Lcore come from the same impulse: understanding the tools I use every day by building a small, honest version of one myself.
Try it
Clone the repository and run the demo app:
pnpm install
pnpm dev:demo # development, with hot reload and SSR
pnpm build:demo # production build
pnpm start:demo
The demo runs at http://localhost:3000. Guides for routing, data loading, server functions and type-safe routes are in the docs/ folder. The source code is on GitHub at github.com/Lusan-sapkota/velox, under the MIT license.