Home JavaScript next.config.ts — Wrong images.remotePatterns Blocked All External Images in Production
Advanced 7 min · July 12, 2026
next.config.ts Deep Dive: All Configuration Options Explained

next.config.ts — Wrong images.remotePatterns Blocked All External Images in Production

A wildcard typo in images.remotePatterns blocked all external images in production for 6 hours.

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Written from production experience, not tutorials.

Follow
Production
production tested
July 12, 2026
last updated
1,787
articles · all by Naren
Before you start⏱ 25 min
  • Node.js 18+
  • Next.js 14+ project experience
  • Basic familiarity with JavaScript/TypeScript config files
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • images.remotePatterns requires BOTH protocol and hostname — missing either blocks all external images silently (no build error, images just 404 in production)
  • next.config.ts supports 50+ config options grouped into: Images, Routing (redirects/rewrites/headers), Build (webpack/turbopack), Experimental, Server (serverActions/output), and React (reactCompiler/strictMode)
  • output: 'export' disables ALL Next.js server features — API routes, ISR, middleware, draft mode. Use output: 'standalone' for self-hosted server deployments
  • serverActions allows configuring allowed origins — required when calling Server Actions from different domains (e.g., staging subdomain)
  • reactCompiler: true (experimental) automatically memoizes components — eliminates most useMemo/useCallback calls at the cost of ~5% longer build time
✦ Definition~90s read
What is next.config.ts?

next.config.ts (or next.config.js) is the single configuration entry point for Next.js applications. It controls every aspect of the framework's behaviour: how images are optimised and which external hosts are allowed (images.remotePatterns), how URLs are redirected or proxied (redirects, rewrites, headers), how the build pipeline works (webpack, transpilePackages, turbopack), what server features are enabled (serverActions, output), and how React components are compiled (reactCompiler, reactStrictMode).

Like a building security system that requires both a badge AND a face scan for entry — but the badge reader is broken and the security guard just shrugs.

The configuration file is evaluated at build time — not at request time. This means it has access to all environment variables, but changes to environment variables after the build starts are not reflected. The file must export a valid NextConfig object, either as a default export (ES modules) or via module.exports (CommonJS).

TypeScript users benefit from import type { NextConfig } from 'next' which provides autocomplete and type validation.

The file supports 50+ configuration options grouped into logical categories. The most commonly used categories: Images (remotePatterns, formats, device sizes), Routing (redirects, rewrites, headers), Build (webpack, transpilePackages, experimental), Server (serverActions, output, distDir), and React (reactCompiler, reactStrictMode, devIndicators).

Production discipline: the configuration should be self-documenting with comments explaining each option. Use environment variables for values that differ per environment (domains, API URLs, feature flags). Never maintain separate config files per environment — the file should be identical across environments, with differences controlled by environment variables.

Plain-English First

Like a building security system that requires both a badge AND a face scan for entry — but the badge reader is broken and the security guard just shrugs. That's images.remotePatterns with a bad configuration: no error, no warning, every external image silently denied. The worst bugs are the ones that produce no errors, just empty image placeholders across your entire site.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

next.config.ts (or next.config.js for the JavaScript SDK) is the single configuration file that controls every aspect of Next.js behaviour — from how images are optimised to how routes are rewritten to how the React compiler optimises your components. With 50+ configurable options spanning 10 categories, it is simultaneously the most powerful and most misunderstood file in a Next.js project.

The most common production issue: images.remotePatterns configured incorrectly. A single missing field, wrong wildcard placement, or omitted protocol causes every external image in your application to return a 404 or fall back to the unoptimized original URL — silently, with no build errors or runtime warnings. This issue is the #1 support complaint across Next.js projects.

This article covers every meaningful option in next.config.ts: images (the most error-prone), routing (redirects, rewrites, headers, middleware config), build tooling (webpack, turbopack, bundle analysis), server configuration (serverActions, output, experimental features), React configuration (reactCompiler, strictMode), and environment variable handling. For each option, you get the production-ready configuration and the mistakes to avoid.

By the end, you will be able to read any next.config.ts and understand exactly what every line does — and more importantly, which lines are likely to cause production incidents.

images.remotePatterns — The #1 Configuration Footgun

The images config block controls how Next.js optimises images — resizing, format conversion (WebP/AVIF), quality, and caching. It is the most frequently misconfigured option in production. The remotePatterns array defines which external image hosts are allowed for optimisation.

Each remotePattern entry requires three fields: protocol ('http' or 'https'), hostname (exact domain or wildcard pattern), and optionally port and pathname. All fields are validated at request time when the /_next/image endpoint processes an image. If any field does not match, the image request returns 404.

The wildcard syntax: matches this and all subdomains. .example.com matches example.com, cdn.example.com, a.b.example.com. *.example.com (single asterisk) matches only one level — cdn.example.com but NOT a.b.example.com. Single asterisk is often used accidentally and causes intermittent failures.

The production pattern: explicitly list every domain that serves images. Prefer exact hostnames over wildcards where possible. Always specify both protocol and hostname. Add a CI check that validates every remotePattern entry against a whitelist of expected domains — if a domain is missing from the config but appears in the content, the CI should warn.

