TinyKit Pro Docs

Suspense & Server Components Architecture

TinyKit Pro implements strategic use of React Suspense boundaries and Server Components to optimize both SEO performance and user experience. This document o...

Overview

TinyKit Pro implements strategic use of React Suspense boundaries and Server Components to optimize both SEO performance and user experience. This document outlines the patterns and best practices used throughout the codebase.

Key Principles

1. SEO-First Approach

Critical: SEO-critical content (homepage, landing pages) must be server-rendered with data available in the initial HTML.

DO: Use Server Components with preloadQuery for SEO-critical sections ❌ DON'T: Use client-side useQuery for content that needs to be indexed

2. Parallel Data Fetching

Use Promise.all to fetch multiple data sources in parallel rather than sequential waterfalls.

3. Progressive Loading

Use nested Suspense boundaries to show content progressively rather than blocking entire pages.

Implementation Patterns

Pattern 1: Server Component with preloadQuery (SEO-Critical)

Use for: Landing page sections, public content, SEO-critical pages

// FAQSection.tsx - Server Component
import { preloadQuery, preloadedQueryResult } from "convex/nextjs";
import { api } from "@/convex/_generated/api";

export async function FAQSection() {
  const preloadedFAQs = await preloadQuery(api.faqs.public.queries.listActive);
  const faqs = preloadedQueryResult(preloadedFAQs);

  if (!faqs || faqs.length === 0) {
    return null;
  }

  return (
    <section id="faq">
      {/* Render FAQ content - available in initial HTML */}
    </section>
  );
}

Benefits:

  • ✅ Content available to search engine crawlers
  • ✅ Faster First Contentful Paint (FCP)
  • ✅ Better Core Web Vitals scores
  • ✅ Fully type-safe with Convex

Pattern 2: Server Component with Client Wrapper (Interactive Content)

Use for: SEO-critical content that needs client-side interactivity (animations, state management)

// TestimonialsSection.tsx - Server Component (data fetching)
import { preloadQuery, preloadedQueryResult } from "convex/nextjs";
import { TestimonialsSectionClient } from "./TestimonialsSectionClient";

export async function TestimonialsSection() {
  // Fetch data in parallel
  const [preloadedFeatured, preloadedRegular] = await Promise.all([
    preloadQuery(api.testimonials.public.queries.listFeatured),
    preloadQuery(api.testimonials.public.queries.listRegular),
  ]);

  const featuredTestimonials = preloadedQueryResult(preloadedFeatured);
  const regularTestimonials = preloadedQueryResult(preloadedRegular);

  const allTestimonials = [
    ...(featuredTestimonials ?? []),
    ...(regularTestimonials ?? []),
  ];

  // Pass data to client component for interactivity
  return <TestimonialsSectionClient testimonials={allTestimonials} />;
}
// TestimonialsSectionClient.tsx - Client Component (interactivity)
"use client";

import { motion } from "motion/react";
import type { Doc } from "@/convex/_generated/dataModel";

interface Props {
  testimonials: Doc<"testimonials">[];
}

export function TestimonialsSectionClient({ testimonials }: Props) {
  return (
    <section>
      <motion.div
        initial={{ opacity: 0, y: 20 }}
        whileInView={{ opacity: 1, y: 0 }}
        // Client-side animations work while data is in HTML
      >
        {testimonials.map((testimonial) => (
          // Render testimonials
        ))}
      </motion.div>
    </section>
  );
}

Benefits:

  • ✅ SEO: Content in initial HTML
  • ✅ UX: Full client-side interactivity
  • ✅ Performance: Data fetched on server
  • ✅ Clean separation: Server (data) vs Client (UI)

Pattern 3: Suspense with Parallel Data Fetching (Dashboards)

Use for: Protected dashboards, admin pages, user-specific data

// page.tsx - Dashboard with client-side data fetching
"use client";

import { useQuery, Authenticated } from "convex/react";
import { api } from "@/convex/_generated/api";
import { useFeatureSettings } from "@/lib/features";
import { DashboardSkeleton } from "./DashboardSkeleton";

export default function AdminDashboard() {
  return (
    <Authenticated>
      <AdminDashboardContent />
    </Authenticated>
  );
}

function AdminDashboardContent() {
  const { enableOrganizations } = useFeatureSettings();

  // Conditional query based on feature settings
  const userStats = useQuery(api.users.private.queries.getUserStats);
  const orgStats = useQuery(
    api.orgs.private.queries.getOrgStats,
    enableOrganizations ? {} : "skip",
  );
  const subscriptionStats = useQuery(
    api.billing.private.queries.getSubscriptionStatsAdmin,
  );

  const isLoading =
    userStats === undefined ||
    (enableOrganizations && orgStats === undefined) ||
    subscriptionStats === undefined;

  if (isLoading) return <DashboardSkeleton />;

  return (
    <div>
      <DashboardStatsCards
        userStats={userStats}
        orgStats={enableOrganizations ? (orgStats ?? null) : null}
        subscriptionStats={subscriptionStats}
      />
      {/* More dashboard content */}
    </div>
  );
}

Benefits:

  • ⚡ 20-40% faster perceived load time
  • 🎨 Automatic loading states
  • 📦 Parallel data fetching (no waterfalls)
  • 🧹 Cleaner code (no manual isLoading checks)

