docs/pro/react-server-components/client-reference-diagnostics.md
Use this guide when a public or mostly static RSC page should prove which client-reference chunks it can load before you introduce route-scoped manifest behavior. The current plugin model emits one client manifest per build. It does not automatically infer per-page or per-route manifests.
The published react-on-rails-rsc webpack and rspack plugins do not currently emit a separate
clientReferenceDiagnosticsFilename asset. Inspect the emitted client manifest instead, or generate a
small local report from the client manifest plus loadable-stats.json after the client build
completes.
import { readFileSync } from 'node:fs';
function readJson(filename) {
return JSON.parse(readFileSync(filename, 'utf8'));
}
const manifest = readJson('public/packs/react-client-manifest.json');
const clientReferences = manifest.filePathToModuleMetadata ?? manifest;
const loadableStats = readJson('public/packs/loadable-stats.json');
const assets = new Map();
function assetHref(asset) {
const publicPath = loadableStats.publicPath;
if (!publicPath || publicPath === 'auto') {
return asset;
}
return `${publicPath.replace(/\/?$/, '/')}${asset.replace(/^\/+/, '')}`;
}
function addAsset(file, id, type) {
const entry = assets.get(file) ?? { ids: [], types: [] };
if (!entry.ids.includes(id)) {
entry.ids.push(id);
}
if (!entry.types.includes(type)) {
entry.types.push(type);
}
assets.set(file, entry);
}
function addStylesheetsForChunk(chunkName, id) {
const chunkAssets = loadableStats.assetsByChunkName?.[chunkName] ?? [];
const assetsForChunk = Array.isArray(chunkAssets) ? chunkAssets : [chunkAssets];
for (const asset of assetsForChunk) {
if (typeof asset === 'string' && asset.endsWith('.css')) {
addAsset(assetHref(asset), id, 'css');
}
}
}
for (const [id, metadata] of Object.entries(clientReferences)) {
const chunks = metadata.chunks ?? [];
for (let index = 1; index < chunks.length; index += 2) {
const chunkName = chunks[index - 1];
addAsset(chunks[index], id, 'js');
addStylesheetsForChunk(chunkName, id);
}
}
console.table([...assets.entries()].map(([file, metadata]) => ({ file, ...metadata })));
The report should be derived from the manifest entries that the RSC package emits. Some manifest
versions wrap those entries under filePathToModuleMetadata; normalize that wrapper before iterating.
The manifest's chunks array stores alternating chunk ids and filenames; report only the filename
half for JS assets, and use the chunk id half to look up extracted CSS files in loadable-stats.json.
A shared JS or CSS file can list multiple client-reference owners. A richer local report can include
the client references recorded in the manifest, the JS chunk files attached to each reference, CSS
files, and byte sizes from the build stats when the bundler exposes them:
{
"version": 1,
"manifestFilename": "react-client-manifest.json",
"isServer": false,
"clientReferenceCount": 1,
"totalChunkBytes": 1234,
"clientReferences": [
{
"file": "file:///absolute/path/to/TinyIsland.js",
"id": "./TinyIsland.js",
"name": "*",
"chunks": [
{
"id": "client-TinyIsland-js",
"file": "client-TinyIsland-js.chunk.js",
"bytes": 1234
}
],
"totalBytes": 1234
}
]
}
bytes should be null when the bundler stats do not expose the asset source. totalChunkBytes
should count each emitted JS or CSS asset file once even when multiple client references share that
asset.
On client and server builds, CSS entries are reported from the emitted CSS assets for the generated chunk group for the listed client reference. If one island imports another client reference, the imported child reference does not inherit a separate CSS asset that belongs only to the importing island.
This diagnostics view is chunk-asset scoped, not selector scoped. If the bundler emits an owner island's CSS asset with selectors from a statically imported child in the same physical CSS file, the owner reference still reports that combined asset.
For a server-only static RSC entry, use an explicit empty client reference list for that build:
new RSCWebpackPlugin({
isServer: false,
clientReferences: [],
});
This produces an empty client manifest. Use it only for a build target that cannot render client
components. Do not apply clientReferences: [] to a mixed RSC app; any page that renders a client
component will miss the client-reference metadata it needs at runtime.
For a static page with one or two small islands, isolate the static build and declare only the island files that the public page may render:
new RSCWebpackPlugin({
isServer: false,
clientReferences: [
{
directory: './app/public-rsc',
recursive: false,
include: /TinyIsland\.(js|jsx|ts|tsx)$/,
},
],
});
The same descriptor shape is supported by RSCRspackPlugin. Keep the static page entry separate from
the normal authenticated app entry when the app entry imports large global vendors, analytics, or
dashboard-only clients. The manifest gives a direct audit trail for whether the tiny island pulls only
its own chunk or also pulls an unexpected vendor chunk through an import.
If an island imports a heavy dependency, the manifest or derived report will show that dependency through the emitted chunk files and byte totals. Remove or defer the import in the island itself; the manifest only reports what the build emitted and does not rewrite the module graph.
This diagnostics slice is intentionally narrow:
Use the manifest output to decide whether an explicit static-page build is acceptable today, or whether the app needs broader manifest-scoping work before treating static RSC pages as performance-isolated.