next.config.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  images: {
    // REQUIRED for remote images with next/image
    // Each entry must have BOTH protocol AND hostname
    remotePatterns: [
      {
        protocol: 'https',
        // Exact match:
        hostname: 'cdn.example.com',
      },
      {
        protocol: 'https',
        // Wildcard: ** matches any subdomain (including none)
        hostname: '**.example.com',
      },
      {
        protocol: 'https',
        hostname: 'images.unsplash.com',
      },
      // Optional: restrict path and port
      {
        protocol: 'https',
        hostname: 'media.example.com',
        port: '443',
        pathname: '/uploads/**',
      },
    ],

    // Image optimisation settings
    formats: ['image/avif', 'image/webp'],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    minimumCacheTTL: 60 * 60 * 24, // 24 hours

    // Danger zone: disables image optimisation
    // unoptimized: true, // Only for static export
  },
}

export default nextConfig
Try it live
The Silent 404 Pattern
Every misconfigured remotePattern produces a 404 from /_next/image with NO build warning, NO runtime console error, and NO visible error in the UI — just an empty grey box. The only way to catch it is to check the browser Network tab or write a Playwright test that asserts naturalWidth > 0 on images.
Production Insight
An e-commerce team deployed with hostname: '.amazonaws.com' (single asterisk) but their images were served from s3.us-east-1.amazonaws.com (two subdomain levels). The single asterisk matched only one level. Images from bucket.s3.amazonaws.com (two levels) worked but bucket.s3.us-east-1.amazonaws.com (four levels) did not. The fix: hostname: '*.amazonaws.com'. Rule: always test remotePatterns with a curl command against the production /_next/image URL for every image domain you use.
Key Takeaway
remotePatterns requires protocol AND hostname — missing either blocks all external image optimisation silently.
Wildcard * matches any number of subdomain levels; matches only one — use ** unless you specifically need one-level matching.
Test remotePatterns in production with curl: /_next/image?url=https://your-domain.com/img.jpg&w=640&q=75 must return 200.
nextjs-config-deep-dive THECODEFORGE.IO Next.js Configuration Layers for External Images How remotePatterns interacts with other config options Image Component Layer with src | loader | sizes Configuration Layer images.remotePatterns | images.domains (legacy) | images.unoptimized Build & Optimization Layer next.config.ts | webpack | transpilePackages Deployment Layer output: 'standalone' | serverActions | static export Runtime Layer Image Optimization API | CDN | CORS headers THECODEFORGE.IO
thecodeforge.io
Nextjs Config Deep Dive

Redirects, Rewrites, and Headers — Async Functions, Not Objects

A common mistake: defining redirects, rewrites, or headers as static arrays instead of async functions. Next.js expects these to be async functions that return an array — because in some configurations, you need to fetch external data (like feature flags) to determine the routing config.

The syntax: async redirects() { return [ / array of rules / ] }. Each rule has source (incoming URL pattern), destination (target URL), and permanent (true = 301, false = 307) for redirects. headers similarly returns an array of { source, headers: [{ key, value }] } objects.

Path patterns: use :param for named parameters, for zero-or-more wildcard, :path for catch-all segments. The pattern /blog/:slug matches /blog/hello-world with slug = 'hello-world'. The pattern /api/:path* matches /api/v1/users with path = 'v1/users'.

Performance implication: redirects and headers are evaluated on every request. Having 100+ redirect rules can add 10-50ms per request on cold start. For large redirect sets, consider a middleware-based approach or a CDN-level redirect (Vercel Edge Config, Cloudflare Bulk Redirects) instead.

next.config.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  async redirects() {
    return [
      {
        source: '/old-blog/:slug',
        destination: '/blog/:slug',
        permanent: true, // 301 — SEO-friendly
      },
      {
        source: '/products/:id',
        has: [
          {
            type: 'cookie',
            key: 'vip',
            value: 'true',
          },
        ],
        destination: '/vip/products/:id',
        permanent: false, // 307 — temporary
      },
    ]
  },

  async rewrites() {
    return [
      // Proxy external API through Next.js
      {
        source: '/api/external/:path*',
        destination: 'https://external-api.com/:path*',
      },
    ]
  },

  async headers() {
    return [
      {
        source: '/:path*',
        headers: [
          { key: 'X-Frame-Options', value: 'DENY' },
          { key: 'X-Content-Type-Options', value: 'nosniff' },
          { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
        ],
      },
      {
        source: '/_next/static/:path*',
        headers: [
          { key: 'Cache-Control', value: 'public, max-age=31536000, immutable' },
        ],
      },
    ]
  },
}

export default nextConfig
Try it live
Source vs Destination Path Matching
Source paths use Next.js route pattern syntax (:param, , :path). Destination paths use standard URL syntax with :param interpolation. The paths must share the same parameter names — source :slug must have destination :slug. Mismatched parameter names cause runtime errors.
Production Insight
A team deployed a redirect with permanent: true (301) on an old URL structure. Three months later, they needed to revert the redirect but discovered that browsers had cached the 301 permanently — users on cached browsers could not access the new URL for months. Rule: always use permanent: false (307) for new redirects during a transition period. Switch to permanent: true only after you are certain the redirect is final. 301s are cached by browsers indefinitely.
Key Takeaway
redirects, rewrites, and headers must be async functions returning arrays — NOT static objects.
301 redirects (permanent: true) are cached by browsers indefinitely — use 307 during transition periods.
Path patterns use :param, , and :path syntax — parameters must match between source and destination.

