Skip to content

feat(adapter-nextjs): turn off Next.js image optimization by default on Cloud Run - #713

Open
abhis3 wants to merge 1 commit into
mainfrom
feat/adapter-nextjs-image-optimization-off
Open

abhis3 wants to merge 1 commit into
mainfrom
feat/adapter-nextjs-image-optimization-off

Conversation

@abhis3

@abhis3 abhis3 commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

Summary

@google-cloud/adapter-nextjs now turns Next.js image optimization off by default, as @apphosting/adapter-nextjs does on App Hosting. During next build, modifyConfig sets images.unoptimized: true in memory. The app's next.config file isn't touched. Pages render plain <img src="/..."> tags that point at the original files, and /_next/image returns 404.

Build images config Page HTML /_next/image
Default unoptimized: true set in memory <img src="/logo.png"> 404
Custom loader in next.config (images.loader other than "default", or images.loaderFile) Unchanged URLs from the custom loader Unchanged (Next.js behavior)
GOOGLE_NEXTJS_IMAGE_OPTIMIZATION=true (or 1) at build time Unchanged /_next/image?url=... 200, optimized (WebP)
images.unoptimized: true already in next.config Unchanged <img src="/logo.png"> 404
  • The env var is the only way to opt back in. modifyConfig receives the config after Next.js fills in its defaults (unoptimized: false, loader: "default", loaderFile: ""), so it can't tell an explicit unoptimized: false from no setting. (@apphosting/adapter-nextjs reads the raw user config, so it can honor unoptimized: false. A modifyConfig hook can't.)
  • A custom loader is never overridden: with unoptimized: true, next/image skips the loader and uses the raw src.
  • One build log line says which case applied, for example:
    [adapter-nextjs] Setting NextConfig images.unoptimized=true (Next.js image optimization is off by default on Cloud Run; set GOOGLE_NEXTJS_IMAGE_OPTIMIZATION=true to turn it on).
  • Static exports get unoptimized: true too. Next.js requires that for next/image with the default loader under output: 'export'.

Why

The optimizer runs inside the serving instance, which isn't sized for it by default, and optimized images aren't persisted, so every new instance optimizes them again. In a local test of a 1 CPU / 512 MiB container, 80 concurrent requests (Cloud Run's default concurrency) for a 12 MP photo ran it out of memory in about 7 s, and all 80 requests failed.

A TODO in modify-config.ts tracks turning the optimizer back on, for example with a concurrency limit on /_next/image in serve.js and a disk-cache size based on instance memory.

Caveat: custom servers

modifyConfig only changes the build. An app that starts Next.js from its own server (for example next({ dev: false }) in server.js) loads next.config again at runtime. So its /_next/image endpoint stays live, even though its pages render plain <img> tags. The README documents this.

Changes

  • src/adapter/modify-config.ts: the images logic, plus an isEnvFlagEnabled() helper that CRASHLYTICS_UPLOAD_MAPPING now uses too (same parsing as before).
  • src/adapter.spec.ts: 7 new tests:
    • default off (other images fields kept, input not mutated)
    • no images field
    • custom loader
    • custom loaderFile
    • env var set to true, TRUE or 1
    • env var set to other values
    • an existing unoptimized: true is kept, with or without the env var
  • README.md: a new "Image Optimization" section, a modifyConfig bullet, and an env var row.

Testing

  • npm test in packages/@google-cloud/adapter-nextjs, on Node 18.20.8 and Node 22.23.4: 44 passing, 11 pending. The pending ones are the SMOKE_TEST=1 suites, the same as on main. npm run build is clean on both.
  • npm run lint:quiet passes, with no new warnings in the touched files.
  • Real next@16.4.3 builds of a sample app (App Router + next/image), with NEXT_ADAPTER_PATH pointing at this branch and served by node .next/standalone/server.js:
    • Default: required-server-files.json has unoptimized: true. The HTML has <img ... src="/logo.png"/> and no /_next/image. /_next/image?url=%2Flogo.png&w=32&q=75 returns 404, and /logo.png returns 200 image/png.
    • GOOGLE_NEXTJS_IMAGE_OPTIMIZATION=true: unoptimized stays false. The HTML uses a /_next/image?url=%2Flogo.png&w=… srcset, and /_next/image returns 200 image/webp.
    • images.loaderFile: the config is untouched, and the HTML uses the custom loader's URLs. When only loaderFile is set, Next.js keeps loader: "default", which is why the adapter checks loaderFile too.
  • Served by .cloudrun/serve.js instead, the full route checks (cache guard plus the image routes) pass for both a default build and an opt-in build.

…on Cloud Run

modifyConfig now sets images.unoptimized: true in memory during production
builds, as App Hosting does. It leaves the images config unchanged when:

- the app uses a custom image loader (images.loader other than "default",
  or images.loaderFile), because unoptimized: true would bypass the loader;
- the build sets GOOGLE_NEXTJS_IMAGE_OPTIMIZATION=true (or "1"). This is the
  only way to opt back in: modifyConfig receives the config after Next.js
  fills in its defaults, so an explicit unoptimized: false can't be told
  apart from no setting;
- images.unoptimized is already true.

One build log line says which case applied. The README documents the
behavior, the env var, and the custom-server caveat.
@abhis3 abhis3 self-assigned this Nov 9, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request disables Next.js image optimization by default on Cloud Run by setting images.unoptimized to true during the build phase. This default behavior can be bypassed if a custom image loader is configured or if the GOOGLE_NEXTJS_IMAGE_OPTIMIZATION environment variable is set to true or 1. The changes include the core logic implementation in modify-config.ts, corresponding unit tests in adapter.spec.ts, and updated documentation in README.md. No review comments were provided, and there is no additional feedback.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant