SSR framework hydration data analysis
Chrome 5+ Edge 12+ Firefox 4+ Safari 6+ Details
Analyzes hydration data scripts used by SSR frameworks like Next.js, Nuxt, Remix, Gatsby, and others. These scripts contain serialized state that the client needs to "hydrate" the server-rendered HTML into an interactive application.
Supported frameworks
| Framework | Script ID / Pattern | Typical Content |
|---|---|---|
| Next.js | #__NEXT_DATA__ | Page props, router state, build info |
| Nuxt 3 | #__NUXT_DATA__ | Payload, state, config |
| Nuxt 2 | Pattern: window.__NUXT__ | SSR state, error info |
| Remix | #__remixContext | Loader data, routes |
| Gatsby | #___gatsby + page-data | Static query results |
| SvelteKit | #__sveltekit_data | Load function data |
| Astro | astro-island props | Component props |
Why hydration data matters
| Issue | Impact | Threshold |
|---|---|---|
| Large payload | Slower TTFB, longer parsing | > 128 KB |
| Duplicate data | Same data in HTML + hydration | Any duplication |
| Unused props | Data fetched but never rendered | Varies |
| Sensitive data | Exposed in client bundle | Any secrets |
Warning: Everything in hydration scripts is visible to users. Never include API keys, tokens, or sensitive data in server-side props.
Next.js limit: Next.js warns when __NEXT_DATA__ exceeds 128 KB. Large payloads delay hydration and increase Time to Interactive.
Snippet
// SSR Framework Hydration Data Analysis
// https://webperf-snippets.nucliweb.net
(() => {
// @shared formatBytes
function formatBytes(bytes) {
if (bytes === null || bytes === undefined || Number.isNaN(bytes)) return "-";
if (bytes === 0) return "0 B";
const units = ["B", "KB", "MB", "GB"];
const i = Math.max(0, Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1));
return (bytes / Math.pow(1024, i)).toFixed(1) + " " + units[i];
}
// @end-shared formatBytes
// Framework detection patterns
const frameworks = [
{
name: "Next.js",
selector: "#__NEXT_DATA__",
type: "json",
threshold: 128 * 1024, // 128 KB official warning threshold
docs: "https://nextjs.org/docs/messages/large-page-data",
},
{
name: "Nuxt 3",
selector: "#__NUXT_DATA__",
type: "json-array",
threshold: 100 * 1024,
docs: "https://nuxt.com/docs/api/composables/use-hydration",
},
{
name: "Nuxt 2",
pattern: /window\.__NUXT__\s*=/,
type: "script",
threshold: 100 * 1024,
docs: "https://nuxtjs.org/",
},
{
name: "Remix",
selector: "#__remixContext",
type: "script",
threshold: 100 * 1024,
docs: "https://remix.run/docs/en/main/guides/performance",
},
{
name: "Gatsby",
selector: "#___gatsby",
pattern: /window\.___/,
type: "script",
threshold: 100 * 1024,
docs: "https://www.gatsbyjs.com/docs/",
},
{
name: "SvelteKit",
selector: "[data-sveltekit-hydrate]",
type: "json",
threshold: 100 * 1024,
docs: "https://kit.svelte.dev/docs/performance",
},
{
name: "Astro",
selector: "astro-island",
type: "props",
threshold: 50 * 1024,
docs: "https://docs.astro.build/en/concepts/islands/",
},
];
// Find all hydration scripts
const detected = [];
const allInlineScripts = Array.from(
document.querySelectorAll("script:not([src])")
).filter((s) => s.innerHTML.trim().length > 0);
// Check each framework
frameworks.forEach((fw) => {
let elements = [];
let content = "";
let size = 0;
if (fw.selector) {
const el = document.querySelector(fw.selector);
if (el) {
elements = [el];
content = el.innerHTML || el.textContent || "";
size = new Blob([content]).size;
}
}
if (fw.pattern && elements.length === 0) {
allInlineScripts.forEach((script) => {
if (fw.pattern.test(script.innerHTML)) {
elements.push(script);
content = script.innerHTML;
size += new Blob([content]).size;
}
});
}
// Special handling for Astro islands
if (fw.name === "Astro") {
const islands = document.querySelectorAll("astro-island");
if (islands.length > 0) {
elements = Array.from(islands);
let totalProps = 0;
islands.forEach((island) => {
const props = island.getAttribute("props");
if (props) totalProps += new Blob([props]).size;
});
size = totalProps;
content = `${islands.length} islands with props`;
}
}
if (elements.length > 0) {
detected.push({
...fw,
elements,
content,
size,
exceedsThreshold: size > fw.threshold,
});
}
});
// Calculate other inline scripts
const frameworkScripts = new Set(detected.flatMap((d) => d.elements));
const otherScripts = allInlineScripts.filter((s) => !frameworkScripts.has(s));
const otherSize = otherScripts.reduce(
(sum, s) => sum + new Blob([s.innerHTML]).size,
0
);
// Display results
console.group(
"%c🚀 SSR Framework Hydration Analysis",
"font-weight: bold; font-size: 14px;"
);
if (detected.length === 0) {
console.log("");
console.log(
"%c📭 No SSR framework hydration data detected.",
"color: #6b7280; font-weight: bold;"
);
console.log("This page may be:");
console.log(" • A static HTML page");
console.log(" • A client-side rendered SPA");
console.log(" • Using a framework not yet supported by this snippet");
console.log("");
console.log(
`Found ${otherScripts.length} other inline scripts (${formatBytes(otherSize)})`
);
console.groupEnd();
return { script: "SSR-Hydration-Data-Analysis", status: "ok", count: 0, details: { frameworksFound: 0, totalHydrationBytes: 0, otherInlineBytes: otherSize }, items: [], issues: [] };
}
// Summary
console.log("");
console.log("%cDetected Framework(s):", "font-weight: bold;");
detected.forEach((fw) => {
const status = fw.exceedsThreshold ? "🔴" : "🟢";
console.log(
` ${status} ${fw.name}: ${formatBytes(fw.size)} (threshold: ${formatBytes(fw.threshold)})`
);
});
const totalHydrationSize = detected.reduce((sum, d) => sum + d.size, 0);
console.log("");
console.log(` Total hydration data: ${formatBytes(totalHydrationSize)}`);
console.log(` Other inline scripts: ${formatBytes(otherSize)}`);
// Structured findings for the return value
const itemsData = [];
const issuesData = [];
// Detailed analysis for each framework
detected.forEach((fw) => {
if (fw.exceedsThreshold) {
issuesData.push({
severity: "warning",
message: `${fw.name} hydration data (${Math.round(fw.size / 1024)} KB) exceeds ${Math.round(fw.threshold / 1024)} KB threshold`,
});
}
console.log("");
console.group(
`%c${fw.exceedsThreshold ? "🔴" : "🟢"} ${fw.name} Analysis`,
`color: ${fw.exceedsThreshold ? "#ef4444" : "#22c55e"}; font-weight: bold;`
);
console.log(`Size: ${formatBytes(fw.size)}`);
console.log(`Threshold: ${formatBytes(fw.threshold)}`);
console.log(
`Status: ${fw.exceedsThreshold ? "⚠️ Exceeds recommended limit" : "✅ Within limits"}`
);
// Parse and analyze content
if (fw.type === "json" && fw.content) {
try {
const data = JSON.parse(fw.content);
console.log("");
console.log("%cData Structure:", "font-weight: bold;");
if (fw.name === "Next.js" && data.props) {
const pageProps = data.props?.pageProps || {};
const pagePropsSize = new Blob([JSON.stringify(pageProps)]).size;
console.log(` Build ID: ${data.buildId || "N/A"}`);
console.log(` Page: ${data.page || "N/A"}`);
console.log(` pageProps size: ${formatBytes(pagePropsSize)}`);
// Analyze pageProps keys
const propsKeys = Object.keys(pageProps);
if (propsKeys.length > 0) {
console.log("");
console.log("%c pageProps breakdown:", "font-weight: bold;");
const propSizes = propsKeys.map((key) => ({
key,
size: new Blob([JSON.stringify(pageProps[key])]).size,
}));
propSizes.sort((a, b) => b.size - a.size);
propSizes.slice(0, 10).forEach((prop) => {
const pct = ((prop.size / pagePropsSize) * 100).toFixed(1);
const bar = "█".repeat(Math.min(Math.round(parseFloat(pct) / 5), 20));
console.log(
` ${prop.key}: ${formatBytes(prop.size)} (${pct}%) ${bar}`
);
});
if (propsKeys.length > 10) {
console.log(` ... and ${propsKeys.length - 10} more props`);
}
}
// Check for common issues
console.log("");
console.log("%cPotential Issues:", "font-weight: bold;");
// Large arrays
const largeArrays = propsKeys.filter((key) => {
const val = pageProps[key];
return Array.isArray(val) && val.length > 50;
});
if (largeArrays.length > 0) {
console.log(
` ⚠️ Large arrays: ${largeArrays.join(", ")} (consider pagination)`
);
}
// Deeply nested objects
const checkDepth = (obj, depth = 0) => {
if (depth > 5) return true;
if (typeof obj !== "object" || obj === null) return false;
return Object.values(obj).some((v) => checkDepth(v, depth + 1));
};
if (checkDepth(pageProps)) {
console.log(" ⚠️ Deeply nested data (> 5 levels)");
}
// Potential sensitive data patterns
const sensitivePatterns = /password|secret|token|apikey|api_key|private/i;
const propsString = JSON.stringify(pageProps);
if (sensitivePatterns.test(propsString)) {
console.log(
" 🚨 Possible sensitive data detected - review prop names"
);
}
// Key paths that look sensitive. Only keys are checked: a value that contains
// the word "token" is not a sign of leaked data.
const sensitiveKeys = [];
const findSensitiveKeys = (obj, path, depth) => {
if (typeof obj !== "object" || obj === null || depth > 4) return;
for (const key of Object.keys(obj)) {
const fullPath = path ? `${path}.${key}` : key;
if (sensitivePatterns.test(key)) sensitiveKeys.push(fullPath);
findSensitiveKeys(obj[key], fullPath, depth + 1);
}
};
findSensitiveKeys(pageProps, "", 0);
propsKeys.forEach((key) => {
const flags = [];
if (largeArrays.includes(key)) flags.push("large-array");
if (checkDepth(pageProps[key])) flags.push("deeply-nested");
if (sensitiveKeys.some((k) => k === key || k.startsWith(`${key}.`))) flags.push("sensitive-key");
itemsData.push({
framework: "Next.js",
kind: "prop",
name: key,
sizeBytes: new Blob([JSON.stringify(pageProps[key])]).size,
flags,
detail: null,
});
});
if (largeArrays.length > 0) {
issuesData.push({ severity: "warning", message: `pageProps has large arrays (${largeArrays.join(", ")}). Consider pagination.` });
}
if (checkDepth(pageProps)) {
issuesData.push({ severity: "warning", message: "pageProps contains deeply nested data (more than 5 levels)." });
}
if (sensitiveKeys.length > 0) {
issuesData.push({
severity: "warning",
message: `pageProps has keys that look sensitive (${sensitiveKeys.slice(0, 5).join(", ")}). Everything in pageProps is sent to every visitor.`,
});
}
if (
largeArrays.length === 0 &&
!checkDepth(pageProps) &&
!sensitivePatterns.test(propsString)
) {
console.log(" ✅ No obvious issues detected");
}
}
// Raw data reference
console.log("");
console.log("%c📦 Raw data object:", "font-weight: bold;");
console.log(data);
} catch {
console.log("Could not parse JSON content");
}
}
if (fw.name === "Astro") {
console.log("");
console.log(`Islands found: ${fw.elements.length}`);
const islandTable = fw.elements.map((island, i) => {
const props = island.getAttribute("props");
const clientDirective =
island.getAttribute("client") ||
Array.from(island.attributes)
.find((a) => a.name.startsWith("client:"))
?.name.replace("client:", "") ||
"unknown";
return {
"#": i + 1,
Component: island.getAttribute("component-url")?.split("/").pop() || "Unknown",
Client: clientDirective,
"Props Size": props ? formatBytes(new Blob([props]).size) : "0 B",
};
});
console.table(islandTable);
fw.elements.forEach((island) => {
const props = island.getAttribute("props");
itemsData.push({
framework: "Astro",
kind: "island",
name: island.getAttribute("component-url")?.split("/").pop() || "Unknown",
sizeBytes: props ? new Blob([props]).size : 0,
flags: [],
detail:
island.getAttribute("client") ||
Array.from(island.attributes).find((a) => a.name.startsWith("client:"))?.name.replace("client:", "") ||
"unknown",
});
});
}
// Element reference
console.log("");
console.log("%c🔎 Element(s):", "font-weight: bold;");
fw.elements.forEach((el, i) => {
console.log(`${i + 1}.`, el);
});
console.log("");
console.log(`📚 Docs: ${fw.docs}`);
console.groupEnd();
});
// Recommendations
const hasIssues = detected.some((d) => d.exceedsThreshold);
if (hasIssues) {
console.log("");
console.group("%c📝 Recommendations", "color: #3b82f6; font-weight: bold;");
console.log("");
console.log("%cTo reduce hydration data size:", "font-weight: bold;");
console.log("");
console.log("1. Only fetch data you actually render");
console.log(" → Remove unused fields from API responses");
console.log(" → Use GraphQL or tRPC for precise data fetching");
console.log("");
console.log("2. Paginate large lists");
console.log(" → Don't send 100+ items in initial props");
console.log(" → Implement infinite scroll or pagination");
console.log("");
console.log("3. Defer non-critical data");
console.log(" → Fetch some data client-side after hydration");
console.log(" → Use React Query, SWR, or similar");
console.log("");
console.log("4. Transform data before sending");
console.log(" → Remove unnecessary nested data");
console.log(" → Flatten structures where possible");
console.log("");
const nextJs = detected.find((d) => d.name === "Next.js");
if (nextJs) {
console.log("%cNext.js specific:", "font-weight: bold;");
console.log(
'%c// In getServerSideProps or getStaticProps:\nreturn {\n props: {\n // Only include what the page renders\n items: items.slice(0, 10), // Paginate\n // Omit unused fields\n user: { name: user.name, avatar: user.avatar },\n },\n};',
"font-family: monospace; color: #22c55e;"
);
}
console.groupEnd();
}
console.groupEnd();
return {
script: "SSR-Hydration-Data-Analysis",
status: "ok",
count: detected.length,
details: {
frameworksFound: detected.length,
totalHydrationBytes: totalHydrationSize,
otherInlineBytes: otherSize,
hasExceedingThreshold: detected.some(d => d.exceedsThreshold),
frameworks: detected.map(fw => ({ name: fw.name, sizeBytes: fw.size, thresholdBytes: fw.threshold, exceedsThreshold: fw.exceedsThreshold })),
},
// Props and islands, the biggest first (at most 30)
items: itemsData.sort((a, b) => b.sizeBytes - a.sizeBytes).slice(0, 30),
issues: issuesData,
};
})();
Understanding the results
Summary section
- Detected framework(s) with size and status
- Green () = within limits, Red () = exceeds threshold
- Total hydration data vs other inline scripts
Framework-specific analysis
For Next.js, the snippet analyzes:
- Build ID and page route
pagePropsbreakdown by key with size percentages- Visual bars showing which props consume the most space
- Detection of large arrays, deep nesting, and potential sensitive data
For Astro, shows:
- Number of islands
- Client directive for each (load, idle, visible, etc.)
- Props size per island
Common issues detected
| Issue | Why It Matters |
|---|---|
| Large arrays (50+ items) | Should be paginated |
| Deep nesting (> 5 levels) | Increases parsing time |
| Sensitive data patterns | Security risk - visible to users |
Recommended thresholds
| Framework | Recommended Max | Official Warning |
|---|---|---|
| Next.js | 128 KB | Yes, shows warning |
| Nuxt | 100 KB | No official limit |
| Remix | 100 KB | No official limit |
| Gatsby | 100 KB | No official limit |
| Astro | 50 KB total props | No official limit |
Common optimization patterns
Pagination
// ❌ Bad: All items in initial props
export async function getServerSideProps() {
const allItems = await fetchAllItems(); // 500 items
return { props: { items: allItems } };
}
// ✅ Good: Paginated
export async function getServerSideProps() {
const items = await fetchItems({ limit: 10 });
return { props: { items, hasMore: true } };
}Selective fields
// ❌ Bad: Full objects
return { props: { users } }; // Each user has 50 fields
// ✅ Good: Only needed fields
return {
props: {
users: users.map(({ id, name, avatar }) => ({ id, name, avatar })),
},
};Browser support
| Feature | Chrome | Edge | Firefox | Safari |
|---|---|---|---|---|
| Blob.size (opens in a new tab) | 5 | 12 | 4 | 6 |
Source: MDN browser compatibility data (opens in a new tab), version 8.1.4.
Further reading
- Next.js: Large Page Data warning (opens in a new tab) | Next.js Docs
- Nuxt: Reducing Payload Size (opens in a new tab) | Nuxt Docs
- Remix: Performance (opens in a new tab) | Remix Docs
- Astro: Islands Architecture (opens in a new tab) | Astro Docs
- Understanding Hydration (opens in a new tab) | Josh W. Comeau