MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

Next.js 12: Middleware, React 18, and Server Components

Explore Next.js 12: middleware, React Server Components, SWC compiler, and Edge Functions.

Next.jsReactServer ComponentsFrontend

By MinhVo

Introduction

Next.js 12 represents a watershed moment in the evolution of React-based web development. Released in October 2021, this version introduced foundational features that would reshape how developers think about server-side rendering, edge computing, and application architecture. With Native ES Modules support, Middleware at the Edge, React Server Components (RFC), and the SWC compiler replacing Babel, Next.js 12 laid the groundwork for the modern full-stack React paradigm.

The release addressed long-standing pain points around cold compilation times, lack of edge-native request interception, and the tension between client and server rendering models. For teams building production applications at scale, these weren't incremental improvements—they were architectural shifts that demanded rethinking routing strategies, data fetching patterns, and deployment pipelines.

Next.js 12 Middleware and Server Components Architecture

Understanding Next.js 12 Middleware: Core Concepts

Next.js 12 introduced Middleware as a first-class concept, enabling developers to run code before a request is completed. Unlike API routes that handle specific endpoints, Middleware intercepts requests at the edge, before they reach your application's routing layer. This paradigm shift opened up possibilities for authentication, A/B testing, geolocation-based redirects, and request rewriting—all without the latency penalty of a round-trip to an origin server.

Middleware in Next.js 12 runs on the Edge Runtime by default, which means it executes in a V8 isolates environment rather than Node.js. This distinction is critical: Edge Runtime supports only Web APIs, not Node.js APIs like fs or process. The runtime constraint ensures Middleware can execute in under 50 milliseconds at edge locations worldwide.

The Middleware API is defined in a special middleware.js or middleware.ts file at the root of your project. The function receives a NextRequest object and returns a NextResponse, giving you full control over redirects, rewrites, header manipulation, and response streaming.

// middleware.ts - Next.js 12 Middleware example
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token')
  const isPublicPath = request.nextUrl.pathname.startsWith('/login') ||
                        request.nextUrl.pathname.startsWith('/register')
 
  if (!token && !isPublicPath) {
    const loginUrl = new URL('/login', request.url)
    loginUrl.searchParams.set('from', request.nextUrl.pathname)
    return NextResponse.redirect(loginUrl)
  }
 
  if (token && isPublicPath) {
    return NextResponse.redirect(new URL('/dashboard', request.url))
  }
 
  const response = NextResponse.next()
  response.headers.set('x-request-id', crypto.randomUUID())
  return response
}
 
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}

The matcher configuration is a powerful pattern-matching system that determines which paths trigger the Middleware. It uses a simplified syntax that supports path parameters, wildcards, and negative lookahead patterns.

Edge Runtime vs Node.js Runtime

Understanding the distinction between Edge and Node.js runtimes is crucial. The Edge Runtime provides a subset of Web APIs: fetch, Request, Response, URL, TextEncoder, crypto, and basic JavaScript globals. You cannot use fs, path, Buffer, or any npm package that depends on Node.js-specific APIs.

If your authentication logic requires JWT verification using a library like jsonwebtoken (which uses Node.js crypto), you'll need to use the Web Crypto API alternative or a library that supports Edge Runtime, such as jose.

// Edge-compatible JWT verification
import { jwtVerify } from 'jose'
 
export async function verifyAuth(token: string) {
  const secret = new TextEncoder().encode(process.env.JWT_SECRET)
  const { payload } = await jwtVerify(token, secret)
  return payload
}

React Server Components in Next.js 12

React Server Components (RSC) represent the most significant architectural change in React since hooks. Server Components render exclusively on the server and send serialized HTML to the client—they never ship JavaScript to the browser. This fundamentally changes the bundle size equation: a Server Component that imports a 50KB markdown parsing library adds zero bytes to your client bundle because that library runs only on the server.