webpack and transpilePackages — When Dependencies Break the Build

Next.js abstracts Webpack configuration behind its own build pipeline. Most of the time, you don't touch Webpack directly. When you do — to add custom loaders, modify resolve aliases, or adjust SplitChunks — you must use the webpack config function.

The webpack function receives the default config and the build context ({ isServer, webpack, nextRuntime }). You modify the config and return it. Common customisations: adding SVG loaders (SVGR), configuring module aliases, adding bundle analysis plugins, and modifying resolve fallbacks for Node.js modules in browser context.

transpilePackages is a simpler alternative to custom webpack config for one specific use case: transpiling external npm packages that are not ESM-compatible. Some packages distribute only as CommonJS or untranspiled TypeScript. Adding them to transpilePackages tells Next.js to run them through the Babel/SWC pipeline, making them compatible with the browser build.

The production insight: every custom webpack configuration increases the risk of build failures when Next.js updates. The Webpack API is considered internal — Vercel changes it between major versions without deprecation warnings. Minimise custom webpack config and prefer Next.js-native solutions (transpilePackages, next/dynamic, next/image) whenever possible.

next.config.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  // Transpile specific packages to ESM
  transpilePackages: [
    '@acme/ui',
    'some-esm-incompatible-lib',
  ],

  webpack: (config, { isServer, webpack }) => {
    // SVG loader example
    config.module.rules.push({
      test: /\.svg$/,
      use: ['@svgr/webpack'],
    })

    // Module aliases
    config.resolve.alias = {
      ...config.resolve.alias,
      '@': __dirname + '/src',
    }

    // Bundle analysis (run with ANALYZE=true)
    if (process.env.ANALYZE === 'true') {
      const { BundleAnalyzerPlugin } =
        require('webpack-bundle-analyzer')
      config.plugins.push(
        new BundleAnalyzerPlugin({
          analyzerMode: 'static',
          reportFilename: isServer
            ? '../analyze/server.html'
            : './analyze/client.html',
        })
      )
    }

    return config
  },

  // Custom server component module rules
  serverComponentsExternalPackages: [
    'mongoose',
    'prisma',
    'typeorm',
  ],
}

export default nextConfig
Try it live
Webpack Config Breaks Between Next.js Versions
The webpack config function is considered an internal API. Next.js makes breaking changes to the config object shape between major versions without notice. Pin your Next.js version and test custom webpack config in CI when upgrading. Prefer transpilePackages over custom webpack for dependency transpilation.
Production Insight
A project with 15 custom webpack loaders (SVGR, GraphQL codegen, raw-loader, CSS modules customisations) could not upgrade from Next.js 14 to 15 because three loaders broke. The migration required rewriting the custom webpack config for the new API shape, taking 3 developer-days. Rule: minimising custom webpack config is an investment in future upgrade speed. Each custom loader is a migration risk.
Key Takeaway
Use transpilePackages for ESM-incompatible dependencies instead of custom webpack config.
The webpack config function is an internal API — it breaks between Next.js versions without warning.
Custom webpack config is justified for: SVG loaders, bundle analysis, module aliases, SplitChunks customisation.
images.remotePatterns vs images.domains Modern vs legacy approach for allowing external images images.remotePatterns images.domains Configuration Format Array of objects with protocol, hostname Simple array of domain strings Wildcard Support Supports ** for subdomains and * for par No wildcard support; exact domains only Granularity Can restrict by pathname and port Only domain-level restriction Deprecation Status Recommended for Next.js 13+ Deprecated in Next.js 14 Backward Compatibility Requires migration from domains Still works but not recommended THECODEFORGE.IO
thecodeforge.io
Nextjs Config Deep Dive

output — Static Export vs Server vs Standalone

The output option determines how Next.js builds your application. The default (no output specified) builds for a Node.js server with all Next.js features — API routes, ISR, middleware, draft mode, server actions. The output includes .next/ with server-side code that next start runs.

output: 'export' — static HTML export. Builds every page as static HTML/CSS/JS files. Output goes to out/ directory. Tradeoffs: zero server runtime (fastest, cheapest to host), NO API routes, NO ISR (all pages built at deploy time), NO middleware, NO draft mode, NO server actions. Images must be unoptimized. Suitable for: marketing sites, documentation, blogs where every page is pre-built.

output: 'standalone' — builds a self-contained deployment with minimal dependencies. Output includes a standalone/ directory with the server code and a minimal node_modules. Tradeoffs: larger output (includes server runtime), enables all Next.js features. Requires a Node.js runtime. Suitable for: Docker deployments, self-hosted production, any app that needs ISR or API routes.

The mistake: teams choose output: 'export' for performance, then discover they need ISR or API routes and must migrate — a significant effort. Choose output based on your feature requirements, not perceived performance.

next.config.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  // Option 1: Static export (no server needed)
  // Output goes to `out/` directory
  // Caveats: no API routes, no ISR, no middleware, no draft mode
  // output: 'export',

  // Option 2: Self-hosted server (Docker, Node.js)
  // Output goes to `.next/standalone/`
  // Includes minimal node_modules — deploy to any Node.js host
  // output: 'standalone',

  // Option 3: Default — Vercel or full Node.js deployment
  // No output option needed
  // All features enabled

  // For static export, images must be unoptimized
  // images: { unoptimized: true },
}

export default nextConfig
Try it live
Static Export Checklist
If you choose output: 'export', you must: disable image optimization (images.unoptimized: true), remove all API routes, remove all getServerSideProps, remove middleware (fetch called in middleware requires server), remove draft mode, and remove server actions. These all fail at build time with clear errors — but it's easier to choose the right option initially than to strip features.
Production Insight
A team built a blog with output: 'export' then needed a search API endpoint. They had to deploy a separate serverless function (Cloudflare Worker) for the search API, creating two separate deployment pipelines and a CORS configuration challenge. Two months later they migrated to output: 'standalone' and consolidated everything into a single Next.js deployment. Rule: if you think you might need ANY server-side feature in the future, do not start with static export. The migration cost is high.
Key Takeaway
output: 'export' = static HTML, no server features, fastest hosting. output: 'standalone' = self-hosted server, all features enabled.
Static export disables: API routes, ISR, middleware, draft mode, server actions, image optimisation.
Choose output based on feature requirements, not perceived performance — migrating from export to standalone is costly.

serverActions — The Origin Check That Blocks Cross-Domain Requests

Server Actions in Next.js 16 include a security feature: they check the Origin header of incoming requests against an allowed list. If your app is accessed from multiple domains — for example, www.example.com and staging.example.com, or if you use a reverse proxy that changes the origin — Server Actions will return 403 Forbidden for requests from unlisted origins.

The configuration is serverActions.allowedOrigins: string[]. Each entry should be the origin (protocol + hostname, no path). The check applies to all Server Action calls, including those from client components and forms.

The production issue: teams deploy to a staging subdomain, test Server Actions, and get 403 errors. The staging domain is not in the allowed origins list. The fix is adding both the production and staging domains to the config. For local development, http://localhost:3000 must also be in the list.

Security consideration: the origin check prevents CSRF-style attacks where a malicious site tricks a user's browser into calling your Server Action. Only add trusted origins to the allowed list. Use environment variables to differentiate origins per environment.

next.config.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  serverActions: {
    // REQUIRED for Server Actions called from different domains
    // Without this, cross-origin Server Action calls return 403
    allowedOrigins: [
      'https://www.example.com',
      'https://example.com',
      process.env.VERCEL_URL && `https://${process.env.VERCEL_URL}`,
      process.env.STAGING_URL,
      'http://localhost:3000',
    ].filter(Boolean) as string[],

    // Optional: body size limit for Server Actions
    // Default: 1MB — increase for file uploads via Server Actions
    bodySizeLimit: '2mb',
  },
}

export default nextConfig
Try it live
Environment-Specific Origins
Use environment variables to dynamically build the allowed origins list. process.env.VERCEL_URL is automatically set on Vercel deployments. For self-hosted, pass ALLOWED_ORIGINS as a comma-separated env var and parse it in next.config.ts.
Production Insight
An e-commerce site used Server Actions for add-to-cart functionality. The marketing team created an A/B test on a subdomain test.example.com that called the production Server Action. Every A/B test interaction returned 403 because test.example.com was not in allowedOrigins. The fix: added **.example.com as a wildcard pattern (not supported — wildcards don't work in allowedOrigins). The actual fix: added every known subdomain explicitly. Rule: if you have more than 3 domains, use a CDN with origin consolidation or switch to API routes with manual origin validation instead of Server Actions.
Key Takeaway
serverActions.allowedOrigins lists which domains can call Server Actions — missing origins cause 403 errors.
Use environment variables for environment-specific origins (localhost, staging, production).
The origin check is a CSRF protection — add only trusted domains, but remember every development and staging subdomain.

experimental — Where Future Features Live (and Break)

The experimental config block contains features that are not yet stable. Using experimental features gives you access to cutting-edge capabilities but comes with risks: the API may change without notice, the feature may be removed, or it may have bugs that affect production.

Key experimental features in Next.js 16: mdxRs (Rust-based MDX compiler — faster builds but different output), turbo (enable Turbopack in development — default since 15.3), serverActions (now stable in 16, moved from experimental), reactCompiler (automatic memoization with React Forget), ppr (Partial Prerendering — experimental), optimizePackageImports (automatic tree shaking of barrel files from specific packages).

The discipline: pin the exact Next.js version when using experimental features. Never use latest in your package.json. Add a comment explaining why each experimental flag is enabled, what it does, and what the migration path is when it becomes stable. Audit experimental flags every quarter and remove those that have graduated to stable.

next.config.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  experimental: {
    // Rust-based MDX compiler — 5-10x faster MDX compilation
    // Graduated from experimental in some Next.js 16 releases
    mdxRs: true,

    // React Compiler (React Forget) — automatic memoization
    // Automatically adds useMemo/useCallback calls
    // Build time impact: ~5% longer
    reactCompiler: true,

    // Partial Prerendering - static shell + dynamic holes
    // Combines static generation with streaming SSR
    ppr: true,

    // Optimise imports from specific packages
    // Automatically treeshakes barrel files
    optimizePackageImports: [
      'lucide-react',
      '@radix-ui/react-icons',
      '@mui/material',
    ],

    // Scroll restoration options
    scrollRestoration: true,
  },
}

