question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Custom Cache-Control response header for `/_next/image`

See original GitHub issue

Bug report

Describe the bug

When requesting /_next/image(.*), I’m getting this Response header:

Cache-Control: public, max-age=60

And it’s fine… What’s not fine is that I’m getting the same exact Response headers even when I use custom headers in next.config.js, i.e.:

module.exports = {
  async headers() {
    return [
      {
        // This works, and returns appropriate Response headers:
        source: '/(.*).jpg',
        headers: [
          {
            key: 'Cache-Control',
            value:
              'public, max-age=180, s-maxage=180, stale-while-revalidate=180',
          },
        ],
      },
      {
        // This doesn't work for 'Cache-Control' key (works for others though):
        source: '/_next/image(.*)',
        headers: [
          {
            key: 'Cache-Control',
            // Instead of this value:
            value: 'public, max-age=180, s-maxage=180, stale-while-revalidate=180',
            // Cache-Control response header is `public, max-age=60` in production
            // and `public, max-age=0, must-revalidate` in development
          },
        ],
      },
    ]
  },
}

To Reproduce

Try this next.config.js above, with an image that uses common <img> tag like this:

<img src="/YOUR_IMAGE_IN_PUBLIC_FOLDER.jpg">

And another tag that uses the new next/image component, with the same or whatever image url, for example:

<Image src="/YOUR_IMAGE_IN_PUBLIC_FOLDER.jpg">

And try requesting those images in the browser, and look into Devtools -> Network tab, the Response headers, and see that for common <img> tag it’s actually changed, whereas for new <Image> component it is not.

Expected behavior

For /_next/image(.*) urls, I expected to see Headers that I setup, i.e. Cache-Control: public, max-age=180, s-maxage=180, stale-while-revalidate=180.

Screenshots

Common <img>:

image

New <Image>:

image

System information

  • OS: not applicable / any
  • Browser (if applies): not applicable / any
  • Version of Next.js: 10.0.3
  • Version of Node.js: 12.18.0
  • Deployment: both next start on development machine, and actual Vercel deployment (screenshots demonstrate the case with local machine, but I’ve confirmed that the same is happening on a Vercel deployment)

Additional context

Originally I created an issue using “Feature request” template, and it got translated into “Idea discussion”: https://github.com/vercel/next.js/discussions/19896 (how to delete/close it pls? doesn’t seem to be possible), - initially I wasn’t sure if it’s even possible to setup custom headers, and how that supposed to work, though I just needed that functionality. But later, upon discovering that there’s such a functionality already, I figured out that actually /_next/image overrides both custom headers in next.config.js and headers property of Vercel config, i.e. it’s not just some idea of a feature that is “not implemented”, but it’s actual bug.

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Reactions:99
  • Comments:113 (22 by maintainers)

github_iconTop GitHub Comments

23reactions
TheThirdRacecommented, May 26, 2021

Intro

Just to be clear this issue was not ignored at all, we investigated it and proposed a solution which seemingly was ignored, no feedback was provided except for 1 comment, we were waiting to see if that proposed solution would work for the people in this thread.

Relevant comments: #19914 (comment) > #19914 (comment)

If the proposed solution would make sense for your case we can add it 👍

@timneutkens

There seem to be a lot of confusion about which cache this ticket is about.

Just to prove my point, users have been posting about 4 types of cache:

  1. The cache from the browser => Status Code: 200 (from memory cache)
  2. The cache from the browser => Status Code: 304 Not Modified
  3. The cache from the upstream image => AWS S3 buckets, CMS, external host, etc.
  4. The cache for the optimized image on Vercel CDN => code from next-server package

Most of the posts are about #2, #3 and #4. Including interventions from NextJs team members.

But the original post in this ticket is pretty clear the issue is about #1.

Example with NextJs / Vercel

What I consider the happy path

I run NextJs 10.2.2 on NodeJs 14.17.0.

I have a public/img folder where I put all my images.

I use next/image to display images in my app.

I build with yarn build which will run the default build command for NextJs.

I don’t use any server-rendering, thus everything is statically rendered.

I installed the official Github plugin from Vercel so everytime I push on a branch, it gets deployed automatically on Vercel CDN.

Everything here is the default for an installation with NextJs and Vercel.

My next.config.js file

I’ve tried a lot of different combinations for Cache-Control, none of which worked, here are some of them:

const withPlugins = require('next-compose-plugins')
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true'
})
const generateSitemap = require('./script/generateSitemap')

module.exports = withPlugins([
  {
    future: {
      webpack5: true // TODO eventually remove as it will be the default value
    },
    async headers() {
      return [
        {
          source: '/(.*).(jpg|png|webp)',
          headers: [
            {
              key: 'Cache-Control',
              value: 'public, max-age=180, s-maxage=180, stale-while-revalidate=180'
            }
          ]
        },
        {
          source: '/_next/image(.*)',
          headers: [
            {
              key: 'Cache-Control',
              value: 'public, max-age=180, s-maxage=180, stale-while-revalidate=180'
            }
          ]
        },
        {
          source: '/image(.*)',
          headers: [
            {
              key: 'Cache-Control',
              value: 'public, max-age=180, s-maxage=180, stale-while-revalidate=180'
            }
          ]
        },
        {
          source: '/img(.*)',
          headers: [
            {
              key: 'Cache-Control',
              value: 'public, max-age=180, s-maxage=180, stale-while-revalidate=180'
            }
          ]
        }
      ]
    },
    webpack: (config, { isServer }) => {
      if (isServer) {
        generateSitemap()
      }
      return config
    }
  },
  // ? This is after the custom webpack config to avoid being overriden
  [withBundleAnalyzer]
])

Running the production build

While running the production build, I open Chrome Dev Tools and look at the Network tab.

I click on an image entry to see its headers:

// General
Request URL: http://localhost:3000/_next/image?url=%2Fimg%2Fskyrim%2Fdragonborn-staring-at-ruins-aspect-ratio-3-1.jpg&w=992&q=75
Request Method: GET
Status Code: 304 Not Modified
Remote Address: 127.0.0.1:3000
Referrer Policy: strict-origin-when-cross-origin
// Request Headers
Accept: image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: keep-alive
Host: localhost:3000
If-None-Match: L5JFC21DkugcwrVSEmGHBfkeOz2TzYXN5pVhKuJNwyU=
Referer: http://localhost:3000/news
sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="90", "Microsoft Edge";v="90"
sec-ch-ua-mobile: ?0
Sec-Fetch-Dest: image
Sec-Fetch-Mode: no-cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 Edg/90.0.818.66
// Response Headers
Cache-Control: public, max-age=0, must-revalidate
Connection: keep-alive
Date: Wed, 26 May 2021 02:35:47 GMT
ETag: L5JFC21DkugcwrVSEmGHBfkeOz2TzYXN5pVhKuJNwyU=
Keep-Alive: timeout=5

As you can see, no matter what I put in my next.config.js, the Cache-Control is never what I asked for, it’s always:

public, max-age=0, must-revalidate

We can see the Etag is working just fine because of the If-None-Match and the status code 304 Not Modified. But that would be case #2:

The cache from the browser => `Status Code: 304 Not Modified`

It’s impossible for me to make NextJs use case #1:

The cache from the browser => `Status Code: 200 (from memory cache)`

Why case #1 is desirable?

With case #1, the browser cached the image and uses it directly without any network request if the cache isn’t expired.

In the best case scenario where the cache is valid and available:

  • the whole latency to load an image is from 1 to 5ms
  • there is absolutely no data sent on the network

How case #2 differs from case #1?

With case #2, the browser cached the image, but needs to make a network request to validate if the cached image is still valid before using it.

