Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/cloudflare/vinext/llms.txt

Use this file to discover all available pages before exploring further.

Build Pipeline

Vinext’s build pipeline transforms your Next.js application into production-ready bundles for deployment. This guide covers the build process, optimization strategies, and deployment preparation.

Build Orchestration

Using createBuilder

You must use createBuilder() + builder.buildApp() for production builds, not build() directly.
Calling build() from the Vite JS API doesn’t trigger the RSC plugin’s multi-environment build pipeline. From the CLI (packages/vinext/src/cli.ts):
import { createBuilder } from "@vitejs/plugin-rsc";

const builder = createBuilder({
  root: process.cwd(),
  configFile: resolvedConfig,
});

await builder.buildApp();

Build Sequence

The buildApp() method runs a 5-step build pipeline:
  1. RSC environment build
    • Bundles server components
    • Applies react-server import condition
    • Generates RSC runtime modules
  2. SSR environment build
    • Bundles SSR runtime
    • Applies node import condition
    • Links to RSC chunks
  3. Client environment build
    • Bundles browser code
    • Code-splits by route and shared dependencies
    • Applies browser import condition
  4. Manifest generation
    • Maps source modules to output chunks
    • Used for preload hints and modulepreload
  5. Asset optimization
    • CSS extraction and minification
    • Image asset copying
    • Compression (gzip/brotli)

Code Splitting Strategy

Manual Chunks

Vinext uses a conservative code-splitting strategy optimized for real-world performance: From packages/vinext/src/index.ts:
function clientManualChunks(id: string): string | undefined {
  // React framework — always loaded, shared across all pages.
  // Isolating React into its own chunk is the single highest-value
  // split: it's ~130KB compressed, loaded on every page, and its
  // content hash rarely changes between deploys.
  if (id.includes("node_modules")) {
    const pkg = getPackageName(id);
    if (!pkg) return undefined;
    if (
      pkg === "react" ||
      pkg === "react-dom" ||
      pkg === "scheduler"
    ) {
      return "framework";
    }
    // Let Rollup handle all other vendor code via its default
    // graph-based splitting. This produces a reasonable number of
    // shared chunks (typically 5-15) based on actual import patterns,
    // with good compression efficiency.
    return undefined;
  }

  // Vinext shims — small runtime, shared across all pages.
  if (id.startsWith(shimsDir)) {
    return "vinext";
  }

  return undefined;
}

Output Configuration

const clientOutputConfig = {
  manualChunks: clientManualChunks,
  experimentalMinChunkSize: 10_000,
};
experimentalMinChunkSize merges tiny shared chunks (< 10KB) back into their importers:
  • Reduces HTTP request count
  • Improves gzip compression efficiency (small files restart the compression dictionary)
  • Adds ~5-15% wire overhead for many small files vs fewer larger chunks

Why Not Per-Package Splitting?

Many bundlers split every npm package into its own chunk. Vinext deliberately doesn’t: Problems with per-package splitting:
  • Creates 50-200+ chunks for typical apps (exceeds HTTP/2 sweet spot of ~25 requests)
  • gzip/brotli compress small files poorly (each file restarts with empty dictionary)
  • ES module evaluation has per-module overhead that compounds on mobile
  • No major Vite framework (Remix, SvelteKit, Astro, TanStack) uses per-package splitting
  • Next.js only isolates packages > 160KB
Rollup’s graph-based splitting handles this well:
  • Shared dependencies between routes get their own chunks automatically
  • Route-specific code stays in route chunks
  • Results in 5-15 vendor chunks based on actual usage patterns

Treeshaking

Aggressive Vendor Treeshaking

Vinext uses aggressive treeshaking to eliminate unused exports from vendor packages:
const clientTreeshakeConfig = {
  preset: "recommended" as const,
  moduleSideEffects: "no-external" as const,
};
moduleSideEffects: "no-external" means:
  • Local project modules: preserve side effects (CSS imports, polyfills)
  • node_modules packages: treat as side-effect-free unless exports are used
This is the single highest-impact optimization for large barrel-exporting libraries: Example: mermaid Without this setting, importing one diagram type includes all 15+ diagram renderers (~400KB). With "no-external", only the used renderer is included (~50KB). Example: @mui/material Importing Button from the barrel doesn’t pull in the entire library (80+ components). Only Button and its dependencies are included.

Why Not the “smallest” Preset?

Vite’s "smallest" preset also sets:
  • propertyReadSideEffects: false - Can break libraries that rely on property access side effects
  • tryCatchDeoptimization: false - Can break feature detection patterns
"recommended" + "no-external" gives most of the benefit with less risk.

Lazy Chunk Detection