Before vs After:

// ❌ BEFORE: Sequential waterfall
const userStats = useQuery(api.users.private.queries.getUserStats);
const orgStats = useQuery(api.orgs.private.queries.getOrgStats); // Waits for userStats
const subscriptionStats = useQuery(
  api.billing.private.queries.getSubscriptionStatsAdmin,
); // Waits for orgStats

// ✅ AFTER: Parallel fetching
const [userStatsPreloaded, orgStatsPreloaded, subscriptionStatsPreloaded] =
  await Promise.all([
    preloadQuery(api.users.private.queries.getUserStats),
    preloadQuery(api.orgs.private.queries.getOrgStats),
    preloadQuery(api.billing.private.queries.getSubscriptionStatsAdmin),
  ]);

Pattern 4: Nested Suspense for Progressive Loading

Use for: Complex pages with multiple independent data sources

export default function ComplexDashboard() {
  return (
    <div>
      {/* Header loads first */}
      <Suspense fallback={<HeaderSkeleton />}>
        <DashboardHeader />
      </Suspense>

      {/* Stats load independently */}
      <div className="grid gap-6">
        <Suspense fallback={<StatsSkeleton />}>
          <StatsCards />
        </Suspense>

        <Suspense fallback={<ChartSkeleton />}>
          <AnalyticsChart />
        </Suspense>

        <Suspense fallback={<ActivitySkeleton />}>
          <RecentActivity />
        </Suspense>
      </div>
    </div>
  );
}

Benefits:

  • 🎯 Progressive loading (show content as soon as ready)
  • 🛡️ Better resilience (one failing query doesn't block others)
  • ⚡ Faster perceived performance

Type Safety with preloadQuery

TinyKit Pro uses Convex's fully type-safe preloadQuery system:

// ✅ Correct: Fully type-safe
import { preloadQuery, preloadedQueryResult } from "convex/nextjs";

const preloaded = await preloadQuery(api.faqs.public.queries.listActive);
const faqs = preloadedQueryResult(preloaded);
// faqs is automatically typed as Doc<"faqs">[]

// ❌ Incorrect: Don't use type casting
const faqs = preloaded._valueJSON as unknown as Doc<"faqs">[];

SEO Optimization Checklist

When implementing Server Components for SEO:

  • Use preloadQuery for data fetching
  • Extract data with preloadedQueryResult
  • Ensure content is rendered server-side (not in client component)
  • Verify content appears in "View Source" (browser)
  • Test with Lighthouse SEO audit
  • Check Core Web Vitals (LCP, FCP, CLS)

Performance Optimization Checklist

When implementing Suspense boundaries:

  • Use Promise.all for parallel data fetching
  • Create reusable skeleton components
  • Place Suspense boundaries at appropriate granularity
  • Avoid over-nesting (max 2-3 levels)
  • Test loading states visually
  • Measure performance improvements

Anti-Patterns to Avoid

❌ Don't: Suspense on Above-the-Fold SEO Content

// BAD - Homepage hero hidden from crawlers
export default function HomePage() {
  return (
    <Suspense fallback={<Spinner />}>
      <HeroSection /> {/* SEO-critical! */}
      <PricingSection /> {/* SEO-critical! */}
    </Suspense>
  );
}

Solution: Use Server Components with preloadQuery instead.

❌ Don't: Over-nest Suspense Boundaries

// BAD - Too many nested boundaries
<Suspense>
  <Suspense>
    <Suspense>
      <Suspense>
        <Component />
      </Suspense>
    </Suspense>
  </Suspense>
</Suspense>

Solution: Max 2-3 levels of nesting for related content.

❌ Don't: Client-side useQuery for SEO Content

// BAD - Content not available to search engines
"use client";

export function FAQSection() {
  const faqs = useQuery(api.faqs.public.queries.listActive);
  // Content loads client-side, invisible to crawlers
}

Solution: Convert to Server Component with preloadQuery.

Current Implementation

SEO-Optimized Pages

  • Homepage FAQs (/src/features/landing/sections/faq-section.tsx)

    • Server Component with preloadQuery
    • Content in initial HTML
  • Homepage Testimonials (/src/features/landing/sections/testimonials-section.tsx)

    • Server Component with client wrapper
    • Server fetches data, client handles animations
    • Content in initial HTML

Performance-Optimized Pages

  • Admin Dashboard (/app/admin/page.tsx)
    • Suspense boundary with DashboardSkeleton
    • Parallel data fetching (3 queries simultaneously)
    • 20-40% faster perceived load time

Future Enhancements

Planned Improvements

  1. Organization Dashboard - Add nested Suspense for progressive loading
  2. Notifications Page - Simple Suspense wrapper
  3. Admin CRUD Pages - Bulk update 40+ pages with standard Suspense pattern
  4. Shared Skeleton Library - Create reusable skeleton components

Best Practices Summary

  1. SEO-Critical Content: Server Components with preloadQuery
  2. Interactive Content: Server Component (data) + Client Component (UI)
  3. Dashboards: Suspense + Parallel data fetching
  4. Complex Pages: Nested Suspense for progressive loading
  5. Type Safety: Always use preloadedQueryResult, never cast
  6. Performance: Measure with Lighthouse and Core Web Vitals

References

On this page

Ship your startup faster. In minutes.

Get TinyKit Pro