In Next.js 12, Server Components were available as an experimental feature behind the experimental.runtime flag. The mental model is straightforward: components that need interactivity (event handlers, useState, useEffect) must be Client Components marked with 'use client'. Everything else can remain a Server Component.

React Server Components Architecture

Server Component Data Fetching

Server Components can directly await async operations, including database queries, API calls, and file system reads. This eliminates the need for getServerSideProps at the page level—each component can fetch exactly the data it needs.

// app/dashboard/page.tsx - Server Component
import { db } from '@/lib/database'
import { UserStats } from './user-stats'  // Client Component
import { ActivityFeed } from './activity-feed'  // Server Component
 
export default async function DashboardPage() {
  const user = await db.user.findUnique({
    where: { id: getCurrentUserId() },
    include: { stats: true, activities: { take: 20 } }
  })
 
  return (
    <div className="dashboard">
      <h1>Welcome back, {user.name}</h1>
      <UserStats stats={user.stats} />
      <ActivityFeed activities={user.activities} />
    </div>
  )
}

The key insight is the component boundary: UserStats is a Client Component because it likely uses interactive charts, while ActivityFeed is a Server Component that renders a static list.

The SWC Compiler: A Performance Revolution

Next.js 12 replaced Babel with SWC (Speedy Web Compiler) as the default JavaScript/TypeScript compiler. Written in Rust, SWC is 17x faster than Babel in single-threaded benchmarks and leverages all available CPU cores for parallel compilation. For large codebases, this translates to cold start improvements from 30+ seconds to under 2 seconds.

SWC handles TypeScript transpilation, JSX transformation, and code minification. It also powers Fast Refresh (Hot Module Replacement), providing near-instantaneous feedback during development.

// next.config.js - SWC configuration
/** @type {import('next').NextConfig} */
const nextConfig = {
  swcMinify: true,
  compiler: {
    styledComponents: true,
    removeConsole: process.env.NODE_ENV === 'production',
    relay: {
      src: './',
      artifactDirectory: './__generated__',
      language: 'typescript',
    },
  },
  experimental: {
    runtime: 'experimental-edge',
    serverComponents: true,
  },
}
 
module.exports = nextConfig

SWC Plugins and Customization

Unlike Babel's ecosystem of transform plugins, SWC plugins are compiled WebAssembly modules. This architecture ensures plugins execute at near-native speed while maintaining sandboxed isolation. In a typical React application with 500 components, Babel compilation takes 8-12 seconds on initial build and 300-500ms for incremental changes. SWC reduces these to 0.5-1 second and 10-30ms respectively.

Step-by-Step Implementation

Setting up a Next.js 12 project with Middleware and Server Components requires careful configuration:

// package.json dependencies (relevant)
{
  "dependencies": {
    "next": "12.3.4",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "jose": "^4.14.0"
  }
}

Now implement the Middleware for authentication and request tracking:

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { jwtVerify } from 'jose'
 
const protectedRoutes = ['/dashboard', '/settings', '/profile']
const authRoutes = ['/login', '/register']
 
export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl
  const token = request.cookies.get('session-token')?.value
  const requestId = crypto.randomUUID()
  const response = NextResponse.next()
  response.headers.set('x-request-id', requestId)
  response.headers.set('x-edge-location', request.geo?.region || 'unknown')
 
  if (protectedRoutes.some(route => pathname.startsWith(route))) {
    if (!token) {
      const url = new URL('/login', request.url)
      url.searchParams.set('redirectTo', pathname)
      return NextResponse.redirect(url)
    }
    try {
      const secret = new TextEncoder().encode(process.env.JWT_SECRET)
      await jwtVerify(token, secret)
    } catch {
      response.cookies.delete('session-token')
      return NextResponse.redirect(new URL('/login', request.url))
    }
  }
 
  if (authRoutes.some(route => pathname.startsWith(route)) && token) {
    try {
      const secret = new TextEncoder().encode(process.env.JWT_SECRET)
      await jwtVerify(token, secret)
      return NextResponse.redirect(new URL('/dashboard', request.url))
    } catch {
      response.cookies.delete('session-token')
    }
  }
 
  return response
}
 
