JSX Engine & Hydration Architecture
A comprehensive guide to understanding how Harpy.js renders React components server-side, integrates with Fastify, and automatically hydrates client components with optimistic chunking for maximum performance.
Architecture Overview
Visual Architecture Flow
This diagram illustrates the complete request-response cycle, from browser request through Fastify, NestJS, React SSR, hydration chunk generation, and back to the browser with interactive components.
The Complete Flow
// Core JSX Engine FlowRequest → Fastify → NestJS Controller → @JsxRender Decorator ↓JSX Engine receives: - Component to render - Props from controller - Layout (optional) ↓Server-side rendering with React: - renderToString() for HTML - Detects client components ('use client') - Generates hydration metadata ↓HTML Response includes: - Rendered HTML - Hydration scripts - Component chunks - Props for hydrationHarpy.js implements a sophisticated JSX rendering engine that seamlessly integrates React Server-Side Rendering with NestJS controllers and Fastify's high-performance HTTP server. The engine intelligently distinguishes between server components (rendered once on the server) and client components (hydrated on the browser) to deliver optimal performance.
1-7ms Render Time
Highly optimized React SSR engine delivers HTML in milliseconds, ensuring instant First Contentful Paint.
Automatic Detection
Client components are automatically detected via 'use client' directive. No manual configuration needed.
Optimistic Chunking
Each client component gets its own chunk, enabling parallel loading and aggressive browser caching.
Seamless Hydration
Client components automatically hydrate without flickering, preserving server-rendered HTML perfectly.
How It Works: Step by Step
Request Handling with @JsxRender
When a request arrives, Fastify routes it to a NestJS controller. The @JsxRender decorator tells Harpy to render the specified component server-side with the returned props.
1import { Controller, Get } from '@nestjs/common';2import { JsxRender } from '@harpy-js/core';3import HomePage from './views/home-page';45@Controller()6export class HomeController {7 @Get()8 @JsxRender(HomePage)9 async home() {10 return {11 title: 'Welcome to Harpy.js',12 items: ['Fast', 'Flexible', 'Modern']13 };14 }15}Key Point: The controller returns plain data (props), and the decorator handles all the JSX rendering complexity automatically.
Server vs Client Components
Harpy distinguishes between two types of React components:
🖥️ Server Components
Rendered once on the server. Pure HTML sent to client. No JavaScript needed. Perfect for static content.
import React from 'react';import type { PageProps } from '@harpy-js/core';interface HomePageProps extends PageProps { title: string; items: string[];}export default function HomePage({ title, items }: HomePageProps) { return ( <div> <h1>{title}</h1> <ul> {items.map((item) => ( <li key={item}>{item}</li> ))} </ul> </div> );}⚡ Client Components
Require interactivity (hooks, events). Rendered on server, then hydrated on client. Marked with 'use client'.
'use client';import React, { useState } from 'react';export default function Counter() { const [count, setCount] = useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}> Increment </button> </div> );}Best Practice: Use server components by default. Only add 'use client' when you need interactivity (useState, useEffect, onClick, etc.). This minimizes JavaScript sent to the browser.
Server-Side Rendering Process
The JSX Engine uses React's renderToString() to generate HTML on the server. This happens in 1-7ms for most pages. The engine tracks which client components were used during rendering.
Rendering Steps:
- 1.Execute component function with props from controller
- 2.Recursively render children including layouts and nested components
- 3.Detect client components in the tree and mark them for hydration
- 4.Generate complete HTML string with proper structure
- 5.Inject hydration metadata and script tags for required chunks
1// Page.tsx - Server Component2import React from 'react';3import Counter from '../components/Counter'; // Client Component4import type { PageProps } from '@harpy-js/core';56interface HomeProps extends PageProps {7 initialCount: number;8 serverData: string;9}1011// This renders on the server12export default function HomePage({ initialCount, serverData }: HomeProps) {13 return (14 <div>15 {/* Static content - rendered once on server */}16 <h1>Welcome</h1>17 <p>Server data: {serverData}</p>18 19 {/* Client component - hydrates on client */}20 <Counter initialCount={initialCount} />21 22 {/* More static content */}23 <footer>© 2025</footer>24 </div>25 );26}Client-Side Hydration
After the HTML arrives and renders (instant FCP!), the hydration scripts execute to make client components interactive. This process is automatic and seamless.
// Automatic Hydration Process1. Build Time: - Scan all components for 'use client' directive - Create separate bundles for each client component - Generate hydration entry points - Build vendor chunk (shared dependencies)2. Server Render: - Render component tree to HTML string - Track which client components were used - Inject hydration metadata into HTML - Add <script> tags for required chunks3. Client Hydration: - Browser loads HTML (instant First Contentful Paint) - Hydration scripts execute - React.hydrateRoot() attaches event listeners - Client components become interactiveZero Flickering: Because the HTML is already correct from the server, there's no layout shift or content flickering during hydration. The page looks perfect immediately.
Optimistic Chunking Strategy
Harpy implements an "optimistic" code-splitting strategy where each client component gets its own JavaScript chunk. This approach optimizes for HTTP/2 parallel loading and aggressive browser caching.
// Optimistic Chunking Strategydist/ chunks/ vendor.js # Shared: React, ReactDOM, utilities Counter.js # Individual client component Modal.js # Individual client component Form.js # Individual client componentBenefits:✓ Parallel loading of component chunks✓ Aggressive browser caching per component✓ Only load what's actually rendered on page✓ Automatic code splitting without manual configuration✓ Minimal bundle size per pageExample Page Load: vendor.js (188kb) - cached across all pages Counter.js (2.3kb) - only if Counter is on page Modal.js (4.1kb) - only if Modal is on page Total: ~194kb for a page with Counter + ModalCompare: Traditional SPA might load 500kb+ upfront🚀 Parallel Loading
With HTTP/2, browsers load all chunks simultaneously. No waterfall delays.
💾 Smart Caching
Change one component, only that chunk invalidates. vendor.js cached forever.
📦 Minimal Size
Only load JavaScript for components actually rendered on the page.
Why Not Bundle Everything Together?
Traditional approaches bundle all JavaScript into one large file. This seems simpler, but has major downsides:
- ✗Large initial download (500kb+)
- ✗Cache invalidation on any change (users re-download everything)
- ✗Loads code for components not on the current page
- ✗Slower Time to Interactive
Fastify Integration
Harpy.js leverages Fastify's performance and extensibility to serve JSX-rendered content efficiently. The integration is seamless and requires minimal configuration.
1import { NestFactory } from '@nestjs/core';2import { FastifyAdapter } from '@nestjs/platform-fastify';3import { AppModule } from './app.module';4import { setupHarpyApp } from '@harpy-js/core';5import type { NestFastifyApplication } from '@nestjs/platform-fastify';6import DefaultLayout from './layouts/layout';7import Custom404Page from './error-pages/404';8import Custom500Page from './error-pages/500';910async function bootstrap() {11 // Create NestJS app with Fastify adapter12 const app = await NestFactory.create<NestFastifyApplication>(13 AppModule,14 new FastifyAdapter(),15 );1617 // setupHarpyApp configures everything:18 // - JSX rendering engine integration19 // - Static file serving for chunks20 // - Cookie handling21 // - Error page rendering22 // - Content-Type headers23 await setupHarpyApp(app, {24 layout: DefaultLayout, // Default layout wrapper25 distDir: 'dist', // Build output directory26 publicDir: 'public', // Static assets directory27 errorPages: {28 404: Custom404Page, // Custom 404 page component29 '500': Custom500Page, // Custom 500 error page component30 },31 });3233 await app.listen({34 port: 3000,35 host: '0.0.0.0',36 });3738 console.log(`Application is running on: ${await app.getUrl()}`);39}4041bootstrap();What Harpy Adds to Fastify:
- •JSX Reply Decorator: Adds
reply.jsx()method for rendering components - •Static File Serving: Automatically serves chunks from
dist/chunks/ - •Content-Type Headers: Proper HTML/CSS/JS/JSON headers automatically set
- •Error Handling: Custom error pages rendered with JSX
- •Response Streaming: Efficient HTML delivery with low memory usage
Performance Note: Fastify is one of the fastest Node.js web frameworks (30,000+ req/sec). Combined with Harpy's 1-7ms SSR, you get sub-50ms Time to First Byte consistently.
Build Process Deep Dive
Understanding the build process helps you optimize your application and debug issues effectively. Harpy's build is fast and transparent.
# Harpy Build Process```bashharpy build```Steps:1. TypeScript Compilation - Compile server-side code - Preserve JSX for components 2. Client Component Detection - Scan for 'use client' directives - Build dependency graph - Extract component boundaries3. Hydration Build - Create hydration entry for each component - Bundle with esbuild (fast!) - Generate shared vendor chunk - Output to dist/chunks/4. Style Compilation - Process Tailwind CSS - Minify with cssnano - Output to dist/assets/5. Asset Copying - Copy public files - Generate manifest.jsonResult: Production-ready optimized build1. TypeScript Compilation
Server-side code is compiled with tsc. JSX is preserved in components for the JSX engine to process at runtime. Fast incremental builds in development.
2. Client Component Detection
The build system scans all .tsx files for the 'use client' directive. Each client component becomes a separate entry point.
Dependencies are analyzed to create the vendor chunk with shared code (React, ReactDOM, common utilities).
3. Hydration Build (esbuild)
Using esbuild for maximum speed (10-100x faster than Webpack):
- • Bundle each component independently
- • Tree-shaking to remove unused code
- • Minification for production
- • Source maps for debugging
- • Typical build time: 30-200ms per component
4. Style Compilation
Tailwind CSS processed with PostCSS. Production builds are minified with cssnano. Critical CSS can be inlined for instant first paint.
Performance Metrics
Real-world performance data from production Harpy.js applications:
// Typical Harpy.js Performance MetricsServer-Side Render Time: Simple page: 1-3ms Complex page: 5-10ms With database query: 10-50ms (query dependent)Time to First Byte (TTFB): < 50ms (including SSR)First Contentful Paint (FCP): < 200ms (HTML arrives fast)Time to Interactive (TTI): < 500ms (after hydration)Bundle Sizes: vendor.js: ~188kb (shared, cached) Average component: 2-6kb Page-specific JS: 10-30kb totalWhy so fast?✓ Optimized React SSR✓ Minimal JavaScript sent to client✓ Parallel chunk loading✓ Aggressive caching strategy✓ Fastify's low overhead1-7ms SSR
Most pages render in under 5 milliseconds on the server. Complex pages with many components: under 10ms.
Compare: Traditional SSR frameworks: 50-200ms
~200kb Total JS
vendor.js (188kb cached) + page-specific chunks (10-30kb). Only interactive components send JavaScript.
Compare: Typical SPA: 500kb-2mb initial bundle
<200ms FCP
First Contentful Paint happens almost instantly because HTML is already rendered and sent immediately.
Compare: Client-side apps: 1-3 seconds FCP
<500ms TTI
Time to Interactive is extremely fast. Small JS bundles load and execute quickly. Page is fully interactive in half a second.
Compare: Heavy SPAs: 3-10 seconds TTI
Best Practices
✓DO: Use Server Components by Default
Start with server components. They're simpler, faster, and send zero JavaScript to the client. Only add 'use client' when you actually need interactivity.
Example: Headers, footers, static content, lists → server components
✓DO: Keep Client Components Small
Don't wrap entire pages in 'use client'. Extract only the interactive parts into separate client components. This minimizes JavaScript bundle size.
Example: Interactive button/form inside a static article → button is client, rest is server
✓DO: Trust the Chunking Strategy
Don't try to manually optimize chunk sizes or bundle components together. Harpy's optimistic chunking strategy is designed for HTTP/2 and modern browsers. More small chunks = better caching and parallel loading.
✗DON'T: Use 'use client' Everywhere
Adding 'use client' to every component defeats the purpose of SSR. You'll send unnecessary JavaScript and lose the performance benefits of server rendering.
✗DON'T: Import Heavy Libraries in Server Components
If a server component imports a large library (like lodash entire package), it increases server memory. Use targeted imports or move heavy logic to services.
Debugging & Troubleshooting
Common Issue: Component Not Hydrating
Symptom: Click events don't work, useState doesn't update.
Cause: Forgot to add 'use client' directive.
Solution: Add 'use client' as the first line of the component file.
Common Issue: Hydration Mismatch
Symptom: Console warning about server/client HTML mismatch.
Cause: Component renders different content on server vs client (e.g., Date.now(), random numbers).
Solution: Move dynamic values to useEffect or pass them as props from the server.
Debugging Tip: Check Build Output
Run harpy build and check the console output. You'll see:
- • List of detected client components
- • Chunk sizes for each component
- • Build time per component
- • Any warnings about large dependencies
Debugging Tip: Check Network Tab
Open browser DevTools → Network tab. Filter by JS. You should see vendor.js and individual component chunks. If a chunk is missing, that component won't hydrate.
Summary
Harpy.js delivers exceptional performance through its sophisticated JSX engine architecture:
- •Server-first rendering with 1-7ms response times
- •Automatic client component detection via 'use client'
- •Optimistic chunking for parallel loading and aggressive caching
- •Seamless hydration without flickering or layout shifts
- •Fastify integration for maximum server performance
The result: lightning-fast pages that feel instant to users while maintaining full React interactivity where needed.