Skip to main content
</>Rosecraft Studios
next-jsreactweb-developmentperformance

Building Modern Web Apps with Next.js 15

4 min read

Why Next.js 15 Changes Everything

The web development landscape has shifted dramatically. With Next.js 15, the App Router and React Server Components aren't just incremental improvements — they represent a fundamental rethinking of how we build web applications.

At Rosecraft Studios, we've migrated production applications to Next.js 15 and seen measurable improvements across the board: smaller JavaScript bundles, faster Time to First Byte, and a developer experience that finally makes server-side rendering feel natural.

React Server Components: The Core Shift

React Server Components (RSC) allow components to render entirely on the server, never shipping their JavaScript to the client. This isn't SSR as we knew it — it's a new rendering paradigm.

// This component runs on the server. Zero client-side JS.
export async function RecentProjects() {
  const projects = await db.project.findMany({
    where: { published: true },
    orderBy: { createdAt: 'desc' },
    take: 6,
  });

  return (
    <section>
      {projects.map((project) => (
        <ProjectCard key={project.id} project={project} />
      ))}
    </section>
  );
}

The key insight: data fetching lives where the data is consumed. No more prop drilling from getServerSideProps, no more global state management for server data.

Default to Server Components. Only add `"use client"` when you need browser APIs, state, or event handlers. Most of your application can and should remain server-rendered.

The App Router Architecture

The App Router introduces a file-system based routing model with powerful conventions:

Layouts and Templates

Layouts persist across navigations, preserving state and avoiding unnecessary re-renders. Templates re-mount on every navigation, useful for entrance animations.

src/app/
├── layout.tsx        # Root layout (shared header, footer)
├── template.tsx      # Page transition wrapper
├── page.tsx          # Homepage
├── blog/
│   ├── layout.tsx    # Blog-specific layout (sidebar TOC)
│   ├── page.tsx      # Blog index
│   └── [slug]/
│       └── page.tsx  # Individual blog posts

Static Generation with Dynamic Data

generateStaticParams pre-renders pages at build time while generateMetadata provides per-page SEO:

export async function generateStaticParams() {
  const posts = getAllPosts();
  return posts.map((post) => ({ slug: post.slug }));
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  const post = getPostBySlug(slug);
  if (!post) return {};

  return {
    title: post.frontmatter.title,
    description: post.frontmatter.description,
  };
}

Performance Wins We've Measured

After migrating a client's SaaS dashboard from Pages Router to App Router:

Metric Before After Improvement
First Load JS 287 KB 142 KB 50% reduction
Time to First Byte 1.2s 0.3s 75% faster
Lighthouse Performance 72 96 +24 points
Largest Contentful Paint 3.1s 1.4s 55% faster

The biggest win came from moving data fetching to Server Components. Previously, the client loaded a 45 KB state management library just to cache API responses. With RSC, that entire layer disappears.

Streaming and Suspense

Next.js 15 leverages React Suspense for streaming HTML to the browser. Heavy components don't block the entire page:

import { Suspense } from 'react';

export default function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<MetricsSkeleton />}>
        <AnalyticsMetrics />
      </Suspense>
      <Suspense fallback={<ActivitySkeleton />}>
        <RecentActivity />
      </Suspense>
    </div>
  );
}

The shell renders instantly. Each Suspense boundary streams its content as it resolves. Users see a meaningful page in milliseconds, with data filling in progressively.

Server Actions: Mutations Without API Routes

Server Actions bring form handling back to its simplest form — a function that runs on the server:

'use server';

export async function submitContactForm(formData: FormData) {
  const name = formData.get('name') as string;
  const email = formData.get('email') as string;
  const message = formData.get('message') as string;

  await db.contactSubmission.create({
    data: { name, email, message },
  });

  await sendNotificationEmail({ name, email, message });
  revalidatePath('/admin/submissions');
}
Server Actions are progressively enhanced — forms work even before JavaScript loads. This is a significant accessibility and resilience improvement.

What We Recommend

For new projects, Next.js 15 with the App Router is our default recommendation. The combination of Server Components, streaming, and Server Actions provides a foundation that scales from marketing sites to complex SaaS applications.

The learning curve is real, but the payoff — smaller bundles, faster pages, simpler data flow — makes it worthwhile for any team serious about web performance.

Ready to modernize your web application? Get in touch to discuss how Next.js 15 can improve your product's performance and user experience.

Share this article

Corey Rosamond, Founder and Principal Engineer of Rosecraft Studios

Corey Rosamond

Founder & Principal Engineer

Learn more

Enjoyed this article?

Get notified when we publish new insights on web development and engineering.