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

> Learn to optimize React Server Components for the edge runtime in Nextjs. Keep bundles small, use async functions, and query edge data sources directly for faster performance.

- Repository: [Muhammad Arifin/fullstack-next-cloudflare](https://github.com/ifindev/fullstack-next-cloudflare)
- Tags: performance
- Published: 2026-03-04

---

**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`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/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.

```typescript
// 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`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/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`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/modules/todos/todo-list.page.tsx) to query Cloudflare D1, which is bound to the Worker via `wrangler.jsonc`.

```typescript
// 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`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/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.

```typescript
// 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:**

```typescript
// ❌ 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:**

```typescript
// ✅ 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`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/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`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/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`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/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.