Performance

Guide for optimizing performance in ShipKit applications

Performance

This guide covers performance optimization techniques, caching strategies, and best practices for ShipKit applications.

Core Web Vitals

Measuring Performance

// src/lib/analytics/web-vitals.ts
import { onCLS, onFID, onLCP, onTTFB } from "web-vitals";

const vitalsUrl = "https://vitals.vercel-analytics.com/v1/vitals";

function getConnectionSpeed() {
  return "connection" in navigator &&
    navigator["connection"] &&
    "effectiveType" in navigator["connection"]
    ? (navigator["connection"] as any)["effectiveType"]
    : "";
}

export function sendWebVitals(metric: any) {
  const body = {
    dsn: process.env.NEXT_PUBLIC_ANALYTICS_ID,
    id: metric.id,
    page: window.location.pathname,
    href: window.location.href,
    event_name: metric.name,
    value: metric.value.toString(),
    speed: getConnectionSpeed(),
  };

  const blob = new Blob([new URLSearchParams(body).toString()], {
    type: "application/x-www-form-urlencoded",
  });
  if (navigator.sendBeacon) {
    navigator.sendBeacon(vitalsUrl, blob);
  } else {
    fetch(vitalsUrl, {
      body: blob,
      method: "POST",
      credentials: "omit",
      keepalive: true,
    });
  }
}

export function webVitals() {
  try {
    onFID((metric) => sendWebVitals(metric));
    onTTFB((metric) => sendWebVitals(metric));
    onLCP((metric) => sendWebVitals(metric));
    onCLS((metric) => sendWebVitals(metric));
  } catch (err) {
    console.error("[Web Vitals]", err);
  }
}

Performance Monitoring

// src/app/layout.tsx
import { Analytics } from "@/components/analytics";
import { SpeedInsights } from "@vercel/speed-insights/next";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        {children}
        <Analytics />
        <SpeedInsights />
      </body>
    </html>
  );
}

Image Optimization

Next.js Image Component

// src/components/ui/optimized-image.tsx
import NextImage, { ImageProps } from "next/image";
import { useState } from "react";
import { cn } from "@/lib/utils";

interface OptimizedImageProps extends Omit<ImageProps, "onLoadingComplete"> {
  wrapperClassName?: string;
}

export const OptimizedImage = ({
  alt,
  src,
  className,
  wrapperClassName,
  ...props
}: OptimizedImageProps) => {
  const [isLoading, setIsLoading] = useState(true);

  return (
    <div
      className={cn(
        "overflow-hidden",
        isLoading && "animate-pulse bg-gray-200",
        wrapperClassName
      )}
    >
      <NextImage
        className={cn(
          "duration-700 ease-in-out",
          isLoading
            ? "scale-110 blur-2xl grayscale"
            : "scale-100 blur-0 grayscale-0",
          className
        )}
        src={src}
        alt={alt}
        onLoadingComplete={() => setIsLoading(false)}
        {...props}
      />
    </div>
  );
};

Image Loading Strategies

// src/components/ui/gallery.tsx
import { useInView } from "react-intersection-observer";
import { OptimizedImage } from "./optimized-image";

interface GalleryProps {
  images: Array<{
    src: string;
    alt: string;
  }>;
}

export const Gallery = ({ images }: GalleryProps) => {
  const { ref, inView } = useInView({
    triggerOnce: true,
    rootMargin: "50px 0px",
  });

  return (
    <div
      ref={ref}
      className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"
    >
      {inView &&
        images.map((image, index) => (
          <OptimizedImage
            key={index}
            src={image.src}
            alt={image.alt}
            width={400}
            height={300}
            loading={index < 4 ? "eager" : "lazy"}
            sizes="(max-width: 640px) 100vw,
                   (max-width: 1024px) 50vw,
                   33vw"
          />
        ))}
    </div>
  );
};

Caching Strategies

React Query Configuration

// src/lib/query/config.ts
import { QueryClient } from "@tanstack/react-query";
import { cache } from "react";

export const queryConfig = {
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5 minutes
      gcTime: 1000 * 60 * 60 * 24, // 24 hours
      retry: 1,
      refetchOnWindowFocus: false,
    },
  },
};

export const getQueryClient = cache(() => new QueryClient(queryConfig));

API Route Caching

// src/app/api/products/route.ts
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { products } from "@/lib/db/schema";

export const revalidate = 3600; // Revalidate every hour

