next.js icon indicating copy to clipboard operation
next.js copied to clipboard

`next/image` is unusable in Server Components with `loader` function

Open raed667 opened this issue 2 years ago • 1 comments

Verify canary release

  • [X] I verified that the issue exists in the latest Next.js canary release

Provide environment information

Operating System:
  Platform: darwin
  Arch: x64
  Version: Darwin Kernel Version 21.6.0: Mon Aug 22 20:17:10 PDT 2022; root:xnu-8020.140.49~2/RELEASE_X86_64
Binaries:
  Node: 16.13.1
  npm: 8.1.2
  Yarn: 1.22.19
  pnpm: 7.1.7
Relevant packages:
  next: 13.0.0
  eslint-config-next: 13.0.0
  react: 18.2.0
  react-dom: 18.2.0

What browser are you using? (if relevant)

No response

How are you deploying your application? (if relevant)

No response

Describe the Bug

When using next/image with a custom loader in a Server Component we get the following error:

Uncaught Error: Functions cannot be passed directly to Client Components because they're not serializable.
  <... loader={function} src=... alt=...>
              ^^^^^^^^^^

Expected Behavior

I expect a custom loader to be usable with server components.

Link to reproduction

https://codesandbox.io/s/cocky-bohr-g3tqkt?file=/app/post/ImageWrapper.tsx:275-287

To Reproduce

Go to /post in the provided codesandbox.

raed667 avatar Oct 27 '22 01:10 raed667

Getting the same thing, to get the image working it has to be a client component which is not what I want.

iankduffy avatar Oct 28 '22 15:10 iankduffy

Not optimal but as a workaround it seems to work if you set the loader in next.config.js file

images: {
    loader: 'custom',
    loaderFile: './loader.tsx',
...
}

https://nextjs.org/docs/api-reference/next/image#loader-configuration

jee-r avatar Feb 05 '23 17:02 jee-r

next/image has to be a client component. It uses JS to detect when the image has loaded. Although I agree that it would be nice to be able to create a shell around next/image using loader which isn't a client component (perhaps by allowing a loader template string $src?q=$q), it won't save you much. The only thing it saves is that the loader function doens't end up in your JS bundle.

Keep in mind that client components are still server-side rendered, so requiring next/image to be a client component doesn't make it less usable.

LuudJanssen avatar Feb 22 '23 17:02 LuudJanssen

I create a repo to reproduce this bug https://github.com/jee-r/image-loader-test GH action build logs : https://github.com/jee-r/image-loader-test/actions/runs/4632100214/jobs/8195761620

as you can see in this branch if we set the loader in next.config.js https://github.com/jee-r/image-loader-test/commit/51ae06d7a1729abe233f31352920ed9a0595e058

build has succeeded https://github.com/jee-r/image-loader-test/actions/runs/4632232545

loader in page : GitHub Workflow Status (with branch) loader in next.config.js : GitHub Workflow Status (with branch)

jee-r avatar Apr 06 '23 19:04 jee-r

next/image is a Client Component, which is why you can't serialize a function from a server component to the client component. Creating your own client component around it is entirely fine, there's no performance difference in doing so. As said next/image is already a Client Component, you just don't specifically see it is a Client Component. Also to clarify further Client Components are the same as components in pages, they are pre-rendered to HTML, so the <img /> tags will be in the initial HTML.

TLDR: Client Components are not bad, they are the components that you already knew. They are pre-rendered as part of the initial HTML.

With this context, it’s entirely fine to create your own client component around it:

'use client'
 
import Image from 'next/image'
 
const imageLoader = ({ src, width, quality }) => {
  return `https://example.com/${src}?w=${width}&q=${quality || 75}`
}
 
export default function Page() {
  return (
    <Image
      loader={imageLoader}
      src="me.png"
      alt="Picture of the author"
      width={500}
      height={500}
    />
  )
}

You can learn more in the documentation here: https://nextjs.org/docs/app/api-reference/components/image#loader. You can also set "loaderFile" globally: https://nextjs.org/docs/app/api-reference/components/image#loaderfile, however there is no performance win in doing so, it's purely convenience.

timneutkens avatar Jul 17 '23 11:07 timneutkens