River is a framework for writing modern, type-safe web applications with Go and TypeScript. River provides the benefits of modern web development frameworks without the downsides of isomorphic JavaScript and component hydration patterns.
Core Features:
- Full Vite integration
- Blazing fast TypeScript type generation
- Hybrid-SSR architecture
- React, Preact, and Solid support
Optional Extras:
- Global critical CSS inlining
- Link pre-fetching on hover
- Ability to deeply integrate the build and dev refresh cycle with any upstream dependencies in any language, via our Kiruna build helper
While River currently supports React, Preact, and Solid, 95% of River's client architecture just uses vanilla browser APIs, making it pretty trivial to support any client UI library while still providing the same underlying development model. Even our Link components are light wrappers over a vanilla core.
Our core philosophy is simple: You don't need backend JS to build a fully modern and performant web application.
Yes, it is possible to great performance and SEO scores, a fully type-safe application, and a frictionless full-stack developer experience, all without backend JS.
Now, River doesn't think that all backend JS is bad per se. TypeScript in particular is a wonderfully dynamic language, and it can be great for writing many backend services. But what we do reject is the modern myth that you need to use a full SSR and hydration or component streaming model to achieve the requirements of a modern full-stack web application. It just doesn't need to be that complex.
Now, let me explain the hybrid-SSR architecture and how it recreates the benefits of traditional full SSR without technically doing full SSR.
It's quite simple really. The hybrid-SSR architecture has four ingredients:
- Fully populated document head with all necessary and appropriate tags and data, generated dynamically on a per-route basis.
- Pre-loaded route data immediately available for your client components to consume.
- Properly hashed, immutably edge-cached, and parallel-loaded static dependencies.
- Client-rendered document body inside the dynamic outer shell (ready to execute and paint almost immediately thanks to items 1-3 above).
The fourth item (client-rendered document body) is what will make many of you skeptical about this architecture. That's OK! We welcome skepticism.
We feel that once you try it out and deploy an app or two using the River hybrid-SSR architecture, you'll realize that as long as you have items 1-3 in place, item 4 is not actually a real-world problem. We think you'll be reminded that client-side rendering is really nice, actually (especially when it sits on top of the dynamic scaffolding you get with River). It greatly simplifies your application architecture, frees you up to use cheaper and more performant backend languages, and rids you of those annoying hydration errors. Win-win-win-win-win. You get the actual benefits of full SSR without the downsides.
OK, so we have covered initial page loads. What about subsequent navigations? Well, we just do the same thing, except with a smaller JSON payload.
We know all your dependencies (including both JS and CSS) at a route level, so it's trivial to preload it all and block navigations for a few milliseconds until everything is loaded and ready. And if you choose to use link pre-fetching on hover, the route transition will feel instant for anyone with a decent internet connection.
Additionally, all navigations are performed using a single request. No waterfalls or jank. No request-per-route-segment model. Just a fast navigation that feels solid and familiar to users.
To sum up, River provides many benefits over popular full-SSR frameworks. Faster, cheaper server. Smaller all-around payloads. No hydration errors. No subtle JS runtime incompatabilities. Yet it also provides many of the same benefits, just using a different architecture. Excellent SEO. Wonderful and blazing fast user experience. Fully type-safe and frictionless developer experience. All yours for the taking.
go get github.com/sjc5/river
npm i -D @sjc5/river
Why a dev dependency? Because it is more explicit about what is actually happening. The normal/dev dependency distinction in your package.json is a NodeJS concept, not a browser concept. River is both targeted at browsers (not NodeJS) and is shipped as pure TypeScript source (meaning you must have a dev-time build step to prepare the code for browser consumption). By convention, and for explicitness, River projects will usually declare all dependencies as dev dependencies to make it clear that they are being transformed before being shipped to the browser.