React Server Components + Streaming: A Custom Setup with Webpack & Express

Introduction
React Server Components (RSC) let you handle rendering on the server, avoiding the need to send extra JavaScript to the client. When used with streaming (renderToPipeableStream
), RSC allows pages to load faster by delivering HTML to the browser as soon as it's ready.
In this post, we'll create a custom RSC + streaming setup using React 19, Webpack 5, and Express—all without using Next.js. This approach offers maximum flexibility and helps us better understand how everything works together.
Prerequisites
React 19+
Node.js 18+
Basic knowledge of Webpack and Express
Babel for converting modern JavaScript and JSX
Folder Structure
src/
├── server/
│ └── index.js # Express server with RSC stream
├── client/
│ └── index.js # Hydrate app on client
├── components/
│ ├── App.server.js # Server-only component
│ └── Content.client.js # Client interactive component
webpack.client.js
webpack.server.js
Installation
npm install react react-dom express
npm install --save-dev webpack webpack-cli babel-loader @babel/core @babel/preset-env @babel/preset-react webpack-node-externals
Webpack Configs
webpack.client.js
experiments: {
topLevelAwait: true
}
webpack.server.js
target: 'node',
externals: [nodeExternals()],
experiments: {
serverComponents: true,
topLevelAwait: true
}
Make sure you transpile .jsx?
files using Babel with both @babel/preset-env
and @babel/preset-react
.
Server-Side Streaming
const { renderToPipeableStream } = require('react-dom/server');
app.get('/', (req, res) => {
const { pipe } = renderToPipeableStream(
<App />,
{
bootstrapScripts: ['/client.js'],
onShellReady() {
res.setHeader('content-type', 'text/html');
pipe(res);
}
}
);
});
renderToPipeableStream
streams HTML progressively instead of waiting for the full tree to render. This reduces Time To First Byte (TTFB).
App Composition with RSC
App.server.js
import Content from './Content.client';
export default function App() {
return (
<html>
<head>
<title>RSC Streaming Demo</title>
</head>
<body>
<div id="app">
<h1>Welcome to RSC</h1>
<Content />
</div>
</body>
</html>
);
}
Content.client.js
'use client';
import { useState } from 'react';
export default function Content() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
Clicked {count} times
</button>
);
}
'use client'
tells React to hydrate this component on the client. This is how you keep interactivity while offloading everything else to the server.
Client Hydration
import { hydrateRoot } from 'react-dom/client';
import App from '../components/App.client';
hydrateRoot(document.getElementById('app'), <App />);
Challenges Faced
Webpack doesn’t yet have official RSC support; requires custom tuning
.client.js
/.server.js
conventions must be manually followedDebugging streaming with Express and catching hydration issues
Results
Server Components rendered and streamed efficiently
Interactivity layered only where needed
Zero runtime JS for static content
What’s Next
Add Suspense boundaries for asynchronous loading
Explore
React.lazy
with streamingUse caching or fetch-on-server patterns
Move setup to edge runtimes like Cloudflare Workers
Conclusion
This setup proves that React Server Components and streaming can work without frameworks like Next.js. It gives you full control, deeper understanding, and a highly optimized frontend architecture.
If you're experimenting with server-first React, this is your launchpad.
Happy streaming!
Subscribe to my newsletter
Read articles from radhakrishnan gopal directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