export default nextConfig
Try it live
Experimental Feature Discipline
Every experimental feature you enable is a risk. Before enabling: (1) pin Next.js to an exact version, (2) add a comment in the config explaining why it's enabled and what behaviour it changes, (3) test thoroughly in staging, (4) add a calendar reminder to check if the feature has graduated to stable.
Production Insight
A team enabled experimental.ppr: true during migration from Next.js 13 to 14. Six months later, they noticed stale data in dynamic sections of otherwise-static pages. The PPR implementation had changed between 14.0 and 14.2 — the revalidation behaviour was slightly different. The fix: disabling PPR, which required removing the experimental flag and adjusting page-level caching. Rule: experimental features change between minor versions. Pin both the Next.js version AND the experimental feature version in your docs.
Key Takeaway
Experimental features provide early access but risk API changes, bugs, and removal — pin Next.js version exactly.
Key experimental features: mdxRs (Rust MDX), reactCompiler (auto memoization), ppr (partial prerendering).
Document why each experimental flag is enabled and audit quarterly for features that have graduated to stable.

reactCompiler — Automatic Memoization Without useMemo

The React Compiler (formerly React Forget) is a build-time tool that automatically adds useMemo and useCallback calls to your components. It analyses component renders and determines which values need memoization to prevent unnecessary re-renders. Enabled via experimental.reactCompiler: true.

The impact: for a typical app with 50+ components, the compiler can eliminate 80-90% of manual memoization code. Components that re-render unnecessarily due to missing useMemo or inline function references are automatically optimised. The developer no longer needs to think about React.memo, useMemo, or useCallback — the compiler handles it.

The tradeoff: build time increases by approximately 5% (more for large component trees). The compiler's output is conservative — it may add memoization that is unnecessary, slightly increasing the generated bundle. Runtime behaviour is identical — the compiler only adds memoization that preserves existing semantics.

The production consideration: enable the compiler on a route-by-route basis if needed via the compilationMode option. For existing apps, enable it in a staging environment first and use React DevTools Profiler to verify that unnecessary re-renders are eliminated without breaking component behaviour.

next.config.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  experimental: {
    reactCompiler: {
      // Compilation mode:
      // 'all' — compile all components (default)
      // 'strict' — compile all, but error on patterns the
      //   compiler cannot safely optimise
      compilationMode: 'all',

      // Optional: suppress specific diagnostics
      // panicThreshold: 'error',
    },
    // Alternative: boolean shorthand
    // reactCompiler: true,
  },
}

export default nextConfig
Try it live
Verify With React DevTools Profiler
After enabling reactCompiler, use the React DevTools Profiler to verify that components are memoized correctly. Look for components that re-render without changed props — these should be eliminated by the compiler. If you see unexpected behaviour, use compilationMode: 'strict' to surface warnings about patterns the compiler cannot safely optimise.
Production Insight
A dashboard app with complex filtering logic had manually added useMemo and useCallback across 30 components. Enabling the React Compiler reduced the codebase by 450 lines of manual memoization code. Performance was identical — the compiler's output was as good as the manual memoization. Build time increased from 120s to 127s (6%). The team's next sprint removed all manual memoization calls and relied entirely on the compiler. Rule: the React Compiler eliminates 80-90% of manual memoization code with zero runtime cost. Enable it on new projects from day one.
Key Takeaway
reactCompiler automatically adds useMemo/useCallback — eliminates 80-90% of manual memoization code.
Build time increases ~5% but runtime behaviour is identical to hand-crafted memoization.
Use React DevTools Profiler to verify compiler output — compilationMode 'strict' surfaces unsafe patterns.

Turbopack Configuration — Matching the Rust Bundler to Your Needs

Turbopack is the default development bundler in Next.js 16. It replaces Webpack for next dev with a Rust-based implementation that is approximately 10x faster for cold starts and substantially faster for Hot Module Replacement (HMR). The configuration is minimal because Turbopack aims to work out of the box.

The turbo config block in experimental lets you customise Turbopack-specific settings: resolve aliases, load rules for specific file types, and webpack loader compatibility. Most projects don't need this — the default Turbopack config handles TypeScript, JSX, CSS, CSS Modules, and static assets.

Key limitation: Turbopack does not support custom webpack loaders. If you use SVGR for SVG imports, custom CSS post-processors, or GraphQL codegen imports, those features won't work with Turbopack. You must either: (1) disable Turbopack and use Webpack (--no-turbo flag), (2) convert the loader to a Turbopack-compatible transform, or (3) pre-process those files outside the bundler.

The production decision: use Turbopack for development (default, faster iteration). Use Webpack for production builds (more mature optimisation, custom loader support). Do not enable Turbopack for production builds unless you have verified identical output.

next.config.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  experimental: {
    turbo: {
      // Resolve aliases (equivalent to webpack resolve.alias)
      resolveAlias: {
        '@': './src',
        '@components': './src/components',
      },

      // Load rules for specific file extensions
      rules: {
        '*.svg': {
          loaders: ['@svgr/webpack'],
          as: '*.js',
        },
      },

      // Tree shaking options
      treeShaking: true,
    },
  },

  // Force Webpack for production builds
  // webpack: (config) => { ... return config },
}

