How to Optimize React Server Components for Edge Runtime in Next.js

Optimize React Server Components for edge runtime by keeping server bundles under 1 MiB, declaring pages as async functions for streaming, and querying edge-native data sources like D1 or Workers AI directly inside your components.

The ifindev/fullstack-next-cloudflare template demonstrates production-ready patterns for running Next.js on Cloudflare Workers, where every React Server Component (RSC) executes at the edge. Because Cloudflare Workers enforce a strict 1 MiB script size limit, optimization is not optional—it is architecture-critical. Below are the specific patterns used in the repository to ensure sub-second response times while respecting edge constraints.

Keep Components Async for Streaming

Edge Workers stream responses instantly, so you should declare every top-level page as an async function. In src/app/dashboard/todos/page.tsx, the component is defined as export default async function Page(), which allows Next.js to begin streaming HTML as soon as the first chunk renders.

// src/app/dashboard/todos/page.tsx
import TodoListPage from "@/modules/todos/todo-list.page";

export default async function Page() {
  // No await needed here – Next streams the component as soon as it mounts.
  return <TodoListPage />;
}

This pattern reduces Time-to-First-Byte (TTFB) because the worker does not wait for the entire component tree to resolve before sending bytes to the client. The same approach appears in src/app/dashboard/page.tsx, where the root Dashboard page streams the Dashboard component from the edge without blocking on data fetching.

Leverage Edge-Native Data Sources

Perform all database and storage queries inside your RSCs or server actions to keep latency at zero. The template imports Drizzle ORM directly into src/modules/todos/todo-list.page.tsx to query Cloudflare D1, which is bound to the Worker via wrangler.jsonc.

// src/modules/todos/todo-list.page.tsx
import { db } from "@/db";
import { todos } from "@/db/schema";

export default async function TodoListPage() {
  // Runs on the edge; the D1 binding is provided by the Worker.
  const items = await db.select().from(todos).orderBy(todos.created_at);

  return (
    <ul>
      {items.map(t => (
        <li key={t.id}>{t.title}</li>
      ))}
    </ul>
  );
}

Because this code runs inside a server component, the Drizzle driver never ships to the browser, and the query executes within the same isolate as the Worker. For AI inference, src/services/summarizer.service.ts calls Cloudflare Workers AI directly from a server action, avoiding external API latency entirely.

Cache External Fetches at the Edge

When fetching from third-party APIs, use the next fetch options to store responses in the Edge Cache. This bypasses repeated round-trips to origin servers or external endpoints.

// Example pattern for external data fetching
export default async function Page() {
  const resp = await fetch(
    "https://api.example.com/summary",
    { next: { revalidate: 300 } } // 5 minute edge cache
  );
  const { summary } = await resp.json();

  return <p>{summary}</p>;
}

Setting revalidate: 300 tells the Cloudflare Worker to cache the response for five minutes, serving subsequent requests directly from the edge node without contacting the origin.

Avoid Bundle Bloat in Client Components

Importing server-only modules like Drizzle or Workers AI into a "use client" component forces the bundler to include those libraries in the client-side JavaScript, violating the 1 MiB limit and exposing server logic.

Avoid this pattern:

// ❌ Bad: client component importing server-only DB code
"use client";

import { db } from "@/db"; // bundles Drizzle into the client

export default function TodoForm() {
  // …
}

Use this pattern instead:

// ✅ Good: thin client UI, server action handles DB
"use client";

export default function TodoForm() {
  async function onSubmit(data) {
    "use server";
    await fetch("/api/todos", {
      method: "POST",
      body: JSON.stringify(data),
    });
  }

  // UI code …
}

Move all database logic to server actions or API routes (e.g., src/app/api/summarize/route.ts), keeping client components lean and interaction-focused.

Keep the Worker Bundle Under 1 MiB

Cloudflare Workers enforce a hard 1 MiB limit on the total script size. To stay within this budget:

  • Tree-shake unused UI components (remove unused shadcn/ui pieces).
  • Use dynamic imports for heavy client-only libraries: const Chart = dynamic(() => import("chart.js"), { ssr: false });.
  • Review bundle size with pnpm run build:cf && npx source-map-explorer .next/server/pages/**/*.js.

The next.config.ts file initializes OpenNext for Cloudflare Workers via initOpenNextCloudflareForDev(), ensuring the build pipeline targets the edge runtime during development and production.

Summary

  • Declare top-level pages as async functions to enable streaming and reduce TTFB.
  • Query D1, KV, R2, and Workers AI directly inside RSCs to eliminate network latency.
  • Add next: { revalidate } to external fetch calls to leverage Edge Cache.
  • Never import server-only libraries (Drizzle, Workers AI) into "use client" components.
  • Monitor bundle size to stay under the 1 MiB Worker limit using dynamic imports and tree-shaking.

Frequently Asked Questions

What is the bundle size limit for Cloudflare Workers?

Cloudflare Workers enforce a 1 MiB limit on the total script size after compression. Exceeding this limit causes deployment failures, so you must tree-shake dependencies and use dynamic imports for client-side libraries.

How does async streaming improve performance in RSCs?

When you export an async function from a page component, Next.js streams the rendered HTML as soon as the first chunk is ready rather than waiting for the entire tree to resolve. This reduces Time-to-First-Byte (TTFB) because the edge worker sends data to the browser immediately while continuing to render asynchronous content.

Can I use Drizzle ORM inside a React Server Component?

Yes. In the ifindev/fullstack-next-cloudflare template, Drizzle queries run inside server components (e.g., src/modules/todos/todo-list.page.tsx) and server actions. Because these execute on the Worker, the ORM never ships to the client, keeping bundles small and queries fast.

How do I cache API responses at the edge?

Pass next: { revalidate: seconds } or next: { tags: ['tag-name'] } to the fetch options. This stores the response in the Cloudflare Edge Cache, allowing subsequent requests to hit the cached copy directly at the edge node without reaching your origin server or external API.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →