Internals
Implementation details that are useful for debugging and understanding how Juice works under the hood. These are not part of the public API and may change between versions.
The Webpack Shim
Juice uses react-server-dom-webpack for React Server Components (RSC). Despite the name, webpack is not involved. The package is React's only maintained implementation of the RSC wire format, and it hardcodes calls to __webpack_require__() for module resolution.
Juice installs a shim on globalThis that bridges these calls to Vite's module system:
// What the shim installs (simplified):
globalThis.__webpack_require__ = function(id) {
return moduleCache.get(id);
};
globalThis.__webpack_chunk_load__ = function(chunkId) {
return Promise.resolve(); // All chunks are pre-loaded
};Why globalThis?
react-server-dom-webpack reads __webpack_require__ from the global scope at runtime. There is no configuration option to provide a custom module resolver. The global shim is the only integration point available.
This is the same approach used by other Vite-based RSC implementations (Waku, vite-rsc). The globals are namespaced (__webpack_require__, __webpack_chunk_load__) and only set when RSC is in use.
Dev vs production
- Dev mode: the shim uses a lazy-loading proxy. When React requests a module that is not cached, the proxy awaits
server.ssrLoadModule(id)from Vite. This enables HMR — new module versions are loaded on demand. - Production: the shim is pre-populated from the flight manifest. All client modules are loaded at startup via
import(). No lazy loading.
Cleanup
The shim provides removeWebpackShim() for test cleanup. This deletes the global properties. In production, the shim lives for the lifetime of the process.
Flight Manifest
The flight manifest is a JSON file generated by the Vite plugin at build time. It maps URL patterns to component modules, tracks client component chunks, and registers server action IDs. The router reads this file at startup — it is the only contract between the build step and the runtime.
{
"routes": {
"/": { "moduleId": "./app/routes/home.tsx" },
"/blog/:slug": { "moduleId": "./app/routes/blog/[slug].tsx" }
},
"clientModules": {
"./app/components/Counter.tsx": {
"id": "Counter",
"chunks": ["assets/Counter-abc123.js"]
}
},
"serverActions": {
"action_1a2b3c": {
"moduleId": "./app/routes/tasks.tsx",
"exportName": "createTask"
}
}
}The manifest is plain JSON. You can inspect it after a build at dist/flight-manifest.json. Diffing manifests across deploys shows exactly what routes, components, or actions changed.
Manifest normalization
The compiler and runtime use slightly different field names for historical reasons. The runtime normalizes at startup: module becomes moduleId, name becomes exportName. This is transparent — you never interact with the manifest directly.
Route Matching
Routes are matched using the WinterCG URLPattern API. At startup, createRouter compiles the manifest's route patterns into an array of URLPattern instances, sorted by specificity (static routes before dynamic routes).
Static routes (no :params or * wildcards) are also indexed in a Map for O(1) lookup. The matching flow:
- Check the static route map (O(1)) — handles most requests
- Fall through to URLPattern iteration for dynamic routes
- Try with and without trailing slash for both paths
Streaming Pipeline
The render pipeline uses renderToReadableStream from react-dom/server (the web streams version, not Node.js streams). The three streaming modes control when headers are committed:
false(wait): callsstream.allReadybefore creating the Response. All Suspense boundaries resolve before the first byte.'shell': the shell renders synchronously (therenderToReadableStreampromise resolves when the shell is ready). The Response is created immediately, and remaining Suspense content streams in.true(full stream): uses aTransformStreamto pipe React's output to the client immediately. Recovery scripts are injected between data chunks for errors that occur after streaming starts.
Error recovery in full streaming mode works by: React's onError callback pushes recovery HTML chunks into an array. The transform stream writer flushes these between React's data chunks. Each recovery chunk is a <script> that replaces the nearest incomplete Suspense boundary with a fallback message.
Context Isolation
Request context uses a two-tier system:
- AsyncLocalStorage (primary): imported from
node:async_hooksin a try-catch. When available, each request runs insideals.run(new Map(), fn). This provides implicit context — any function in the async call stack can read it without passing the request. - WeakMap (fallback): keyed by
Requestobject. Requires explicitrequestreference to read context. Automatically garbage collected when the request completes.
The cache() function stores its deduplication map in whatever storage is active. In ALS mode, the cache is scoped to the ALS store. In WeakMap mode, it uses a _currentRequest reference set by the render pipeline.