export default nextConfig
Try it live
Turbopack for Dev, Webpack for Prod
Turbopack excels at development speed — instant startup, instant HMR. Webpack excels at production optimisation — smaller bundles, more mature code splitting. Use Turbopack for next dev (default in 16). Let Turbopack handle production only if you disable all custom webpack loaders and verify bundle output is equivalent.
Production Insight
A team enabled Turbopack for production builds and saw inconsistent chunk splitting: some pages that previously loaded in 1 chunk now loaded in 4, increasing total blocking time by 15%. The issue: Turbopack's code splitting algorithm grouped shared dependencies differently than Webpack. The fix: reverting to Webpack for production builds preserved the established chunk structure. Rule: Turbopack production builds are safe only for simple apps without custom webpack config. For complex apps, use Webpack for production until Turbopack's production mode matures.
Key Takeaway
Turbopack is default for dev (10x faster HMR). Webpack still recommended for production builds in complex apps.
Turbopack does NOT support custom webpack loaders (SVGR, raw-loader, GraphQL codegen).
Turbo config block supports resolveAlias and rules — but keep it minimal; the defaults work for most projects.

Environment Variables — Public vs Private and the NEXT_PUBLIC_ Prefix

Next.js handles environment variables through .env files (.env.local, .env.production, .env.development) and the env config option in next.config.ts. The critical distinction: which variables are available on the server vs the client.

Variables without the NEXT_PUBLIC_ prefix are server-only. They are available in getServerSideProps, API routes, Server Components, middleware, and next.config.ts itself — but NOT in client-side JavaScript. If you reference process.env.API_SECRET in a client component, it will be undefined at runtime (and replaced with the literal string at build time, potentially leaking the value if used in a string).

Variables prefixed with NEXT_PUBLIC_ are inlined at build time and available in client-side code. They are replaced with their actual values during the build — meaning any change to a NEXT_PUBLIC_ variable requires a rebuild to take effect. Use NEXT_PUBLIC_ for values that are safe to expose to the browser: API base URLs, feature flag names, public keys.

The env option in next.config.ts adds variables that are available in both server and browser contexts — essentially treating them as if they had the NEXT_PUBLIC_ prefix. Use this sparingly — it's better to be explicit with prefixes.

next.config.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  // Variables available to both server and client
  // Equivalent to NEXT_PUBLIC_ prefix
  env: {
    SITE_NAME: 'TheCodeForge',
    API_BASE_URL: process.env.API_BASE_URL,
  },

  // Server-only variables (defined in .env.local, .env.production)
  // process.env.DATABASE_URL — only in server components, API routes
  // process.env.API_SECRET — never exposed to browser

  // Public variables (NEXT_PUBLIC_ prefix)
  // NEXT_PUBLIC_GA_ID — inlined at build time, available in browser

  // Stale while revalidate config
  // httpAgentOptions: {
  //   keepAlive: true,
  // },
}

export default nextConfig

// .env.local (never committed):
// DATABASE_URL=postgres://...
// API_SECRET=sk-...
// NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
Try it live
Never Store Secrets in next.config.ts
The env option in next.config.ts inlines values into the client bundle at build time. Any value in env (or prefixed with NEXT_PUBLIC_) is visible in the browser's rendered HTML and JavaScript source. Never put API keys, database URLs, or auth tokens in the env config option or as NEXT_PUBLIC_ variables.
Production Insight
A developer accidentally placed a database connection string in .env.production and imported it in a client component via NEXT_PUBLIC_DATABASE_URL. The connection string was inlined into the client-side JavaScript bundle, accessible to every user via View Source. The database was internet-exposed and was compromised within 2 hours. Rule: if it's a secret, keep the NEXT_PUBLIC_ prefix off. If it's safe for the browser, use NEXT_PUBLIC_ explicitly. The prefix system is a security boundary — respect it.
Key Takeaway
Variables without NEXT_PUBLIC_ prefix are server-only — never available in client-side code.
NEXT_PUBLIC_ variables are inlined at build time — changing them requires a rebuild.
The env config option makes variables available in both server and client — use sparingly and never for secrets.

Full Production Config — Putting It All Together

The production-grade next.config.ts combines all the patterns covered above into a single, well-documented configuration file. Every option is explained with a comment. Experimental features are gated by environment variables. Image remotePatterns are validated. Security headers are set. Environment-specific overrides are handled.

The key principle: the configuration should be self-documenting. Anyone on the team should be able to read the file and understand what every option does and why it's set. Use TypeScript for type safety — import type { NextConfig } from 'next' gives you autocomplete and validation.

Version control the configuration file in every environment. The file should be identical across development, staging, and production — differences should be controlled by environment variables, not by having different config files. Use .env.local for local overrides.

next.config.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import type { NextConfig } from 'next'
import createMDX from '@next/mdx'