export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico|public).*)'],
}

Real-World Use Cases

Use Case 1: E-Commerce Platform Authentication

An e-commerce platform handling 50,000 requests per second migrated its authentication layer from Express middleware to Next.js 12 Middleware. The edge execution reduced authentication latency from 120ms (round-trip to origin) to 12ms (edge V8 isolate). The platform also implemented geolocation-based pricing using request.geo, showing region-specific product catalogs.

Use Case 2: SaaS Dashboard with Data-Heavy Components

A SaaS analytics dashboard replaced its client-side data fetching pattern (SWR + API routes) with React Server Components. The main dashboard page previously loaded 340KB of JavaScript; after migrating data-fetching components to Server Components, the client bundle dropped to 89KB—a 74% reduction.

Use Case 3: Multi-Language Content Platform

A content platform serving 12 languages used Middleware for locale detection and content negotiation. The Middleware reads the Accept-Language header, checks for a locale cookie, and rewrites the URL to the appropriate locale subpath (/en/about, /fr/about). This approach eliminated locale-based redirects that previously added 200ms to first-page loads for international users.

Edge Computing Architecture

Best Practices for Production

  1. Keep Middleware lean: Middleware runs on every matched request—avoid heavy computation. JWT verification, cookie checks, and URL rewrites are appropriate; database queries are not.

  2. Use the matcher config strategically: Exclude static assets and API routes that don't need Middleware. Overly broad matchers add latency to asset delivery.

  3. Implement graceful Edge Runtime fallbacks: Not all npm packages support Edge Runtime. Test your dependencies before committing to edge-only Middleware.

  4. Separate Server and Client Components deliberately: Place the 'use client' boundary as deep as possible in the component tree. A Client Component that wraps Server Components forces all children to the client bundle.

  5. Leverage Server Component streaming: Use Suspense boundaries around async Server Components to enable progressive page loading.

  6. Cache Server Component responses: Use fetch with next: { revalidate } for data that doesn't change frequently.

  7. Monitor Edge Runtime cold starts: Edge isolates have cold start times of 5-10ms, but complex Middleware can increase this. Use Vercel's Edge Function analytics to track latency.

  8. Test Middleware locally with next dev: Next.js 12 runs Middleware locally during development. Use this to catch Edge Runtime incompatibilities early.

Common Pitfalls and Solutions

PitfallImpactSolution
Using Node.js APIs in MiddlewareRuntime error at the edgeUse Web APIs (crypto, TextEncoder) or edge-compatible libraries like jose
Placing 'use client' at the rootEntire app ships client JSMove the directive to leaf components that need interactivity
Forgetting the matcher configMiddleware runs on static assetsDefine explicit matcher patterns excluding _next, api, and static files
Server Component importing Client ComponentServer Component becomes client boundaryKeep Server Components above Client Components in the tree
Assuming request.geo is always availableReturns undefined in self-hosted deploymentsProvide a fallback: request.geo?.city || 'Unknown'
Not handling Edge Runtime cold startsFirst request after idle has higher latencyImplement health check endpoints and keep-alive strategies

Performance Optimization

The SWC compiler and Edge Runtime provide significant performance wins, but optimization requires deliberate architecture:

// Optimized Server Component with streaming
import { Suspense } from 'react'
 
export default async function ProductPage({ params }: { params: { id: string } }) {
  return (
    <div className="product-page">
      <header>
        <Suspense fallback={<div className="skeleton h-8 w-64" />}>
          <ProductTitle id={params.id} />
        </Suspense>
      </header>
      <main>
        <Suspense fallback={<ProductSkeleton />}>
          <ProductDetails id={params.id} />
        </Suspense>
        <Suspense fallback={<ReviewsSkeleton />}>
          <ProductReviews id={params.id} />
        </Suspense>
      </main>
    </div>
  )
}
 