Vinext computes which chunks are lazy-loaded (behind React.lazy(), next/dynamic, or manual import()) and excludes them from preload hints:
function computeLazyChunks(
  buildManifest: Record<string, ManifestChunk>
): string[] {
  // Collect all chunk files statically reachable from entries
  const eagerFiles = new Set<string>();
  const visited = new Set<string>();
  const queue: string[] = [];

  // Start BFS from all entry chunks
  for (const key of Object.keys(buildManifest)) {
    const chunk = buildManifest[key];
    if (chunk.isEntry) {
      queue.push(key);
    }
  }

  while (queue.length > 0) {
    const key = queue.shift()!;
    if (visited.has(key)) continue;
    visited.add(key);

    const chunk = buildManifest[key];
    if (!chunk) continue;

    eagerFiles.add(chunk.file);

    // Mark CSS as eager (avoid FOUC)
    if (chunk.css) {
      for (const cssFile of chunk.css) {
        eagerFiles.add(cssFile);
      }
    }

    // Follow only static imports — NOT dynamicImports
    if (chunk.imports) {
      for (const imp of chunk.imports) {
        if (!visited.has(imp)) {
          queue.push(imp);
        }
      }
    }
  }

  // Any JS file NOT in eagerFiles is a lazy chunk
  const lazyChunks: string[] = [];
  for (const key of Object.keys(buildManifest)) {
    const chunk = buildManifest[key];
    if (chunk.file && !eagerFiles.has(chunk.file) && chunk.file.endsWith(".js")) {
      lazyChunks.push(chunk.file);
    }
  }

  return lazyChunks;
}
Lazy chunks are stored in __VINEXT_LAZY_CHUNKS__ and excluded from <link rel="modulepreload"> and <script type="module"> tags. They’re fetched on demand when the dynamic import executes.

Static Export

The output: 'export' option renders all pages to static HTML at build time: From packages/vinext/src/build/static-export.ts:
export async function staticExportPages(
  options: StaticExportOptions,
): Promise<StaticExportResult> {
  const { server, routes, apiRoutes, pagesDir, outDir, config } = options;
  const result: StaticExportResult = {
    pageCount: 0,
    files: [],
    warnings: [],
    errors: [],
  };

  // Warn about API routes (not supported)
  if (apiRoutes.length > 0) {
    result.warnings.push(
      `${apiRoutes.length} API route(s) skipped — not supported with output: 'export'`
    );
  }

  // Gather all pages to render
  const pagesToRender: Array<{
    route: Route;
    urlPath: string;
    params: Record<string, string | string[]>;
  }> = [];

  for (const route of routes) {
    const pageModule = await server.ssrLoadModule(route.filePath);

    // Validate: getServerSideProps not allowed
    if (typeof pageModule.getServerSideProps === "function") {
      result.errors.push({
        route: route.pattern,
        error: `Page uses getServerSideProps which is not supported with output: 'export'`,
      });
      continue;
    }

    if (route.isDynamic) {
      // Dynamic route — must have getStaticPaths
      if (typeof pageModule.getStaticPaths !== "function") {
        result.errors.push({
          route: route.pattern,
          error: `Dynamic route requires getStaticPaths with output: 'export'`,
        });
        continue;
      }

      const pathsResult = await pageModule.getStaticPaths({ locales: [], defaultLocale: "" });
      const fallback = pathsResult?.fallback ?? false;

      if (fallback !== false) {
        result.errors.push({
          route: route.pattern,
          error: `getStaticPaths must return fallback: false with output: 'export'`,
        });
        continue;
      }

      const paths = pathsResult?.paths ?? [];
      for (const { params } of paths) {
        const urlPath = buildUrlFromParams(route.pattern, params);
        pagesToRender.push({ route, urlPath, params });
      }
    } else {
      // Static route
      pagesToRender.push({ route, urlPath: route.pattern, params: {} });
    }
  }

  // Render each page
  for (const { route, urlPath, params } of pagesToRender) {
    const html = await renderStaticPage({
      server,
      route,
      urlPath,
      params,
      pagesDir,
      config,
      AppComponent,
      DocumentComponent,
      headShim,
      dynamicShim,
      routerShim,
    });

    const outputPath = urlPath === "/" 
      ? "index.html" 
      : urlPath.slice(1) + "/index.html";
    const fullPath = path.join(outDir, outputPath);
    
    fs.mkdirSync(path.dirname(fullPath), { recursive: true });
    fs.writeFileSync(fullPath, html);
    
    result.files.push(outputPath);
    result.pageCount++;
  }

  return result;
}

Static Export Constraints

Pages Router:
  • ✅ Static pages
  • getStaticProps pages
  • ✅ Dynamic routes with getStaticPaths (must be fallback: false)
  • getServerSideProps (build error)
  • ❌ API routes (skipped with warning)
App Router:
  • ✅ Static pages
  • ✅ Dynamic routes with generateStaticParams()
  • ❌ Dynamic routes without generateStaticParams() (build error)
  • ❌ Route handlers (skipped with warning)