export async function GET() {
  try {
    const data = await db.select().from(products);

    return NextResponse.json(data, {
      headers: {
        "Cache-Control": "public, s-maxage=3600, stale-while-revalidate=86400",
      },
    });
  } catch (error) {
    return NextResponse.json(
      { error: "Failed to fetch products" },
      { status: 500 }
    );
  }
}

Static Generation

// src/app/blog/[slug]/page.tsx
import { Metadata } from "next";
import { notFound } from "next/navigation";
import { ContentService } from "@/lib/content";

interface PageProps {
  params: {
    slug: string;
  };
}

export async function generateMetadata({
  params,
}: PageProps): Promise<Metadata> {
  const post = await ContentService.getPost(params.slug);

  if (!post) {
    return {};
  }

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.image],
    },
  };
}

export async function generateStaticParams() {
  const posts = await ContentService.getAllPosts();

  return posts.map((post) => ({
    slug: post.slug,
  }));
}

export default async function BlogPost({ params }: PageProps) {
  const post = await ContentService.getPost(params.slug);

  if (!post) {
    notFound();
  }

  return (
    <article className="prose prose-lg mx-auto">
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

Code Optimization

Dynamic Imports

// src/components/features/rich-editor.tsx
import dynamic from "next/dynamic";
import { Spinner } from "@/components/ui/spinner";

const Editor = dynamic(
  () => import("@/components/ui/editor").then((mod) => mod.Editor),
  {
    loading: () => (
      <div className="h-[300px] flex items-center justify-center">
        <Spinner />
      </div>
    ),
    ssr: false,
  }
);

export const RichEditor = (props: EditorProps) => {
  return <Editor {...props} />;
};

Bundle Analysis

// next.config.mjs
import { withAnalyzer } from "@next/bundle-analyzer";

const config = {
  // ... other config
};

export default withAnalyzer({
  enabled: process.env.ANALYZE === "true",
})(config);

State Management

Server Components

// src/app/products/page.tsx
import { Suspense } from "react";
import { ProductGrid } from "@/components/products/grid";
import { ProductSkeleton } from "@/components/products/skeleton";

export default function ProductsPage() {
  return (
    <div>
      <Suspense fallback={<ProductSkeleton />}>
        <ProductGrid />
      </Suspense>
    </div>
  );
}

Optimistic Updates

// src/components/features/todo-list.tsx
"use client";

import { useOptimistic } from "react";
import { addTodo } from "@/server/actions/todos";

interface Todo {
  id: string;
  text: string;
  completed: boolean;
}

export const TodoList = ({
  initialTodos,
}: {
  initialTodos: Todo[];
}) => {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    initialTodos,
    (state, newTodo: Todo) => [...state, newTodo]
  );

  async function handleAddTodo(text: string) {
    const newTodo = {
      id: crypto.randomUUID(),
      text,
      completed: false,
    };

    addOptimisticTodo(newTodo);
    await addTodo(newTodo);
  }

  return (
    <ul>
      {optimisticTodos.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
};

Best Practices

Performance Checklist

  1. Core Web Vitals

    • Monitor LCP, FID, and CLS
    • Optimize for mobile devices
    • Use performance monitoring tools
  2. Image Optimization

    • Use Next.js Image component
    • Implement proper loading strategies
    • Optimize image formats and sizes
  3. Caching

    • Implement proper cache strategies
    • Use stale-while-revalidate
    • Configure cache headers
  4. Code Optimization

    • Use dynamic imports
    • Implement code splitting
    • Monitor bundle size
  5. State Management

    • Use server components
    • Implement optimistic updates
    • Minimize client-side state

Performance Monitoring

  1. Analytics

    • Track Core Web Vitals
    • Monitor user interactions
    • Analyze performance metrics
  2. Error Tracking

    • Implement error boundaries
    • Track JavaScript errors
    • Monitor API failures
  3. User Experience

    • Track page load times
    • Monitor user engagement
    • Analyze conversion rates

Examples

Performance Monitoring Setup

// src/lib/monitoring/setup.ts
import * as Sentry from "@sentry/nextjs";
import posthog from "posthog-js";

export function setupMonitoring() {
  if (process.env.NEXT_PUBLIC_SENTRY_DSN) {
    Sentry.init({
      dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
      tracesSampleRate: 0.1,
      replaysSessionSampleRate: 0.1,
    });
  }

  if (process.env.NEXT_PUBLIC_POSTHOG_KEY) {
    posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
      api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
      capture_pageview: false,
    });
  }
}

export function captureException(error: Error) {
  console.error(error);
  Sentry.captureException(error);
}

export function captureEvent(name: string, properties?: Record<string, any>) {
  posthog.capture(name, properties);
}

Related Resources