async function ProductTitle({ id }: { id: string }) {
  const product = await db.product.findUnique({ where: { id }, select: { name: true } })
  return <h1>{product.name}</h1>
}

Comparison with Alternatives

FeatureNext.js 12 MiddlewareExpress MiddlewareCloudflare Workers
Execution LocationEdge (V8 isolates)Origin serverEdge (V8 isolates)
Cold Start5-10msN/A (always warm)5-10ms
Node.js API SupportLimitedFullLimited
Integration with FrameworkNativeSeparate serverSeparate deployment
ConfigurationFile-based (middleware.ts)Code-basedwrangler.toml
GeolocationBuilt-in (request.geo)Requires MaxMindBuilt-in

Advanced Patterns

Chaining Middleware Logic

For complex applications, organize Middleware logic into composable functions:

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
type MiddlewareFn = (req: NextRequest, res: NextResponse) => NextResponse | Promise<NextResponse>
 
const withAuth: MiddlewareFn = async (req, res) => {
  const token = req.cookies.get('session')?.value
  if (!token && isProtected(req.nextUrl.pathname)) {
    return NextResponse.redirect(new URL('/login', req.url))
  }
  return res
}
 
const withSecurityHeaders: MiddlewareFn = async (req, res) => {
  res.headers.set('X-Content-Type-Options', 'nosniff')
  res.headers.set('X-Frame-Options', 'DENY')
  res.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
  return res
}
 
export async function middleware(request: NextRequest) {
  let response = NextResponse.next()
  response = await withSecurityHeaders(request, response)
  response = await withAuth(request, response)
  return response
}

Testing Strategies

Testing Middleware and Server Components requires a combination of unit and integration approaches:

// __tests__/middleware.test.ts
import { middleware } from '../middleware'
import { NextRequest } from 'next/server'
 
function createRequest(path: string, cookies: Record<string, string> = {}) {
  return new NextRequest(new URL(`http://localhost:3000${path}`), {
    headers: { cookie: Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ') }
  })
}
 
describe('Middleware', () => {
  it('redirects unauthenticated users to login', async () => {
    const response = await middleware(createRequest('/dashboard'))
    expect(response.status).toBe(307)
    expect(response.headers.get('location')).toContain('/login')
  })
 
  it('allows authenticated users to access protected routes', async () => {
    const response = await middleware(createRequest('/dashboard', { 'session-token': 'valid-jwt' }))
    expect(response.status).toBe(200)
  })
})

Future Outlook

Next.js 12's Middleware and Server Components were stepping stones to the full App Router architecture in Next.js 13. The edge-first execution model pioneered in v12 became the default for all route handlers. React Server Components evolved from an experimental flag to the primary rendering model. The SWC compiler's success led to Turbopack, the Rust-based bundler that replaced Webpack in Next.js 13+.

Production Deployment and Monitoring

Deploying React applications to production requires careful consideration of build optimization, error tracking, and performance monitoring. A well-configured production build can significantly improve user experience through faster load times and more reliable error reporting.

Build Optimization Checklist

Before deploying, verify that your production build is fully optimized:

// next.config.js
module.exports = {
  reactStrictMode: true,
  poweredByHeader: false,
  compress: true,
 
  // Optimize images
  images: {
    formats: ['image/avif', 'image/webp'],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048],
    minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days
  },
 
  // Security headers
  async headers() {
    return [{
      source: '/(.*)',
      headers: [
        { key: 'X-Frame-Options', value: 'DENY' },
        { key: 'X-Content-Type-Options', value: 'nosniff' },
        { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
      ],
    }];
  },
 
  // Webpack optimization
  webpack: (config, { isServer }) => {
    if (!isServer) {
      config.optimization.splitChunks = {
        chunks: 'all',
        cacheGroups: {
          vendor: {
            test: /[\\/]node_modules[\\/]/,
            name: 'vendor',
            chunks: 'all',
          },
        },
      };
    }
    return config;
  },
};

Error Tracking Integration

Configure Sentry or a similar error tracking service to capture and categorize production errors:

import * as Sentry from '@sentry/nextjs';
 
Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  tracesSampleRate: 0.1,
  replaysSessionSampleRate: 0.1,
  replaysOnErrorSampleRate: 1.0,
  integrations: [
    new Sentry.BrowserTracing(),
    new Sentry.Replay({
      maskAllText: true,
      blockAllMedia: true,
    }),
  ],
  beforeSend(event) {
    // Filter out known non-critical errors
    if (event.exception?.values?.[0]?.type === 'ChunkLoadError') {
      return null;
    }
    return event;
  },
});

