Waku's RSC Default: One Interactive Island in a Static Blog Post
Waku makes React Server Components the default, so only components marked 'use client' ship JavaScript. A worked blog example, the trade-offs, and how to verify the bundle.
06 Feb 2026, 16:04 UTC

The cost of shipping JavaScript for text that never changes
A typical React blog post is mostly static prose. In a client-rendered app, the browser still downloads the component tree, parses it, and re-runs it to attach event handlers before the page becomes interactive. That work — hydration — is paid even when the only interactive element is a single like button.
Waku's answer is an architectural default rather than a library: React Server Components (RSC) are the norm, and client interactivity is opt-in. A server component runs only on the server; it can be async, read files or query a database directly, and its code is never sent to the browser. A client component is marked with the 'use client' directive at the top of the file and is the only kind of component whose JavaScript is bundled for the browser.
That inverts the usual question. Instead of asking "how do I make this page server-rendered?", you ask "which parts of this page actually need JavaScript?"
File-system routing with server components as the default
Routes come from files on disk: a file under the pages directory maps to a URL path, and nested layout files compose around the page. Because the default is a server component, a route file can be an async function that awaits data before returning JSX — no useEffect, no client-side fetching library, and no loading state for the initial render.
Waku also supports prerendering routes at build time. A content site can prebuild its article pages while leaving, for example, a search route to be rendered on the server per request.
Worked example: one interactive island in a static post
The example below assumes a project already scaffolded with Waku's official create command and a pages/ directory. Treat file names and config as version-dependent — see the caveats at the end.
The client island
// components/LikeButton.tsx
'use client';
import { useState } from 'react';
export function LikeButton({ initialLikes }) {
const [likes, setLikes] = useState(initialLikes);
return (
<button onClick={() => setLikes(likes + 1)}>
Likes: {likes}
</button>
);
}
The server page
// pages/posts/[slug].tsx
import { LikeButton } from '../../components/LikeButton';
import { getPostBySlug } from '../../lib/posts';
export default async function PostPage({ params }) {
const post = await getPostBySlug(params.slug);
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.html }} />
<LikeButton initialLikes={post.likes} />
</article>
);
}
The page component itself never reaches the browser as JavaScript. Only LikeButton and its dependencies do. Add a second interactive widget and you add a second island — the client cost scales with interactivity, not with page length.
What you give up
Waku is deliberately small, and that shows up as missing conveniences rather than missing correctness:
- No built-in mutation layer. There is no opinionated server-action equivalent for form submissions and writes; you wire up your own endpoints or integration pattern.
- No rich caching API. Static prerendering exists, but a framework-level cache-and-revalidate system does not, so revalidation is your design decision.
- Smaller ecosystem. Fewer plugins, deployment adapters, and community answers than Next.js, so expect to solve some integration problems yourself.
If your site is mostly dynamic, per-user, and mutation-heavy, the RSC-first default buys you less and the missing conveniences cost you more.
How to check the result yourself
- Build and start in production mode (for example
npm run buildthennpm run start). - Open the browser Network tab, filter by JS, and reload. Confirm the page component's logic is not among the downloaded scripts and that only the island's bundle appears.
- View the HTML source. The article text should be present as markup, not an empty container waiting to be filled.
- Read the build output to see which routes were prerendered and which are server-rendered per request.
- Pin exact dependency versions, then re-run the build after any upgrade to catch changes in routing or config conventions.
Caveats worth checking before you commit
The name "Waku" is ambiguous: it also refers to a libp2p-based messaging protocol family. Search results and package discussions mix the two, so confirm you are reading about the React framework.
Waku is a young, fast-moving project. Config file names and routing conventions have changed between releases, and RSC support in React and bundlers continues to evolve, which can surface as breaking changes in minor versions. Verify the current file layout, config, and create command against the project's own documentation for the version you pin rather than relying on older tutorials or on this example's exact paths.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.