const nextConfig: NextConfig = {
  // --- Images ---
  images: {
    remotePatterns: [
      { protocol: 'https', hostname: 'cdn.example.com' },
      { protocol: 'https', hostname: '**.vercel-storage.com' },
    ],
    formats: ['image/avif', 'image/webp'],
    minimumCacheTTL: 86400,
  },

  // --- Routing ---
  async redirects() {
    return [
      { source: '/old/:path*', destination: '/new/:path*', permanent: true },
    ]
  },
  async rewrites() {
    return [
      { source: '/api/external/:path*', destination: 'https://api.example.com/:path*' },
    ]
  },
  async headers() {
    return [
      {
        source: '/:path*',
        headers: [
          { key: 'X-Frame-Options', value: 'DENY' },
          { key: 'X-Content-Type-Options', value: 'nosniff' },
          { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
          { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
        ],
      },
    ]
  },

  // --- Build & Bundler ---
  transpilePackages: ['@acme/ui', 'three'],
  experimental: {
    // Enable only if using MDX
    mdxRs: true,
    // Auto-memoization (React Forget)
    reactCompiler: process.env.ENABLE_REACT_COMPILER === 'true',
    // Optimise barrel file imports
    optimizePackageImports: ['lucide-react', '@radix-ui/react-icons'],
  },

  // --- Server ---
  serverActions: {
    allowedOrigins: [
      'https://example.com',
      process.env.VERCEL_URL && `https://${process.env.VERCEL_URL}`,
      'http://localhost:3000',
    ].filter(Boolean) as string[],
  },

  // --- Other ---
  productionBrowserSourceMaps: false,
  reactStrictMode: true,
  poweredByHeader: false,
}

const withMDX = createMDX({
  options: {
    remarkPlugins: [],
    rehypePlugins: [],
  },
})

export default withMDX(nextConfig)
Try it live
Type Safety With Import Type
Use import type { NextConfig } from 'next' for TypeScript autocomplete and validation. This catches typos ('headders' instead of 'headers') at the type-checking stage, not in production.
Production Insight
A production next.config.ts should be one of the most-reviewed files in your repository — it controls routing, security, caching, and image handling. A single typo can cause a production incident. The most stable deployments treat the config file with the same discipline as infrastructure code: reviewed, tested, versioned, and deployed through CI. Rule: the config file is infrastructure. Treat it accordingly.
Key Takeaway
The production config should be self-documenting with comments explaining every option.
Use environment variables for environment-specific values — never have different config files per environment.
The config file is infrastructure code — review it with the same rigour as your application code.
● Production incidentPOST-MORTEMseverity: high

6 Hours of Broken Images — A Single Typo in remotePatterns

Symptom
All external product images returned 404. Browser DevTools showed requests to /_next/image?url=https://cdn.example.com/product.jpg returning 404. Next.js image optimizations silently failed. The site looked broken — empty grey boxes where product images should be. Internal monitoring did not catch it because image 404s were not tracked as errors.
Assumption
The team assumed that a wildcard pattern in hostname would match all subdomains. They wrote hostname: '*.example.com' and expected it to match cdn.example.com, images.example.com, and static.example.com. They also assumed that omitting the protocol field would default to both HTTP and HTTPS.
Root cause
Two simultaneous errors in images.remotePatterns. First, the protocol field was omitted — the team assumed it defaulted to HTTPS. It does not. When protocol is missing, Next.js does not match any protocol, so no remote images are optimised. Second, the wildcard syntax .example.com is incorrect — remotePatterns uses as the wildcard prefix, not . The correct pattern is .example.com. The combination: missing protocol + wrong wildcard syntax = zero remote images passed the validation. Every image request to /_next/image failed a pattern check and returned 404. The images appeared in development because Next.js uses unoptimized images in dev mode — the team tested locally, saw images, and assumed the config was correct. Production uses optimized images via /_next/image, which enforces remotePatterns. The fix: corrected remotePatterns to include protocol: 'https' and hostname: '.example.com'. After deploying the fix, all images rendered within 30 seconds (ISR regenerated the optimised images). Added a CI check that validates remotePatterns against a list of expected image domains. Added visual regression testing that catches broken images in staging before deploy.
Fix
Fixed remotePatterns configuration: added protocol: 'https' and changed hostname from '.example.com' to '*.example.com'. Added a validation script in CI that checks the remotePatterns config against a whitelist of expected domains and fails if any domain is missing. Added a Playwright test that loads each page type and asserts that product images have non-zero naturalWidth (not broken). The 6-hour gap between deploy and detection prompted adding synthetic monitoring that checks image load status on the homepage every minute.
Key lesson
  • images.remotePatterns requires BOTH protocol AND hostname — missing either silently blocks all remote image optimisation with no build error
  • The wildcard prefix for remotePatterns hostname is * (double asterisk), NOT (single asterisk) — **.example.com matches all subdomains
  • Development mode does NOT optimise images — what works in dev may fail in production where /_next/image enforces remotePatterns validation
  • Add image monitoring: visual regression tests that catch broken images, and synthetic checks that verify image URLs return 200 in production
Production debug guideDiagnose configuration errors in Next.js production deployments4 entries
Symptom · 01
External images return 404 in production but work in development
Fix
Check images.remotePatterns in next.config.ts. Development uses unoptimized images (no remotePatterns check). Production routes through /_next/image which validates against remotePatterns. Verify protocol AND hostname are correctly specified with proper wildcard syntax
Symptom · 02
Redirects or rewrites not working in production
Fix
Check that redirects/headers/rewrites are exported as async functions returning arrays, not as plain objects. Verify that sources and destinations use correct path pattern syntax (/:path* for catch-all, /:id for parameter). Test with curl -I to check response status codes
Symptom · 03
Server Actions throw 403 in production
Fix
Check serverActions.allowedOrigins — if your app is accessed from multiple domains (e.g., staging.example.com and www.example.com), add all origins. Server Actions include an origin check that rejects requests from unlisted origins
Symptom · 04
Build succeeds but pages fail with 'Module not found' errors
Fix
Check webpack configuration — custom loaders that work in dev may fail in production if they depend on dev-only plugins. Verify transpilePackages for external packages that need transpilation. Check resolve.alias for path resolution errors
★ next.config.ts Quick Debug ReferenceFast commands for diagnosing next.config.ts configuration issues
Images with external URLs return 404 via next/image
Immediate action
Check remotePatterns configuration for protocol and hostname
Commands
cat next.config.ts | grep -A20 'images' || cat next.config.js | grep -A20 'images'
curl -sI 'http://localhost:3000/_next/image?url=https://external.com/img.jpg&w=640&q=75' | head -10
Fix now
Add both protocol ('https') and hostname ('**.example.com') to remotePatterns. Verify with curl that the image URL returns 200
Rewrites are not applied in production+
Immediate action
Check that rewrites is an async function returning array
Commands
node -e "const c = require('./next.config.js'); console.log(typeof c.rewrites)"
curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/rewritten-path
Fix now
Ensure rewrites is an async function: async rewrites() { return [{ source: '/old', destination: '/new' }] } — not a static array
Server Action returns 403 in production+
Immediate action
Check serverActions allowedOrigins config
Commands
cat next.config.ts | grep -A10 'serverActions' || cat next.config.js | grep -A10 'serverActions'
grep -rn "serverActions" next.config.ts next.config.js --include='*.{ts,js}'
Fix now
Add all origins that need to call Server Actions to serverActions.allowedOrigins. Example: serverActions: { allowedOrigins: ['staging.example.com', 'https://staging.example.com', 'http://localhost:3000'] }

Key takeaways

1
images.remotePatterns is the #1 config mistake
requires BOTH protocol and hostname, wildcard * not , and has no build-time validation
2
redirects, rewrites, and headers must be async functions returning arrays
static objects break silently in production
3
output
'export' disables ALL server features (ISR, API routes, middleware, draft mode, server actions) — choose based on feature requirements
4
serverActions.allowedOrigins prevents cross-origin calls
add every domain that calls Server Actions, including staging and localhost
5
The React Compiler (reactCompiler) eliminates 80-90% of manual memoization with zero runtime cost and ~5% longer build time
6
The config file is infrastructure code
review it with the same rigour as application code, version it in every environment
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
You deploy a Next.js 16 app to production and all external images return...
Q02SENIOR
Compare output: 'export', output: 'standalone', and the default server o...
Q03SENIOR
What is the React Compiler (experimental.reactCompiler) and how does it ...
Q04SENIOR
A Server Action returns 403 Forbidden in production but works in local d...
Q01 of 04SENIOR

You deploy a Next.js 16 app to production and all external images return 404 via next/image. The images work fine in local development. Walk through your debugging process.

ANSWER
First, I would confirm the symptom: open Chrome DevTools Network tab on a page with external images. Look for requests to /_next/image?url=... — these should return 200 with an optimized image but return 404. This confirms the image optimization pipeline is rejecting the request. Next, I'd check the next.config.ts file's images.remotePatterns configuration. The most likely issue: the remotePatterns array is either empty, missing the specific domain, or has incorrect fields. I'd run: cat next.config.ts | grep -A20 'images' to inspect. Common root causes: (1) protocol field missing — remotePatterns requires BOTH protocol and hostname. If protocol is omitted, no protocol matches. (2) Wrong wildcard syntax — .example.com (single asterisk) matches only one subdomain level. *.example.com (double asterisk) is needed for multiple levels. (3) The domain is missing from remotePatterns entirely — a common issue when adding a new image CDN. To verify the fix, I'd test with curl: curl -sI 'https://example.com/_next/image?url=https://cdn.example.com/img.jpg&w=640&q=75'. A 200 response confirms the fix. I'd also add a Playwright test that asserts images on key pages have naturalWidth > 0 to catch this in CI. Prevention: add a validation script that checks that every domain in your content's image URLs is covered by a remotePatterns entry. Fail CI if a domain is missing.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between rewrites and redirects in next.config.ts?
02
How do I configure next.config.ts for multiple environments (dev, staging, production)?
03
Can I use environment variables inside next.config.ts?
04
What does reactStrictMode do and should I enable it?
05
How do I configure the build output directory in next.config.ts?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Written from production experience, not tutorials.

Follow
Verified
production tested
July 12, 2026
last updated
1,787
articles · all by Naren
🔥

That's Next.js. Mark it forged?

7 min read · try the examples if you haven't

Previous
MDX, Content Management, and Draft Mode in Next.js
48 / 56 · Next.js
Next
Pages Router to App Router Migration: Step-by-Step Guide