Health Check Endpoints

Implement health check endpoints that your load balancer and monitoring systems can use to verify application availability:

// pages/api/health.ts
export default async function handler(req, res) {
  try {
    // Check database connectivity
    await db.raw('SELECT 1');
 
    // Check external service dependencies
    const redisPing = await redis.ping();
 
    res.status(200).json({
      status: 'healthy',
      timestamp: new Date().toISOString(),
      services: {
        database: 'connected',
        redis: redisPing === 'PONG' ? 'connected' : 'degraded',
      },
      uptime: process.uptime(),
    });
  } catch (error) {
    res.status(503).json({
      status: 'unhealthy',
      error: error.message,
    });
  }
}

This comprehensive monitoring approach ensures you detect and respond to production issues quickly, maintaining high availability for your users.

Production Deployment and Operations

Running backend services in production requires attention to reliability, observability, and operational concerns that don't exist in development environments. Proper deployment practices ensure your service remains available and performant under real-world conditions.

Graceful Shutdown Handling

Implement graceful shutdown to prevent request failures during deployments and restarts:

const server = app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});
 
async function gracefulShutdown(signal) {
  console.log(`Received ${signal}, starting graceful shutdown...`);
 
  // Stop accepting new connections
  server.close(async () => {
    console.log('HTTP server closed');
 
    try {
      // Wait for existing requests to complete (with timeout)
      await Promise.race([
        waitForActiveRequests(),
        new Promise((_, reject) =>
          setTimeout(() => reject(new Error('Shutdown timeout')), 30000)
        ),
      ]);
 
      // Close database connections
      await db.destroy();
      await redis.quit();
 
      console.log('Graceful shutdown completed');
      process.exit(0);
    } catch (error) {
      console.error('Error during shutdown:', error);
      process.exit(1);
    }
  });
 
  // Force shutdown after timeout
  setTimeout(() => {
    console.error('Forced shutdown after timeout');
    process.exit(1);
  }, 35000);
}
 
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));

Structured Logging

Replace console.log with structured logging that supports log aggregation and querying:

const pino = require('pino');
 
const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  formatters: {
    level(label) {
      return { level: label };
    },
  },
  serializers: {
    err: pino.stdSerializers.err,
    req: pino.stdSerializers.req,
    res: pino.stdSerializers.res,
  },
  redact: {
    paths: ['req.headers.authorization', 'req.headers.cookie'],
    remove: true,
  },
});
 
// Request logging middleware
app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    logger.info({
      req,
      res,
      responseTime: Date.now() - start,
    }, `${req.method} ${req.url} ${res.statusCode}`);
  });
  next();
});

Rate Limiting and Abuse Prevention

Protect your API endpoints with rate limiting that adapts to different client types:

const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
 
const apiLimiter = rateLimit({
  store: new RedisStore({ client: redisClient }),
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // 100 requests per window
  standardHeaders: true,
  legacyHeaders: false,
  keyGenerator: (req) => req.user?.id || req.ip,
  handler: (req, res) => {
    logger.warn({ ip: req.ip, user: req.user?.id }, 'Rate limit exceeded');
    res.status(429).json({
      error: 'Too many requests',
      retryAfter: Math.ceil(req.rateLimit.resetTime / 1000),
    });
  },
});
 
