Skip to main content

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):

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:

Output Configuration

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:
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:
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:

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:
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:
The stub throws a descriptive error if the module is accessed at runtime:

Production Optimizations

Compression

Vinext applies compression to static assets:
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):
HTML pages:
ISR pages:

Asset Collection

The Pages Router SSR entry collects assets for each page:

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