In the best case scenario where the cache is valid and available:

  • the whole latency to load an image is around 40 to 120ms (vs 1 to 5ms for case #1)
  • there is a network request involved, but the data is less than 1KB per image (vs no request at all for case #1)

Unfortunately, the best case scenario is relatively rare for case #2.

If the unusally low cache time for the optimized image is expired (60 seconds only), NextJs needs to optimize the image again, which can push the whole latency upwards of 50 to 1200ms. This can be huge compared to the best case scenario, and it’s even worst compared to case #1.

Now, if the image is NOT hosted on Vercel, say it’s hosted on an S3 buckets, the image needs to be downloaded from upstream (case #3). This can add another 40 to 100ms of latency.

If your user has no network connection, the browser won’t make a network request to validate the Etag and use the cached image directly, which is a huge win.

But if your user has a spotty network connection, the browser will make that costly network request and given the quality of the network, the latency is gonna be very bad.

All these are pretty common situations and these are all weakpoints for case #2. On the other hand, case #1 is working perfectly in these situations.

Comparison with Cloudinary (image optimization service)

Cloudinary uses case #1 in combination with Etag and a fallback on last-modified.

// Response Headers (I've put only the important stuff)
cache-control: private, no-transform, immutable, max-age=2592000
etag: "88bdbf2b78c434a7b8c9ba8ea0400f94"
last-modified: Wed, 11 Oct 2017 02:00:21 GMT

What it does:

  1. Browser can cache the image 30 days
  2. If the cache is not expired yet, the browser will take the image immediately from the cache without any network request
  3. If the cache is expired, the browser will make a network request to validate the Etag and last-modified
  4. If the image hasn’t changed, the browser will receive a 304 Not Modified and renew the cache for another 30 days

You only get to redownload the image if all 3 cache mechanism are busted: max-age, etag, last-modfied.

The only downside is that if you change your image for another and keep the same name, the users that already have the old image in cache might use that old one up to 30 days. On the other hand, I would argue you’re looking for trouble if you keep the same name and you wouldn’t get any sympathy from me on that one… Just change the name, problem solved instantly.

Conclusion

With all that said, I think you can better grasp why this ticket is asking for being able to overwrite the Cache-Control header so we can use case #1.

Best case scenario would be to keep the Etag so we can use it like Cloudinary does.

I haven’t touched much on cases #3 and #4 because they’re whole different cache types which is not the concern of this particular ticket.

I’d just like to re-iterate I’m no expert on images. Maybe I didn’t understand something that was posted in this thread. Maybe I’m not aware of some constraints you have with the system’s inner workings. Maybe I’m making a really stupid mistake on my side and that’s why I can’t make it work. These are all plausible possibilities.

But on the off-chance that I actually nailed it, I hope my post will help you understand what this ticket was aiming for before all the posts for other cache types made it confusing.

22reactions
amuttschcommented, Feb 2, 2021

We fell into this issue today too. According to the docs the upstream cache control header should be used: https://nextjs.org/docs/basic-features/image-optimization#caching

Looking at the code it hard codes the max-age=0 into the response: https://github.com/vercel/next.js/blob/canary/packages/next/next-server/server/image-optimizer.ts#L310

This is not usable for us as our page contains lots of images. If they are not cached our users waste lots of bandwidth. Is there a timeline when this issue will be fixed? Thanks!

Read more comments on GitHub >

github_iconTop Results From Across the Web

next.config.js: Custom Headers
Headers allow you to set custom HTTP headers on the response to an incoming request on a given path. To set custom HTTP...
Read more >
Custom Cache-Control response header for `/_next/image`
I'm facing the problem that is related on this issue: https://github.com/vercel/next.js/issues/19914 Does anyone knows any workaround that I ...
Read more >
Cache-Control - HTTP - MDN Web Docs
The Cache-Control HTTP header field holds directives (instructions) — in both requests and responses — that control caching in browsers and ...
Read more >
How can I cache dynamic images in Next app? - Stack Overflow
Add the below code in the "next.config.js" file of your project to cache images for 60 seconds: module.exports = { images: ...
Read more >
Setting Cache-Control Headers
Through HTTP, Cache-Control gives your frontend applications instructions for caching requests and responses to your app. Optimizing Cache-Control for Atlas ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found