Cloudflare Workers Build

For Cloudflare Workers deployment, Vinext applies additional transformations:

Embedded Manifests

The SSR manifest and lazy chunk list are embedded as globals:
plugins.push({
  name: "vinext:cloudflare-build",
  apply: "build",
  config() {
    return {
      build: {
        rollupOptions: {
          output: {
            // Embed client manifest in SSR bundle
            banner: `
globalThis.__VINEXT_SSR_MANIFEST__ = ${JSON.stringify(ssrManifest)};
globalThis.__VINEXT_LAZY_CHUNKS__ = ${JSON.stringify(lazyChunks)};
globalThis.__VINEXT_CLIENT_ENTRY__ = ${JSON.stringify(clientEntry)};
            `,
          },
        },
      },
    };
  },
});
This eliminates the need to read manifest.json at runtime (Cloudflare Workers has no file system).

Native Module Stubbing

Native Node.js modules (sharp, resvg, satori) are auto-stubbed for Workers:
const NATIVE_MODULES = [
  "sharp",
  "@resvg/resvg-js",
  "@napi-rs/canvas",
  "lightningcss",
];

for (const mod of NATIVE_MODULES) {
  config.resolve.alias[mod] = "vinext/stubs/native-module";
}
The stub throws a descriptive error if the module is accessed at runtime:
// vinext/stubs/native-module.ts
export default new Proxy({}, {
  get(target, prop) {
    throw new Error(
      `Native module accessed at runtime. This module requires Node.js ` +
      `native bindings and cannot run on Cloudflare Workers. ` +
      `To fix: either avoid using this module or implement a pure-JS alternative.`
    );
  },
});

Production Optimizations

Compression

Vinext applies compression to static assets:
import compression from "compression";

const handler = compression()(baseHandler);
Cloudflare Workers automatically applies Brotli compression to responses, so no additional configuration is needed.

Cache Headers

Vinext sets cache headers based on content type: Hashed assets (JS/CSS with content hash in filename):
Cache-Control: public, max-age=31536000, immutable
HTML pages:
Cache-Control: public, max-age=0, must-revalidate
ISR pages:
Cache-Control: s-maxage=60, stale-while-revalidate
X-Vinext-Cache: HIT | MISS | STALE

Asset Collection

The Pages Router SSR entry collects assets for each page:
function collectAssetTags(manifest, moduleIds) {
  const m = manifest || globalThis.__VINEXT_SSR_MANIFEST__ || null;
  const tags = [];
  const seen = new Set();
  const lazySet = globalThis.__VINEXT_LAZY_CHUNKS__ 
    ? new Set(globalThis.__VINEXT_LAZY_CHUNKS__) 
    : null;

  // Inject client entry script
  if (globalThis.__VINEXT_CLIENT_ENTRY__) {
    const entry = globalThis.__VINEXT_CLIENT_ENTRY__;
    seen.add(entry);
    tags.push(`<link rel="modulepreload" href="/${entry}" />`);
    tags.push(`<script type="module" src="/${entry}" crossorigin></script>`);
  }

  if (m) {
    const allFiles = [];

    // Collect assets for page modules
    for (const id of moduleIds || []) {
      let files = m[id];
      if (!files) {
        // Try suffix match
        for (const mk in m) {
          if (id.endsWith("/" + mk) || id === mk) {
            files = m[mk];
            break;
          }
        }
      }
      if (files) {
        allFiles.push(...files);
      }
    }

    // Also inject shared chunks (framework, vinext runtime)
    for (const key in m) {
      const vals = m[key];
      for (const file of vals || []) {
        const basename = file.split("/").pop() || "";
        if (
          basename.startsWith("framework-") ||
          basename.startsWith("vinext-") ||
          basename.includes("vinext-client-entry")
        ) {
          allFiles.push(file);
        }
      }
    }

    for (const file of allFiles) {
      const normalized = file.charAt(0) === "/" ? file.slice(1) : file;
      if (seen.has(normalized)) continue;
      seen.add(normalized);

      if (normalized.endsWith(".css")) {
        tags.push(`<link rel="stylesheet" href="/${normalized}" />`);
      } else if (normalized.endsWith(".js")) {
        // Skip lazy chunks
        if (lazySet && lazySet.has(normalized)) continue;
        tags.push(`<link rel="modulepreload" href="/${normalized}" />`);
        tags.push(`<script type="module" src="/${normalized}" crossorigin></script>`);
      }
    }
  }

  return tags.join("\n  ");
}

Next Steps

Architecture Deep Dive

Core architecture and design decisions

RSC Integration

React Server Components integration

Virtual Modules

Virtual module system explained

Deployment

Deploy to Cloudflare Workers

Build docs developers (and LLMs) love