Loading
SSR Framework Hydration Data Analysis

SSR Framework Hydration Data Analysis

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:

FrameworkScript ID / PatternTypical Content
Next.js#__NEXT_DATA__Page props, router state, build info
Nuxt 3#__NUXT_DATA__Payload, state, config
Nuxt 2Pattern: window.__NUXT__SSR state, error info
Remix#__remixContextLoader data, routes
Gatsby#___gatsby + page-dataStatic query results
SvelteKit#__sveltekit_dataLoad function data
Astroastro-island propsComponent props

Why hydration data matters:

IssueImpactThreshold
Large payloadSlower TTFB, longer parsing> 128 KB
Duplicate dataSame data in HTML + hydrationAny duplication
Unused propsData fetched but never renderedVaries
Sensitive dataExposed in client bundleAny 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
 
(() => {
  const formatBytes = (bytes) => {
    if (bytes === 0) return "0 B";
    const k = 1024;
    const sizes = ["B", "KB", "MB"];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return (bytes / Math.pow(k, i)).toFixed(2) + " " + sizes[i];
  };
 
  // 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;
  }
 
  // 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)}`);
 
  // Detailed analysis for each framework
  detected.forEach((fw) => {
    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"
            );
          }
 
          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 (e) {
        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);
    }
 
    // 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();
})();

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
  • pageProps breakdown 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:

IssueWhy It Matters
Large arrays (50+ items)Should be paginated
Deep nesting (> 5 levels)Increases parsing time
Sensitive data patternsSecurity risk - visible to users

Recommended Thresholds

FrameworkRecommended MaxOfficial Warning
Next.js128 KBYes, shows warning
Nuxt100 KBNo official limit
Remix100 KBNo official limit
Gatsby100 KBNo official limit
Astro50 KB total propsNo 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 })),
  },
};

Further Reading