app.use('/api/', apiLimiter);

These operational practices form the foundation of a reliable production service that can handle real-world traffic patterns and failure scenarios.

Community Resources and Further Learning

The technology landscape evolves rapidly, making continuous learning essential for maintaining expertise. Building a systematic approach to staying current with developments in your technology stack ensures you can leverage new features and avoid deprecated patterns.

Curated Learning Pathways

Rather than consuming content randomly, create structured learning pathways aligned with your current projects and career goals. Start with official documentation and specification documents, which provide the most accurate and comprehensive information. Follow this with hands-on tutorials and workshops that reinforce concepts through practical application.

Technical blogs from framework maintainers and core team members often provide deeper insights into design decisions and upcoming features. Subscribe to the official blogs of your primary frameworks and libraries to stay ahead of breaking changes and deprecation timelines.

Contributing to Open Source

Contributing to open-source projects in your technology stack provides unparalleled learning opportunities. Start with documentation improvements and bug reports, then progress to fixing small issues tagged as "good first issue" in your favorite projects. This direct engagement with maintainers and the codebase accelerates your understanding far beyond what passive learning can achieve.

# Setting up for contribution
git clone https://github.com/project/repository.git
cd repository
git checkout -b fix/issue-description
 
# Run the project's contribution setup
npm run setup:dev
npm run test  # Ensure tests pass before making changes
 
# Make your changes, then run the full test suite
npm run test:full
npm run lint
npm run build
 
# Submit your contribution
git add -A
git commit -m "fix: description of the fix
 
Closes #1234"
git push origin fix/issue-description

Building a Technical Knowledge Base

Maintain a personal knowledge base that captures insights, solutions, and patterns you discover during your work. Tools like Obsidian, Notion, or even a simple Markdown repository can serve as an external memory that grows more valuable over time.

Organize your notes by topic rather than chronologically, and include code examples, links to relevant documentation, and explanations of why certain approaches work better than others. When you encounter a particularly insightful article or conference talk, write a summary that captures the key takeaways and how they apply to your current projects.

Follow key conferences and their published talks to stay informed about emerging patterns and best practices. Many conferences publish recorded talks on YouTube within weeks of the event, making world-class technical content freely accessible.

Join relevant Discord servers, Slack communities, and forums where practitioners discuss real-world challenges and solutions. These communities provide early warning about emerging issues and access to collective wisdom that isn't available through formal documentation.

Mentorship and Knowledge Sharing

Teaching others is one of the most effective ways to deepen your own understanding. Consider writing technical blog posts, giving talks at local meetups, or mentoring junior developers. The process of explaining concepts to others forces you to organize your knowledge and identify gaps in your understanding.

Pair programming sessions with colleagues of different experience levels create mutual learning opportunities. Senior developers gain fresh perspectives on problems they've solved the same way for years, while junior developers benefit from exposure to production-grade thinking and decision-making processes.

Conclusion

Next.js 12 introduced three paradigm-shifting features that redefined React application architecture. Middleware at the Edge enables sub-20ms request interception for authentication, localization, and A/B testing without origin server round-trips. React Server Components eliminate client-side JavaScript for data-fetching components, reducing bundle sizes by 50-70% in data-heavy applications. The SWC compiler delivers 17x faster compilation, transforming the development feedback loop.

Key takeaways:

  1. Middleware runs on the Edge Runtime with Web API constraints—plan your dependencies accordingly
  2. Server Components can directly await async operations, replacing getServerSideProps for component-level data fetching
  3. The 'use client' boundary determines what ships to the browser—place it as deep as possible in the component tree
  4. SWC plugins are WebAssembly modules, offering near-native compilation speed
  5. Test Middleware locally and monitor edge cold starts in production

For deeper exploration, consult the Next.js 12 documentation, React Server Components RFC, and SWC project documentation.