Skip to content

fix(next): detect next/image in Next.js 15+ App Router projects#10214

Closed
dxdc wants to merge 2 commits intofirebase:mainfrom
dxdc:fix/next15-image-optimization
Closed

fix(next): detect next/image in Next.js 15+ App Router projects#10214
dxdc wants to merge 2 commits intofirebase:mainfrom
dxdc:fix/next15-image-optimization

Conversation

@dxdc
Copy link
Copy Markdown
Contributor

@dxdc dxdc commented Mar 30, 2026

Firebase Hosting with frameworksBackend does not detect next/image usage in Next.js 15+ App Router-only projects. This causes firebase deploy to skip adding Sharp to the Cloud Function, breaking image optimization.

Affected: Any Next.js 15+ project using App Router (no pages/ directory) deployed via Firebase Hosting frameworksBackend.

Root Cause

isUsingImageOptimization() in src/frameworks/next/utils.ts has two detection tiers that both fail:

Tier 1: export-marker.jsonisNextImageImported

Next.js sets isNextImageImported only for Pages Router pages. In next/src/build/index.ts:

if (pageType === 'app' && originalAppPath) {
  // App Router - isNextImageImported is NEVER checked here
} else {
  // Pages Router only
  if (workerResult.isNextImageImported) {
    isNextImageImported = true;  // ← only set here
  }
}

For App Router-only projects, the flag remains false in export-marker.json.

Tier 2: Client reference manifest scan

isUsingNextImageInAppDirectory() searches *client-reference-manifest.js files for the substring node_modules/next/dist/client/image.

In Next.js 15.5.x, the image component is no longer listed as a separate entry in client-reference-manifest files. The next/image component is bundled into page-level client chunks instead of being registered individually. Verified by building a Next.js 15.5.12 project and inspecting every manifest file - none contain the string image.

Result

Both checks return falseisUsingImageOptimization() returns false → Sharp is not added to the Cloud Function → image optimization is silently disabled.

Proposed Fix

Add a third fallback tier: scan project source files for next/image imports. This only runs when both existing checks fail, so it's fully backward compatible.

The source scan is the most defensible approach: targeted (no false positives), backward compatible (only runs when existing checks fail), and robust (from 'next/image' is a stable API contract).

Reproduction

  1. Create a Next.js 15.5+ project using only App Router (no pages/ directory)
  2. Use next/image in any component
  3. Deploy via firebase deploy --only hosting with frameworksBackend
  4. Check .next/export-marker.json - isNextImageImported is false
  5. Image optimization is not included in the Cloud Function

Workaround

Until fixed, patch export-marker.json after build:

const fs = require('fs');
const marker = JSON.parse(fs.readFileSync('.next/export-marker.json', 'utf8'));
marker.isNextImageImported = true;
fs.writeFileSync('.next/export-marker.json', JSON.stringify(marker, null, 2));

Next.js 15+ App Router projects may not populate the export marker or
the client-reference-manifest with next/image references, even when
next/image is actively used.  This adds a source-level fallback that
scans .tsx/.ts/.jsx/.js files under the app directory for next/image
import statements before falling back to the images-manifest.json
check.
Copy link
Copy Markdown
Contributor

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

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 implements a fallback mechanism for Next.js 15+ to detect next/image usage by scanning project source files in the app or src/app directories when build manifests are incomplete. The review feedback suggests refactoring the detection logic to reduce nesting, improving the extensibility of directory checks, and switching to asynchronous globbing to avoid blocking the event loop during file scans.

Reduce nesting in isNextImageInProjectSource by iterating over
candidate directories. Simplify the conditional assignment in
isUsingImageOptimization. Switch from globSync to async glob
to avoid blocking the event loop during file scanning.
@leoortizz
Copy link
Copy Markdown
Member

Hi @dxdc, thanks for the detailed write-up and the PR. I tried reproducing this with an App Router-only project on Next.js 15.5.14 (latest safe 15.5.x) and 16.1.1, and both existing detection tiers work correctly. Here's the 15.5.x app I used to test for reference: https://github.com/monogramdesign/firebase-tools-test-apps/tree/main/nextjs/15.5.x. My findings:

  • Tier 1 (export-marker.json): Confirmed isNextImageImported: false — this is expected per the Next.js source which only sets it for Pages Router.
  • Tier 2 (client-reference-manifest): The string node_modules/next/dist/client/image is present in the manifest files (in this file in my project: .next/server/app/app-image-test/page_client-reference-manifest.js), and the existing substring check catches it.

The deploy correctly detects image optimization and require a Cloud Function for it.

Could you share a minimal reproduction project where both tiers fail? I'd like to understand what's different about your setup.

Separately, I have concerns about the source-scanning fallback approach:

  • The regex /from\s+['"]next\/image['"]/ matches commented-out code, strings, and dead imports — producing false positives
  • It reads files that may not be part of the actual build (e.g. test utilities under app/)
  • The rest of the detection pipeline reads from .next/ build artifacts, which are the source of truth for what's actually bundled — source scanning diverges from that

CC @jamesdaniels @annajowang

@dxdc
Copy link
Copy Markdown
Contributor Author

dxdc commented Mar 30, 2026

Thanks for testing this! I can reproduce the issue - here's the exact trigger:

The image reference disappears from all client-reference-manifest.js files when every file that imports next/image is a 'use client' component (i.e., no Server Component in the tree imports next/image).

Your test app works because layout.tsx and page.tsx are Server Components that import next/image - so the Image component crosses a server→client boundary and gets registered in the manifest. If you add "use client" to all three files in your test app, the reference disappears:

// src/app/layout.tsx
+"use client"
 import Image from "next/image"
 // ...

// src/app/page.tsx  
+"use client"
 import Image from "next/image"
 // ...

// src/app/app-image-test/page.tsx
+"use client"  
 import Image from "next/image"
 // ...

Build → check manifests → node_modules/next/dist/client/image is gone from every *client-reference-manifest.js. I verified this against your test repo.

Why: When a 'use client' component imports next/image, webpack bundles the Image component into the page's client chunk. It never crosses a server→client boundary, so it never gets a separate entry in the client-reference-manifest. The manifest only tracks RSC boundary crossings.

This is a common pattern - any page that uses Redux, useState, form handlers, etc. must be 'use client', and many of these pages also use next/image.

Re: the source-scanning concerns - those are fair points. A tighter alternative: check images-manifest.json directly (it's always written by Next.js and already read as a validation step in the existing code). If images.unoptimized !== true, image optimization is configured. The only false positive would be a project that has the default image config but never actually uses <Image> - which just adds Sharp unnecessarily without breaking anything.

@dxdc
Copy link
Copy Markdown
Contributor Author

dxdc commented Mar 30, 2026

Btw: After experimenting with it some more, another workaround is a 1x1 hidden <Image> in layout.tsx (Server Component) forces the manifest registration.

@leoortizz
Copy link
Copy Markdown
Member

leoortizz commented Mar 31, 2026

Thanks for reporting this @dxdc — your investigation into the "use client" edge case was spot on. The client-reference-manifest only tracks RSC boundary crossings, so when every next/image import lives in a "use client" component, both existing detection tiers miss it.

I've opened #10228 with a fix that stays within the .next build artifacts approach: it scans prerendered HTML for the data-nimg=" attribute that next/image renders on every <img>. The attribute has been stable since Next.js 11.1, so it covers all supported versions. The check only runs as a fallback when the existing manifest-based detection fails.

Thanks again for the detailed write-up — it made tracking this down much easier.

@dxdc
Copy link
Copy Markdown
Contributor Author

dxdc commented Mar 31, 2026

thanks @leoortizz !!

@dxdc dxdc closed this Mar 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants