How to Fix: Image render diff: node .next/standalone/server.js” & next satrt

7 min read

Why node .next/standalone/server.js and next start can produce different rendered images in Next.js

If your visual regression suite shows different screenshots depending on whether the app is started with node .next/standalone/server.js or next start, the problem is usually not the browser snapshot tool. It is a runtime behavior mismatch caused by differences in how the production server is built, which files are bundled into the standalone output, how assets are resolved, and which environment variables are available at runtime.

Understanding the Root Cause

Although both commands run a production build, they do not start the app in exactly the same way.

next start launches Next.js using the full build output and the normal production server behavior. By contrast, .next/standalone/server.js is generated for standalone deployment, where Next.js traces dependencies and creates a minimized server package intended for containers or minimal Node.js environments.

Visual differences usually happen for one or more of these technical reasons:

  • Asset resolution differences: static files, fonts, or images may be served from a different base path, copied incompletely, or referenced differently in standalone mode.
  • Missing runtime files: standalone output may not include everything your app reads dynamically, such as local JSON, font files, i18n resources, template fragments, or files loaded through fs.
  • Environment variable drift: one startup mode may receive different HOST, PORT, NODE_ENV, NEXT_PUBLIC_*, CDN, or image-related variables.
  • Image optimization behavior: the Next.js Image Optimization pipeline may behave differently if loaders, remote patterns, sharp availability, caching, or deployment paths differ.
  • Base path or asset prefix issues: if basePath or assetPrefix is configured incorrectly, generated markup can point to different URLs, which changes the final rendered image.
  • Font fallback changes: if custom fonts are not present or not loaded identically, text reflows slightly, which is enough to trigger screenshot diffs.
  • Hydration mismatches: non-deterministic rendering such as dates, random values, locale-sensitive formatting, or client-only measurements can produce different DOM states during capture.

In practice, the issue is rarely that next start is correct and standalone is broken by default. The real issue is that standalone mode exposes assumptions in the app about filesystem access, asset placement, or runtime configuration.

Step-by-Step Solution

The safest fix is to make both startup paths consume the same build artifacts, same public assets, and same runtime configuration.

1. Enable standalone output explicitly

In your Next.js config, make sure standalone mode is configured intentionally.

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
}

module.exports = nextConfig

This tells Next.js to generate the minimal server bundle under .next/standalone.

2. Build the application cleanly

Delete old build artifacts before comparing rendering results.

rm -rf .next
npm run build

Or with another package manager:

pnpm build
# or
yarn build

3. Copy required static assets when using standalone

A very common mistake is starting .next/standalone/server.js without ensuring that public assets and static build assets are available beside it.

For standalone deployments, you typically need these directories present:

  • public
  • .next/static

Example deployment preparation:

mkdir -p .next/standalone/.next
cp -r .next/static .next/standalone/.next/static
cp -r public .next/standalone/public

Then run:

node .next/standalone/server.js

If these files are missing, image URLs, CSS, font files, and client bundles may resolve differently, causing visual diffs.

4. Verify that both modes use the same environment variables

Create a consistent production startup process.

NODE_ENV=production HOSTNAME=0.0.0.0 PORT=3000 node .next/standalone/server.js
NODE_ENV=production HOSTNAME=0.0.0.0 PORT=3000 next start -p 3000

If you depend on custom variables, confirm they are identical in both runs:

echo $NODE_ENV
echo $HOSTNAME
echo $PORT

Also verify deployment-specific values such as image domains, CDN origin, and feature flags.

5. Audit next/image configuration

If screenshot diffs involve image size, cropping, missing placeholders, or different optimization results, inspect your image config.

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'example-cdn.com',
      },
    ],
  },
}

module.exports = nextConfig

Check whether the standalone environment can reach all remote image hosts and whether the same loader is being used.

6. Check for filesystem-dependent rendering

If a page reads local files at runtime, standalone tracing may not include them unless they are imported or copied properly.

Problematic pattern:

import fs from 'fs'
import path from 'path'

export async function getStaticProps() {
  const filePath = path.join(process.cwd(), 'data', 'content.json')
  const raw = fs.readFileSync(filePath, 'utf8')
  return {
    props: {
      content: JSON.parse(raw),
    },
  }
}

If data/content.json is not present in the runtime package, rendered output can differ or silently fall back.

Safer options:

  • Import static data at build time.
  • Copy required runtime files into the deployment package.
  • Avoid ad hoc filesystem reads in production server code unless they are intentionally packaged.

7. Eliminate non-deterministic rendering

Visual tests become unstable when components render values that change between requests or environments.

// Avoid this in server-rendered UI
const now = new Date().toLocaleString()
const id = Math.random()

Prefer deterministic values:

export async function getServerSideProps() {
  return {
    props: {
      buildLabel: process.env.APP_BUILD_LABEL || 'stable',
    },
  }
}

Also watch for:

  • Locale differences
  • Timezone differences
  • Random IDs
  • A/B flags
  • Client-side layout calculations

8. Compare generated HTML and asset requests

To isolate the exact cause, compare the page source and network activity between both server modes.

Focus on:

  • Different img or picture URLs
  • Missing CSS files
  • Different font requests
  • 404s under /_next/static/
  • Different class names caused by missing styles

If you see 404s in standalone mode, the bug is usually packaging, not rendering.

9. Use a production-like standalone launch script

Create one repeatable command for CI and visual testing.

{
  "scripts": {
    "build": "next build",
    "start:next": "next start -p 3000",
    "start:standalone": "cp -r public .next/standalone/public && mkdir -p .next/standalone/.next && cp -r .next/static .next/standalone/.next/static && node .next/standalone/server.js"
  }
}

This removes manual differences between local runs and CI runs.

10. Validate inside the same environment

If one mode runs locally and the other in Docker, screenshot diffs may be caused by system libraries, fonts, or libc differences rather than Next.js itself. Run both startup modes in the same container image and compare again.

FROM node:20-alpine AS runner
WORKDIR /app
COPY . .
RUN npm ci && npm run build
CMD ["node", ".next/standalone/server.js"]

If you compare this against next start, use the same base image and installed dependencies.

Common Edge Cases

  • Fonts are loaded locally in development but missing in standalone packaging: this causes text spacing changes and large screenshot diffs.
  • Remote images require headers or private access: one environment can fetch them, the other cannot, so placeholders or broken images appear.
  • assetPrefix points to a CDN in one mode only: generated asset URLs differ and styles or images fail to load.
  • Middleware or rewrites depend on hostnames: changing the startup command can indirectly change request host handling.
  • Sharp is unavailable or behaves differently: image optimization output can change if native dependencies differ across environments.
  • App Router streaming timing: visual capture may happen before late content settles, especially when network access differs.
  • Case-sensitive file paths in Linux: image or font imports may work on macOS but fail in CI or containers.
  • Dynamic imports with side effects: if modules load differently due to tracing or runtime order, layout can shift.

FAQ

Why does next start look correct while node .next/standalone/server.js looks wrong?

Because standalone output is a minimized deployment package. If you do not copy public and .next/static, or if your app depends on extra runtime files, the standalone server will render with missing assets or fallback behavior.

Is this a Next.js bug or a deployment packaging problem?

It can be either, but most real-world cases are deployment packaging issues. Standalone mode is stricter and exposes hidden dependencies on files, fonts, images, or environment variables that were available in the full build layout.

What is the fastest way to debug image render diffs between the two modes?

Start by comparing network requests and HTML output. If standalone has 404s for /_next/static, public assets, fonts, or image endpoints, you have found the root cause. Then verify identical environment variables and container/runtime dependencies.

The reliable fix is to treat standalone as its own production artifact: copy the required static files, keep runtime configuration identical, remove filesystem assumptions, and run visual regression tests against the exact deployment layout you ship.

Leave a Reply

Your email address will not be published. Required fields are marked *