cf.completefrontend
Open lab My progress
THE NEXT.JS FIELD GUIDE

Optimizing images

Show photos with next/image: local and remote images, width, height and fill, sizes and srcset, preload, blur placeholders, and the optimizer's security settings.

By the end, you can
  • 01
    Show photos without layout shiftUse next/image with a static import, a public path or a remote address, and give each photo its size or a box to fill.
  • 02
    Send the right fileWrite sizes so the browser picks a small enough file from the srcset, and preload the photo that matters most.
  • 03
    Keep the optimizer safeAllow remote photos with remotePatterns, and read the 400 the optimizer sends for anything else.

The tea shop gets a photo

A photo straight from a camera is big: thousands of pixels wide, and often several megabytes. A phone with a screen 400 pixels wide downloads all of it, then shrinks it. A plain <img> also has no size until the file arrives, so the text under it jumps down when it does. Next.js's Image component, Image from next/image, renders an <img> that fixes both: it asks for a copy of the photo at the size the screen needs, in a smaller format, and it keeps the photo's space from the start.

Here is the tea shop's home page with a photo of mint tea, a JPEG 1,600 pixels wide that sits next to the page in app. It's a real Next.js 16.4.3 project. The Browser tab shows the page as real Chrome drew it.

The home page with a photoNext.js
app/
app/page.tsx
import Image from "next/image";import mintTea from "./mint-tea.jpg"; export default function HomePage() {  return (    <main>      <h1>Tea shop</h1>      <Image src={mintTea} alt="A cup of mint tea on a saucer" width={400} />      <p>Mint tea, $3 a box.</p>    </main>  );}
Recorded from Next.js 16.4.3

GET /200

Response headers
Vary rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch, Accept-Encoding
x-nextjs-cache HIT
x-nextjs-prerender 1
x-nextjs-stale-time 300
X-Powered-By Next.js
Cache-Control s-maxage=31536000
Content-Type text/html; charset=utf-8

The HTML that arrived, one element per line, with the code of its 17 scripts left out

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="/_next/static/chunks/0b5zjm047lwhu.css" data-precedence="next">
    <link rel="preload" as="script" fetchpriority="low" href="/_next/static/chunks/31qp8jfkw0x2h.js">
    <script src="/_next/static/chunks/0i46y6vlt4sc3.js" async=""></script>
    <script src="/_next/static/chunks/20box33a0_n53.js" async=""></script>
    <script src="/_next/static/chunks/turbopack-3-ry682tkyiqk.js" async=""></script>
    <script src="/_next/static/chunks/1tq6ppe__m8m-.js" async=""></script>
    <script src="/_next/static/chunks/0bts8xcjgykqh.js" async=""></script>
    <title>Tea shop</title>
    <script src="/_next/static/chunks/0cz1d0mv5g_q7.js" nomodule=""></script>
  </head>
  <body>
    <div hidden="">
      <!--$-->
      <!--/$-->
    </div>
    <!--&-->
    <main>
      <h1>Tea shop</h1>
      <img alt="A cup of mint tea on a saucer" loading="lazy" width="400" height="267" decoding="async" data-nimg="1" style="color:transparent" srcset="/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fmint-tea.3hpoavs8o5i69.jpg&w=640&q=75 1x, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fmint-tea.3hpoavs8o5i69.jpg&w=828&q=75 2x" src="/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fmint-tea.3hpoavs8o5i69.jpg&w=828&q=75">
      <p>Mint tea, $3 a box.</p>
    </main>
    <!--$-->
    <!--/$-->
    <!--/&-->
    <script id="_R_">…</script>
    <script src="/_next/static/chunks/31qp8jfkw0x2h.js" async=""></script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
  </body>
</html>

The stylesheet the HTML links, with the CSS Next.js built for it, laid out to read

  1. /_next/static/chunks/0b5zjm047lwhu.css
    body {
      color:#1f2a24;
      background:#f6f3ea;
      margin:0;
      font-family:system-ui,sans-serif
    }
    main {
      padding:8px 24px
    }

Line 1 of app/page.tsx imports Image. Line 2 imports the photo itself: a static import, which gives an object with the file's address, width and height. Open app/mint-tea.jpg in the file tree to see the photo. Line 8 renders it with an alt text, which screen readers read out, and width={400}: Next.js works out the height from the file, 267.

The Network tab opens on the HTML. The <img> has width="400" height="267", loading="lazy", and a srcset: a list of files the browser can choose from. Each one is an address of the image optimizer, /_next/image, with the photo's address (url), a width (w=640 for screens with one device pixel per CSS pixel, 1x, and w=828 for 2x) and a quality (q=75). The build copied the photo to /_next/static/media/, under a name with a code made from its content. Pick the second request: the 1x file, asked for the way Chrome asks. The answer is an image/webp, 640 by 427 pixels, 4.0 kB.

Left, a browser window asks for the optimizer's address with w=640. An arrow goes to a window titled Next.js, which reads green-tea.jpg, 1600 pixels wide and 91.9 kilobytes, and makes a WebP 640 pixels wide, 3.9 kilobytes. An arrow goes back to the browser. Below, a card says the next visitor gets the saved copy.browserneeds w=6403.9 kB WebPaskssendsNext.jsJPEG, 1600 px, 91.9 kBWebP, 640 px, 3.9 kBkept: the next request is a HIT
The browser asks /_next/image for the green tea photo, 640 pixels wide. Next.js reads the 91.9 kB JPEG from public, makes a 3.9 kB WebP, sends it, and keeps it: the next request for it is a HIT.
Real-life analogyA print shop for the tea photos

A print shop keeps one big original of each photo. A customer fills in an order form with the photo and the frame's size, and gets a print that fits. The shop keeps a copy of each print it makes, so the next customer with the same order gets it at once.

In real life: The big original photo, kept in the back
In Next.js: The file in app or public
In real life: A print in the size each customer's frame needs
In Next.js: A copy from /_next/image, resized and in WebP
In real life: The order form, with the photo's name and the size
In Next.js: The /_next/image address: url, w and q
In real life: Prints kept on the shelf for the next customer
In Next.js: The optimizer's cache: the next request is a HIT
The main idea

next/image renders an <img> whose files come from the image optimizer, at /_next/image. The optimizer resizes each photo to the width the browser picks from the srcset, converts it to a smaller format, and keeps the result. width and height keep the photo's space, photos are lazy by default, and a remote fetch only starts at an address you allow.

Local and remote images

src can be a photo of your own, or one on another server. A photo of your own comes two ways. A static import, like the mint tea photo, gives Next.js the file at build time: it knows the width and height, and the build copies the file to /_next/static/media/ under a name with a code made from its content, so the address changes whenever the file does. A file in the public folder is served at the site's root as it is: public/teas/green-tea.jpg is at /teas/green-tea.jpg. A path like that is only text, so you write the size yourself. (Static files and the public folder, later in this stage, has more on both.)

A photo from public, by its pathNext.js
app/public/teas/
app/page.tsx
import Image from "next/image"; export default function HomePage() {  return (    <main>      <h1>Tea shop</h1>      <Image src="/teas/green-tea.jpg" alt="A cup of green tea on a saucer" width={400} height={267} />      <p>Green tea, $4 a box.</p>    </main>  );}
Recorded from Next.js 16.4.3

GET /teas/green-tea.jpg200

Response headers
Cache-Control public, max-age=0
Content-Type image/jpeg

The body that arrived is an image, image/jpeg: 1600 × 1067 pixels, 91.9 kB

Line 7 of app/page.tsx points at /teas/green-tea.jpg, which is public/teas/green-tea.jpg in the file tree, with width={400} and height={267}. The Network tab opens on the original file, served from public as it is: image/jpeg, 1,600 by 1,067 pixels, 91.9 kB. The next three requests ask the optimizer for it, 640 pixels wide.

A remote photo is an absolute address, like https://images.example.com/teas/oolong.jpg. Next.js can't read its size, so you write width and height, and you list the server in images.remotePatterns in next.config.ts. The optimizer only starts a fetch at an address that list allows. If that server answers with a redirect, the new address isn't checked against the list again: the limits there are maximumRedirects, 3 by default, and a check that refuses private addresses.

A remote photo, and the pattern that allows itNext.js
app/public/teas/
next.config.ts
import type { NextConfig } from "next"; const nextConfig: NextConfig = {  /* config options here */  cacheComponents: true,  partialPrefetching: true,  images: {    remotePatterns: [      new URL("https://images.example.com/teas/**"),      { protocol: "http", hostname: "localhost", port: "4000", pathname: "/teas/**" },    ],  },}; export default nextConfig;
Recorded from Next.js 16.4.3

GET /200

Response headers
Vary rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch, Accept-Encoding
x-nextjs-cache HIT
x-nextjs-prerender 1
x-nextjs-stale-time 300
X-Powered-By Next.js
Cache-Control s-maxage=31536000
Content-Type text/html; charset=utf-8

The HTML that arrived, one element per line, with the code of its 15 scripts left out

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="/_next/static/chunks/0b5zjm047lwhu.css" data-precedence="next">
    <link rel="preload" as="script" fetchpriority="low" href="/_next/static/chunks/31qp8jfkw0x2h.js">
    <script src="/_next/static/chunks/0i46y6vlt4sc3.js" async=""></script>
    <script src="/_next/static/chunks/20box33a0_n53.js" async=""></script>
    <script src="/_next/static/chunks/turbopack-3-ry682tkyiqk.js" async=""></script>
    <script src="/_next/static/chunks/1tq6ppe__m8m-.js" async=""></script>
    <script src="/_next/static/chunks/0bts8xcjgykqh.js" async=""></script>
    <title>Tea shop</title>
    <script src="/_next/static/chunks/0cz1d0mv5g_q7.js" nomodule=""></script>
  </head>
  <body>
    <div hidden="">
      <!--$-->
      <!--/$-->
    </div>
    <!--&-->
    <main>
      <h1>Tea shop</h1>
      <img alt="A cup of oolong" loading="lazy" width="400" height="267" decoding="async" data-nimg="1" style="color:transparent" srcset="/_next/image?url=https%3A%2F%2Fimages.example.com%2Fteas%2Foolong.jpg&w=640&q=75 1x, /_next/image?url=https%3A%2F%2Fimages.example.com%2Fteas%2Foolong.jpg&w=828&q=75 2x" src="/_next/image?url=https%3A%2F%2Fimages.example.com%2Fteas%2Foolong.jpg&w=828&q=75">
      <img alt="A cup of green tea" loading="lazy" width="400" height="267" decoding="async" data-nimg="1" style="color:transparent" srcset="/_next/image?url=%2Fteas%2Fgreen-tea.jpg&w=640&q=75 1x, /_next/image?url=%2Fteas%2Fgreen-tea.jpg&w=828&q=75 2x" src="/_next/image?url=%2Fteas%2Fgreen-tea.jpg&w=828&q=75">
    </main>
    <!--$-->
    <!--/$-->
    <!--/&-->
    <script id="_R_">…</script>
    <script src="/_next/static/chunks/31qp8jfkw0x2h.js" async=""></script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
  </body>
</html>

Line 9 of next.config.ts allows every photo under https://images.example.com/teas/: ** means any path below it. Line 10 allows a second server, on this computer. In the Network tab, the first <img> has /_next/image?url=https%3A%2F%2Fimages.example.com…: the remote address, written so it fits in a URL. The photo goes through the optimizer like a local one. This recording didn't ask for it, because images.example.com is a made-up server. Each row compares the three kinds of src.

The three kinds of src
QuestionA static importA path in publicA remote address
How do you write src?src={mintTea}, after import mintTea from "./mint-tea.jpg".src="/teas/green-tea.jpg", a path in public.src="https://images.example.com/teas/oolong.jpg".
Who gives the width and height?The build reads them from the file.You do, with width and height.You do, with width and height.
What else do you set up?Nothing.Nothing.images.remotePatterns in next.config.ts.
How long may browsers keep the optimized file?Ten years, immutable: its address changes when the file does.4 hours (minimumCacheTTL), or longer if the file says so.4 hours, or longer if the remote server says so.
Blur placeholder?Made for you: placeholder="blur".Only with your own blurDataURL.Only with your own blurDataURL.

width, height and fill

width and height aren't how big the photo shows on the screen: CSS decides that. They give the <img> its shape, so the browser keeps a box of the right ratio before the file arrives, and nothing under it moves. When things on the page move as it loads, that's a layout shift. Here is the green tea photo with its size left out:

A photo from public with no width or heightNext.js
app/public/teas/
app/page.tsx
import Image from "next/image"; export default function HomePage() {  return (    <main>      <h1>Tea shop</h1>      <Image src="/teas/green-tea.jpg" alt="A cup of green tea on a saucer" />      <p>Green tea, $4 a box.</p>    </main>  );}
Recorded from Next.js 16.4.3

GET /200

Response headers
Vary rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch, Accept-Encoding
x-nextjs-cache HIT
x-nextjs-prerender 1
x-nextjs-stale-time 300
X-Powered-By Next.js
Cache-Control s-maxage=31536000
Content-Type text/html; charset=utf-8

The HTML that arrived, one element per line, with the code of its 15 scripts left out

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="/_next/static/chunks/0b5zjm047lwhu.css" data-precedence="next">
    <link rel="preload" as="script" fetchpriority="low" href="/_next/static/chunks/31qp8jfkw0x2h.js">
    <script src="/_next/static/chunks/0i46y6vlt4sc3.js" async=""></script>
    <script src="/_next/static/chunks/20box33a0_n53.js" async=""></script>
    <script src="/_next/static/chunks/turbopack-3-ry682tkyiqk.js" async=""></script>
    <script src="/_next/static/chunks/1tq6ppe__m8m-.js" async=""></script>
    <script src="/_next/static/chunks/0bts8xcjgykqh.js" async=""></script>
    <title>Tea shop</title>
    <script src="/_next/static/chunks/0cz1d0mv5g_q7.js" nomodule=""></script>
  </head>
  <body>
    <div hidden="">
      <!--$-->
      <!--/$-->
    </div>
    <!--&-->
    <main>
      <h1>Tea shop</h1>
      <img alt="A cup of green tea on a saucer" loading="lazy" decoding="async" data-nimg="1" style="color:transparent" sizes="100vw" srcset="/_next/image?url=%2Fteas%2Fgreen-tea.jpg&w=640&q=75 640w, /_next/image?url=%2Fteas%2Fgreen-tea.jpg&w=750&q=75 750w, /_next/image?url=%2Fteas%2Fgreen-tea.jpg&w=828&q=75 828w, /_next/image?url=%2Fteas%2Fgreen-tea.jpg&w=1080&q=75 1080w, /_next/image?url=%2Fteas%2Fgreen-tea.jpg&w=1200&q=75 1200w, /_next/image?url=%2Fteas%2Fgreen-tea.jpg&w=1920&q=75 1920w, /_next/image?url=%2Fteas%2Fgreen-tea.jpg&w=2048&q=75 2048w, /_next/image?url=%2Fteas%2Fgreen-tea.jpg&w=3840&q=75 3840w" src="/_next/image?url=%2Fteas%2Fgreen-tea.jpg&w=3840&q=75">
      <p>Green tea, $4 a box.</p>
    </main>
    <!--$-->
    <!--/$-->
    <!--/&-->
    <script id="_R_">…</script>
    <script src="/_next/static/chunks/31qp8jfkw0x2h.js" async=""></script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
  </body>
</html>

The stylesheet the HTML links, with the CSS Next.js built for it, laid out to read

  1. /_next/static/chunks/0b5zjm047lwhu.css
    body {
      color:#1f2a24;
      background:#f6f3ea;
      margin:0;
      font-family:system-ui,sans-serif
    }
    main {
      padding:8px 24px
    }

The docs are clear: a photo that isn't a static import must have width and height, or fill. Don't count on anything else. What this recorded 16.4.3 build did with the broken version: next build passed, and the route is ○, prerendered as static content. The <img> in the Network tab has no width and no height, and the Browser tab shows the photo as wide as the window, cut off at the bottom. Until the file arrives, the browser can't know how tall the photo is, so the price under it jumps. In 16.4.3, the check that stops you, Image with src "/teas/green-tea.jpg" is missing required "width" property, is in the development-only part of get-img-props.ts, so next dev shows it and this build didn't.

Two halves. Left, labeled no width or height: a page window where the price line sits right under the heading, and a dashed arrow shows it jumping down when the photo arrives. Right, labeled width and height: a page window with an empty box the size of the photo already reserved, and the price line below it, which doesn't move.no sizeTea shopGreen tea, $4 a box.photo, laterthe price jumpswidth, heightTea shop400 × 267 boxGreen tea, $4 a box.nothing moves
Without width and height, the browser can't know the photo's height until the file arrives, so the price jumps down when it does. With them, it keeps a box of the right shape from the start.

Sometimes you don't know the photo's size, or the photo should cover a box whatever its shape. Then use fill instead of width and height. The <img> becomes position: absolute and fills its nearest parent that has a position, so that parent needs position: relative and a size. objectFit: "cover" crops the photo to the box instead of stretching it.

sizes and the srcset

The tea page shows three teas in a row, each photo a third of the page wide. The teas.module.css file makes the row a grid of three columns, and gives each photo's <div> position: relative and a 3:2 aspect-ratio. The srcset that Next.js writes depends on how you size the photos. Pick each version, and read the end of the Steps tab: the photos Chrome downloaded.

Playground: which file does Chrome download?
The tea page's row of photos, sized this wayNext.js
app/teas/public/teas/
app/teas/page.tsx
import Image from "next/image";import styles from "./teas.module.css"; const teas = [  { name: "Mint tea", photo: "/teas/mint-tea.jpg" },  { name: "Green tea", photo: "/teas/green-tea.jpg" },  { name: "Oolong", photo: "/teas/oolong.jpg" },]; export default function TeasPage() {  return (    <main>      <h1>Our teas</h1>      <ul className={styles.grid}>        {teas.map((tea) => (          <li key={tea.name}>            <Image src={tea.photo} alt={tea.name} width={240} height={160} />            <h2>{tea.name}</h2>          </li>        ))}      </ul>    </main>  );}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/teasTea shop
The page at /teas after step 1, as Chrome drew it with its styles. It says: Our teas. Mint tea. Green tea. Oolong.

A picture of the window in real Chrome after this step. Open it full size

Focus
nothing: the page itself
Scrolled
0 px down
Stylesheets on the page
0b5zjm047lwhu.css, 16gbl6ai3ondr.css

Requests the page made during this step (12), files and other sites included

  1. GET/teas200a whole page
  2. GET/_next/static/chunks/0b5zjm047lwhu.css200text/css
  3. GET/_next/static/chunks/16gbl6ai3ondr.css200text/css
  4. GET/_next/static/chunks/31qp8jfkw0x2h.js200application/javascript
  5. GET/_next/static/chunks/0i46y6vlt4sc3.js200application/javascript
  6. GET/_next/static/chunks/20box33a0_n53.js200application/javascript
  7. GET/_next/static/chunks/turbopack-3-ry682tkyiqk.js200application/javascript
  8. GET/_next/static/chunks/1tq6ppe__m8m-.js200application/javascript
  9. GET/_next/static/chunks/0bts8xcjgykqh.js200application/javascript
  10. GET/_next/image?url=%2Fteas%2Fmint-tea.jpg&w=640&q=75200image/webp
  11. GET/_next/image?url=%2Fteas%2Fgreen-tea.jpg&w=640&q=75200image/webp
  12. GET/_next/image?url=%2Fteas%2Foolong.jpg&w=640&q=75200image/webp
Try it yourself
width and height

Each photo is 240 by 160 CSS pixels, always. Next.js writes a short srcset with two candidates, 1x and 2x. The window is 800 pixels wide with two device pixels per CSS pixel, so Chrome picked the 2x file: w=640.

Each version was built with next build from Next.js 16.4.3. Real Chrome opened /teas in a window 800 pixels wide, at 2 device pixels per CSS pixel; the Steps tab lists every file it asked for, and the photos are the /_next/image requests at the end. The Network tab shows each <img> and its srcset. Reset goes back to the first version.

With width and height, the photo is a fixed size, so two candidates are enough: 1x and 2x. With fill, Next.js doesn't know the width, and without sizes it assumes the photo is as wide as the window: sizes="100vw", with a candidate for every width in deviceSizes, from 640w to 3840w. In the recording, Chrome took w=1920 for each of three photos about 240 pixels wide.

sizes tells the browser how wide the photo will be, before the CSS has loaded: (max-width: 640px) 100vw, 33vw reads "the whole window on screens up to 640 pixels, a third of it on wider ones". On line 18 of the last version, it makes Next.js add the smaller widths from imageSizes to the list, and the recorded Chrome took w=640. The pictures of the page are the same; the files are a third as wide. Which candidate a browser takes is up to the browser: sizes and the screen are what it starts from.

Two rows, from the recording. Top row, no sizes: 100vw on an 800 pixel window with 2 pixels per CSS pixel is 1600, and Chrome picked w=1920. Bottom row, sizes 33vw: about 264 times 2 is 528, and Chrome picked w=640.no sizes: 100vw800 × 2 = 1600w=1920sizes: 33vw264 × 2 = 528w=640what the recorded Chrome picked
The width sizes gives, times the screen's pixels per CSS pixel, is what the browser starts from. In the recording, an 800-pixel window with 2 pixels per CSS pixel, Chrome took the first file in the srcset at least that wide: w=1920 without sizes, w=640 with it.

What /_next/image sends back

The green tea project asked the optimizer three times for the photo 640 pixels wide, at quality 75. Pick each request in the Network tab:

The optimizer's answers for the green tea photoNext.js
app/public/teas/
app/page.tsx
import Image from "next/image"; export default function HomePage() {  return (    <main>      <h1>Tea shop</h1>      <Image src="/teas/green-tea.jpg" alt="A cup of green tea on a saucer" width={400} height={267} />      <p>Green tea, $4 a box.</p>    </main>  );}
Recorded from Next.js 16.4.3

GET /_next/image?url=%2Fteas%2Fgreen-tea.jpg&w=640&q=75200

Sent with the request
accept image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Response headers
Vary Accept
Cache-Control public, max-age=14400, must-revalidate
Content-Type image/webp
Content-Disposition attachment; filename="green-tea.webp"
Content-Security-Policy script-src 'none'; frame-src 'none'; sandbox;
X-Nextjs-Cache MISS

The body that arrived is an image, image/webp: 640 × 427 pixels, 3.9 kB

  • As Chrome asks for it, with Chrome's Accept header, which lists the image formats the browser can show: image/webp, 640 by 427 pixels, 3.9 kB, from a 91.9 kB original. X-Nextjs-Cache: MISS: the optimizer had to make it.
  • Again: the same file, and X-Nextjs-Cache: HIT. The optimizer kept what it made, on disk in .next/cache/images, and sent the copy.
  • With no Accept header: image/jpeg, 7.2 kB. The optimizer only sends a format the request says it accepts. Vary: Accept tells caches between the server and the browser to keep one copy per Accept.

The other headers are the same on all three. Cache-Control: public, max-age=14400, must-revalidate lets browsers keep the file for 14,400 seconds, 4 hours: that's minimumCacheTTL, whose default went from 60 seconds to 4 hours in Next.js 16. The photo of mint tea, a static import, got public, max-age=315360000, immutable instead: ten years, because its address changes whenever the file does. Content-Disposition: attachment and Content-Security-Policy: script-src 'none'; frame-src 'none'; sandbox; are for safety: someone who opens the address straight in a tab gets a download, and nothing in the file can run.

WebP is the default format. To send AVIF, smaller still and slower to make, list it in images.formats:

AVIF first, then WebPNext.js
app/public/teas/
next.config.ts
import type { NextConfig } from "next"; const nextConfig: NextConfig = {  /* config options here */  cacheComponents: true,  partialPrefetching: true,  images: {    formats: ["image/avif", "image/webp"],  },}; export default nextConfig;
Recorded from Next.js 16.4.3

GET /_next/image?url=%2Fteas%2Fgreen-tea.jpg&w=640&q=75200

Sent with the request
accept image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Response headers
Vary Accept
Cache-Control public, max-age=14400, must-revalidate
Content-Type image/avif
Content-Disposition attachment; filename="green-tea.avif"
Content-Security-Policy script-src 'none'; frame-src 'none'; sandbox;
X-Nextjs-Cache MISS

The body that arrived is an image, image/avif: 640 × 427 pixels, 3.3 kB

Line 8 of next.config.ts lists image/avif first. Asked the way Chrome asks, the optimizer sent image/avif, 3.3 kB. Asked with image/webp,*/*, like a browser without AVIF, it sent the 3.9 kB WebP. The first format in the list that the browser accepts wins.

preload for the LCP image

Largest Contentful Paint (LCP) is the time until the biggest thing in the first screen, often a large photo, has been drawn. Search engines and tools like Lighthouse measure it. A lazy photo starts late: the browser first reads the HTML, works out the layout, sees the photo is on screen, and only then asks for it. The home page now opens with a big mint tea photo, then seven more teas far down the page.

Playground: what does preload change in the HTML?
The home page, with this big photo on line 17Next.js
app/public/teas/
app/page.tsx
import Image from "next/image";import mintTea from "./mint-tea.jpg"; const teas = [  { name: "Green tea", photo: "/teas/green-tea.jpg" },  { name: "Oolong", photo: "/teas/oolong.jpg" },  { name: "Chamomile", photo: "/teas/chamomile.jpg" },  { name: "Jasmine", photo: "/teas/jasmine.jpg" },  { name: "Hibiscus", photo: "/teas/hibiscus.jpg" },  { name: "Black tea", photo: "/teas/black-tea.jpg" },  { name: "Rooibos", photo: "/teas/rooibos.jpg" },]; export default function HomePage() {  return (    <main>      <Image src={mintTea} alt="A cup of mint tea on a saucer" sizes="100vw" style={{ width: "100%", height: "auto" }} />      <h1>Tea shop</h1>      {teas.map((tea) => (        <section key={tea.name}>          <h2>{tea.name}</h2>          <Image src={tea.photo} alt={tea.name} width={600} height={400} />        </section>      ))}    </main>  );}
Recorded from Next.js 16.4.3

GET /200

Response headers
Vary rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch, Accept-Encoding
x-nextjs-cache HIT
x-nextjs-prerender 1
x-nextjs-stale-time 300
X-Powered-By Next.js
Cache-Control s-maxage=31536000
Content-Type text/html; charset=utf-8

The HTML that arrived, one element per line, with the code of its 17 scripts left out

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="/_next/static/chunks/0b5zjm047lwhu.css" data-precedence="next">
    <link rel="preload" as="script" fetchpriority="low" href="/_next/static/chunks/31qp8jfkw0x2h.js">
    <script src="/_next/static/chunks/0i46y6vlt4sc3.js" async=""></script>
    <script src="/_next/static/chunks/20box33a0_n53.js" async=""></script>
    <script src="/_next/static/chunks/turbopack-3-ry682tkyiqk.js" async=""></script>
    <script src="/_next/static/chunks/1tq6ppe__m8m-.js" async=""></script>
    <script src="/_next/static/chunks/0bts8xcjgykqh.js" async=""></script>
    <title>Tea shop</title>
    <script src="/_next/static/chunks/0cz1d0mv5g_q7.js" nomodule=""></script>
  </head>
  <body>
    <div hidden="">
      <!--$-->
      <!--/$-->
    </div>
    <!--&-->
    <main>
      <img alt="A cup of mint tea on a saucer" loading="lazy" width="1600" height="1067" decoding="async" data-nimg="1" style="color:transparent;width:100%;height:auto" sizes="100vw" srcset="/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fmint-tea.3hpoavs8o5i69.jpg&w=640&q=75 640w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fmint-tea.3hpoavs8o5i69.jpg&w=750&q=75 750w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fmint-tea.3hpoavs8o5i69.jpg&w=828&q=75 828w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fmint-tea.3hpoavs8o5i69.jpg&w=1080&q=75 1080w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fmint-tea.3hpoavs8o5i69.jpg&w=1200&q=75 1200w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fmint-tea.3hpoavs8o5i69.jpg&w=1920&q=75 1920w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fmint-tea.3hpoavs8o5i69.jpg&w=2048&q=75 2048w, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fmint-tea.3hpoavs8o5i69.jpg&w=3840&q=75 3840w" src="/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fmint-tea.3hpoavs8o5i69.jpg&w=3840&q=75">
      <h1>Tea shop</h1>
      <section>
        <h2>Green tea</h2>
        <img alt="Green tea" loading="lazy" width="600" height="400" decoding="async" data-nimg="1" style="color:transparent" srcset="/_next/image?url=%2Fteas%2Fgreen-tea.jpg&w=640&q=75 1x, /_next/image?url=%2Fteas%2Fgreen-tea.jpg&w=1200&q=75 2x" src="/_next/image?url=%2Fteas%2Fgreen-tea.jpg&w=1200&q=75">
      </section>
      <section>
        <h2>Oolong</h2>
        <img alt="Oolong" loading="lazy" width="600" height="400" decoding="async" data-nimg="1" style="color:transparent" srcset="/_next/image?url=%2Fteas%2Foolong.jpg&w=640&q=75 1x, /_next/image?url=%2Fteas%2Foolong.jpg&w=1200&q=75 2x" src="/_next/image?url=%2Fteas%2Foolong.jpg&w=1200&q=75">
      </section>
      <section>
        <h2>Chamomile</h2>
        <img alt="Chamomile" loading="lazy" width="600" height="400" decoding="async" data-nimg="1" style="color:transparent" srcset="/_next/image?url=%2Fteas%2Fchamomile.jpg&w=640&q=75 1x, /_next/image?url=%2Fteas%2Fchamomile.jpg&w=1200&q=75 2x" src="/_next/image?url=%2Fteas%2Fchamomile.jpg&w=1200&q=75">
      </section>
      <section>
        <h2>Jasmine</h2>
        <img alt="Jasmine" loading="lazy" width="600" height="400" decoding="async" data-nimg="1" style="color:transparent" srcset="/_next/image?url=%2Fteas%2Fjasmine.jpg&w=640&q=75 1x, /_next/image?url=%2Fteas%2Fjasmine.jpg&w=1200&q=75 2x" src="/_next/image?url=%2Fteas%2Fjasmine.jpg&w=1200&q=75">
      </section>
      <section>
        <h2>Hibiscus</h2>
        <img alt="Hibiscus" loading="lazy" width="600" height="400" decoding="async" data-nimg="1" style="color:transparent" srcset="/_next/image?url=%2Fteas%2Fhibiscus.jpg&w=640&q=75 1x, /_next/image?url=%2Fteas%2Fhibiscus.jpg&w=1200&q=75 2x" src="/_next/image?url=%2Fteas%2Fhibiscus.jpg&w=1200&q=75">
      </section>
      <section>
        <h2>Black tea</h2>
        <img alt="Black tea" loading="lazy" width="600" height="400" decoding="async" data-nimg="1" style="color:transparent" srcset="/_next/image?url=%2Fteas%2Fblack-tea.jpg&w=640&q=75 1x, /_next/image?url=%2Fteas%2Fblack-tea.jpg&w=1200&q=75 2x" src="/_next/image?url=%2Fteas%2Fblack-tea.jpg&w=1200&q=75">
      </section>
      <section>
        <h2>Rooibos</h2>
        <img alt="Rooibos" loading="lazy" width="600" height="400" decoding="async" data-nimg="1" style="color:transparent" srcset="/_next/image?url=%2Fteas%2Frooibos.jpg&w=640&q=75 1x, /_next/image?url=%2Fteas%2Frooibos.jpg&w=1200&q=75 2x" src="/_next/image?url=%2Fteas%2Frooibos.jpg&w=1200&q=75">
      </section>
    </main>
    <!--$-->
    <!--/$-->
    <!--/&-->
    <script id="_R_">…</script>
    <script src="/_next/static/chunks/31qp8jfkw0x2h.js" async=""></script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
  </body>
</html>

The stylesheet the HTML links, with the CSS Next.js built for it, laid out to read

  1. /_next/static/chunks/0b5zjm047lwhu.css
    body {
      color:#1f2a24;
      background:#f6f3ea;
      margin:0;
      font-family:system-ui,sans-serif
    }
    main {
      padding:8px 24px
    }
Try it yourself
Nothing

The big photo gets loading="lazy", like every next/image by default, and the head has no <link rel="preload"> for it. The browser finds the photo only when it reads the <img> in the body.

Each version was built with next build from Next.js 16.4.3. Only line 17 changes. In the Network tab, look at the head for a <link rel="preload" as="image">, then at the first <img>'s loading attribute. Reset goes back to the first version.

With nothing on line 17, the big photo is loading="lazy", like every other. preload drops loading="lazy" and puts <link rel="preload" as="image" imagesrcset="…" imagesizes="100vw"> in the head. In the App Router, Next.js does that by calling React's ReactDOM.preload. preload is new in Next.js 16; it replaces priority, which still works, makes the same HTML, and is deprecated.

For most cases, the docs give two alternatives to preload, each on its own: loading="eager", which loads the photo at once instead of lazily, or fetchPriority="high", which tells the browser to fetch it before other images. Keep preload for a photo that is surely the LCP. Don't combine preload with loading or fetchPriority, and don't preload several photos that could each be the LCP on different screens.

The two alternatives are recorded too. With loading="eager", this build's head still has a preload link: React's server renderer added one for an image that isn't lazy. With fetchPriority="high" alone, the <img> kept loading="lazy" and the head has no preload link. Both are what 16.4.3 and its React did here, not promises in the docs: what the docs promise is the attribute on the <img>.

The Steps tab of the preload version shows what lazy loading does for the rest of the menu:

The menu in real Chrome: open, then scroll downNext.js
app/public/teas/
app/page.tsx
import Image from "next/image";import mintTea from "./mint-tea.jpg"; const teas = [  { name: "Green tea", photo: "/teas/green-tea.jpg" },  { name: "Oolong", photo: "/teas/oolong.jpg" },  { name: "Chamomile", photo: "/teas/chamomile.jpg" },  { name: "Jasmine", photo: "/teas/jasmine.jpg" },  { name: "Hibiscus", photo: "/teas/hibiscus.jpg" },  { name: "Black tea", photo: "/teas/black-tea.jpg" },  { name: "Rooibos", photo: "/teas/rooibos.jpg" },]; export default function HomePage() {  return (    <main>      <Image src={mintTea} alt="A cup of mint tea on a saucer" preload sizes="100vw" style={{ width: "100%", height: "auto" }} />      <h1>Tea shop</h1>      {teas.map((tea) => (        <section key={tea.name}>          <h2>{tea.name}</h2>          <Image src={tea.photo} alt={tea.name} width={600} height={400} />        </section>      ))}    </main>  );}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/Tea shop
The page at / after step 1, as Chrome drew it with its styles. It says: Tea shop. Green tea. Oolong. Chamomile. Jasmine. Hibiscus. Black tea. Rooibos.

A picture of the window in real Chrome after this step. Open it full size

Focus
nothing: the page itself
Scrolled
0 px down
Stylesheets on the page
0b5zjm047lwhu.css

Requests the page made during this step (12), files and other sites included

  1. GET/200a whole page
  2. GET/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fmint-tea.3hpoavs8o5i69.jpg&w=1920&q=75200image/webp
  3. GET/_next/static/chunks/0b5zjm047lwhu.css200text/css
  4. GET/_next/static/chunks/31qp8jfkw0x2h.js200application/javascript
  5. GET/_next/static/chunks/0i46y6vlt4sc3.js200application/javascript
  6. GET/_next/static/chunks/20box33a0_n53.js200application/javascript
  7. GET/_next/static/chunks/turbopack-3-ry682tkyiqk.js200application/javascript
  8. GET/_next/static/chunks/1tq6ppe__m8m-.js200application/javascript
  9. GET/_next/static/chunks/0bts8xcjgykqh.js200application/javascript
  10. GET/_next/image?url=%2Fteas%2Fgreen-tea.jpg&w=1200&q=75200image/webp
  11. GET/_next/image?url=%2Fteas%2Foolong.jpg&w=1200&q=75200image/webp
  12. GET/_next/image?url=%2Fteas%2Fchamomile.jpg&w=1200&q=75200image/webp

Step 1 opens /. At the end of its list are the photos Chrome asked for: the big mint tea, and the first three menu photos, Green tea, Oolong and Chamomile. In this recording, Chrome loaded lazy photos a little before they reached the screen; how far ahead is up to the browser. Step 2 scrolls down the page, and Chrome asks for the other four, Jasmine to Rooibos. A visitor who never scrolls never downloads them.

A blurred placeholder

While a photo loads, its box is empty. placeholder="blur" shows a blurred version of the photo there instead, until the real one arrives:

The mint tea photo with a blurred placeholderNext.js
app/
app/page.tsx
import Image from "next/image";import mintTea from "./mint-tea.jpg"; export default function HomePage() {  return (    <main>      <h1>Tea shop</h1>      <Image src={mintTea} alt="A cup of mint tea on a saucer" width={400} placeholder="blur" />      <p>Mint tea, $3 a box.</p>    </main>  );}
Recorded from Next.js 16.4.3

GET /200

Response headers
Vary rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch, Accept-Encoding
x-nextjs-cache HIT
x-nextjs-prerender 1
x-nextjs-stale-time 300
X-Powered-By Next.js
Cache-Control s-maxage=31536000
Content-Type text/html; charset=utf-8

The HTML that arrived, one element per line, with the code of its 17 scripts left out

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="/_next/static/chunks/0b5zjm047lwhu.css" data-precedence="next">
    <link rel="preload" as="script" fetchpriority="low" href="/_next/static/chunks/31qp8jfkw0x2h.js">
    <script src="/_next/static/chunks/0i46y6vlt4sc3.js" async=""></script>
    <script src="/_next/static/chunks/20box33a0_n53.js" async=""></script>
    <script src="/_next/static/chunks/turbopack-3-ry682tkyiqk.js" async=""></script>
    <script src="/_next/static/chunks/1tq6ppe__m8m-.js" async=""></script>
    <script src="/_next/static/chunks/0bts8xcjgykqh.js" async=""></script>
    <title>Tea shop</title>
    <script src="/_next/static/chunks/0cz1d0mv5g_q7.js" nomodule=""></script>
  </head>
  <body>
    <div hidden="">
      <!--$-->
      <!--/$-->
    </div>
    <!--&-->
    <main>
      <h1>Tea shop</h1>
      <img alt="A cup of mint tea on a saucer" loading="lazy" width="400" height="267" decoding="async" data-nimg="1" style="color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url(&quot;data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 200'%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3CfeColorMatrix values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 100 -1' result='s'/%3E%3CfeFlood x='0' y='0' width='100%25' height='100%25'/%3E%3CfeComposite operator='out' in='s'/%3E%3CfeComposite in2='SourceGraphic'/%3E%3CfeGaussianBlur stdDeviation='20'/%3E%3C/filter%3E%3Cimage width='100%25' height='100%25' x='0' y='0' preserveAspectRatio='none' style='filter: url(%23b);' href='data:image/jpeg;base64,/9j/4AAQSkZJRgABAgAAAQABAAD/wAARCAAFAAgDAREAAhEBAxEB/9sAQwAKBwcIBwYKCAgICwoKCw4YEA4NDQ4dFRYRGCMfJSQiHyIhJis3LyYpNCkhIjBBMTQ5Oz4+PiUuRElDPEg3PT47/9sAQwEKCwsODQ4cEBAcOygiKDs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDoG8Y3TaeZDbx+bu65O3H0/wDr1y+0di3QV7XP/9k='/%3E%3C/svg%3E&quot;)" srcset="/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fmint-tea.3hpoavs8o5i69.jpg&w=640&q=75 1x, /_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fmint-tea.3hpoavs8o5i69.jpg&w=828&q=75 2x" src="/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fmint-tea.3hpoavs8o5i69.jpg&w=828&q=75">
      <p>Mint tea, $3 a box.</p>
    </main>
    <!--$-->
    <!--/$-->
    <!--/&-->
    <script id="_R_">…</script>
    <script src="/_next/static/chunks/31qp8jfkw0x2h.js" async=""></script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
    <script>…</script>
  </body>
</html>

Line 8 adds placeholder="blur". In the Network tab, the <img>'s style now has a background-image: a small SVG with a blur filter, wrapped around data:image/jpeg;base64,…, a copy of the photo only 8 by 5 pixels in this build, written into the HTML as text. The build made it from the imported file, and called it the blurDataURL. When the photo has loaded, React takes the background away.

Only a static import of a JPEG, PNG, WebP or AVIF file gets a blurDataURL made for it. For a photo in public or a remote one, pass your own blurDataURL: a tiny image written as a data: address, 10 pixels wide or less. placeholder can also be a data:image/… address itself, like a flat color.

remotePatterns and image security

The optimizer is a public address on your server that fetches files and does work for anyone who asks. Without limits, a stranger could make it resize photos from any server, at every width, or reach servers inside your own network. So every request is checked first. The remote photo's project also sent the optimizer four bad requests:

Four requests the optimizer refusesNext.js
app/public/teas/
next.config.ts
import type { NextConfig } from "next"; const nextConfig: NextConfig = {  /* config options here */  cacheComponents: true,  partialPrefetching: true,  images: {    remotePatterns: [      new URL("https://images.example.com/teas/**"),      { protocol: "http", hostname: "localhost", port: "4000", pathname: "/teas/**" },    ],  },}; export default nextConfig;
Recorded from Next.js 16.4.3

GET /_next/image?url=https%3A%2F%2Fphotos.other.example%2Foolong.jpg&w=640&q=75400

Response headers

The body that arrived, as it arrived

"url" parameter is not allowed
  • A photo from another server, photos.other.example: 400, "url" parameter is not allowed. It matches no pattern.
  • A width that isn't in the list, w=500: 400, "w" parameter (width) of 500 is not allowed. Only the widths in deviceSizes and imageSizes are.
  • A quality that isn't allowed, q=90: 400, "q" parameter (quality) of 90 is not allowed. Since Next.js 16, qualities is [75] unless you change it. Line 8 of app/page.tsx asks for quality={90}, and the srcset in the first response says q=75: a quality prop outside the list becomes the closest allowed value.
  • A photo on this computer, http://localhost:4000/teas/oolong.jpg, which line 10 of next.config.ts allows: 400 as well. The Terminal tab shows what next start printed: upstream image http://localhost:4000/teas/oolong.jpg hostname resolved to private IP ["::1","127.0.0.1"].

The last check is new in Next.js 16. Before it fetches a remote photo, the optimizer looks up the server's address and refuses a private one, like your own computer or a server inside your network (image-optimizer.ts). That stops an attack called server-side request forgery: making your server fetch something only it can reach. To allow private addresses, for an app on a private network, set images.dangerouslyAllowLocalIP to true, knowing the risk.

A request on the left goes through four gates in a column: is the url allowed, is the width in the list, is the quality in the list, and is the server's address public. Each gate has an arrow to the right to a card saying 400. After the last gate, an arrow goes down to the photo.url allowed?width in the list?quality in the list?public address?no400, not allowed400, not allowed400, not allowed400, not allowedall yes: fetch, resize, send
Every request to /_next/image is checked before anything is fetched or resized. The url must match remotePatterns (or be a local path), the width and the quality must be in the lists, and a remote server must not be on a private network. Any "no" is a 400.

A remotePatterns entry can be a new URL(…), as on line 9, or an object with protocol, hostname, port, pathname and search, as on line 10. Make it narrow: a part you leave out matches anything, and ** matches any path below. remotePatterns only checks the first address. A remote server that answers with a redirect is followed up to 3 times by default (maximumRedirects; 0 follows none), and the place it redirects to isn't checked against remotePatterns again. Each address on the way is still refused if it's private: in the 16.4.3 source, the optimizer runs the private-address check again for every redirect. SVG files are served as they are, without the optimizer, because an SVG can hold scripts. Each row is one setting that Next.js 16 changed, from its upgrade guide.

Image settings that changed in Next.js 16
SettingBefore Next.js 16Next.js 16
qualitiesAny quality from 1 to 100.Only [75]. Other quality props become the closest allowed value; other q values in a URL get a 400.
dangerouslyAllowLocalIPRemote images on private addresses were fetched.false: a remote image on a private or local address gets a 400.
maximumRedirectsRedirects were followed with no limit.3. Set 0 to follow none.
minimumCacheTTL60 seconds.14400 seconds: 4 hours.
imageSizesStarted at 16.Starts at 32: [32, 48, 64, 96, 128, 256, 384].
A local src with a query string, like /teas/oolong.jpg?v=2Allowed.Needs an images.localPatterns entry whose search is that exact query string: search: "?v=2".

Checking the tea shop's photos

Before the shop goes live, Asha checks each photo on the site: does it keep its space, does it download a sensible file, and will the optimizer serve it? Sort each one.

Fine, or needs a fix?
  • <Image src={mintTea} alt="A cup of mint tea" /> after a static import.
  • <Image src="/teas/oolong.jpg" alt="Oolong" width={400} height={267} />.
  • <Image src="/teas/oolong.jpg" alt="Oolong" />.
  • A fill photo in a box a third of the page wide, with no sizes.
  • A fill photo inside a <div> with position: relative and an aspect-ratio.
  • The seventh tea photo, far down the menu, with no preload.
  • A photo from https://cdn.teafarm.example/ with nothing in remotePatterns.
Try it yourself
0 of 7 correct

Sort each photo: is it fine as it is, or does it need a fix?

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "width and height set how big the photo shows." No. CSS does. They give the <img> its ratio, so the browser keeps the right space before the file arrives.
  • "next build resizes every photo." No. The optimizer resizes on request, the first time a width is asked for, and keeps the result.
  • "fill photos are always the right size." No. Without sizes, the browser plans for the whole window and downloads a much bigger file than the box needs.
  • "A missing width doesn't matter, because next build accepts it." No. The docs require width and height, or fill. The recorded 16.4.3 build passing is no promise, and the page it made jumps when the photo loads.
  • "Every photo should get preload." No. Only the one photo that is the LCP. The rest should stay lazy, so they don't compete with it.
  • "remotePatterns lets the optimizer fetch anything on that server." No. The first address must match the pattern. A redirect from there isn't checked against it again, but redirects stop after maximumRedirects, and no address on the way may be private unless dangerouslyAllowLocalIP is on.

Practice exercises

These exercises are about Next.js projects, which can't run inside this page. Answer first, then use the hints.

Exercise 1 · Warm-upKeep the green tea photo's space

This page builds, but the price jumps down when the photo loads. Which two props do you add to line 7?

Starter codeTSX
import Image from "next/image";

export default function HomePage() {
  return (
    <main>
      <h1>Tea shop</h1>
      <Image src="/teas/green-tea.jpg" alt="A cup of green tea on a saucer" />
      <p>Green tea, $4 a box.</p>
    </main>
  );
}

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upA photo that covers its box

    The tea photos have different shapes, and each should cover a 3:2 box. Which prop on line 18 makes the photo fill its parent?

    Starter codeTSX
    import Image from "next/image";
    import styles from "./teas.module.css";
    
    const teas = [
      { name: "Mint tea", photo: "/teas/mint-tea.jpg" },
      { name: "Green tea", photo: "/teas/green-tea.jpg" },
      { name: "Oolong", photo: "/teas/oolong.jpg" },
    ];
    
    export default function TeasPage() {
      return (
        <main>
          <h1>Our teas</h1>
          <ul className={styles.grid}>
            {teas.map((tea) => (
              <li key={tea.name}>
                <div className={styles.photo}>
                  <Image src={tea.photo} alt={tea.name} fill style={{ objectFit: "cover" }} />
                </div>
                <h2>{tea.name}</h2>
              </li>
            ))}
          </ul>
        </main>
      );
    }
    

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeStop downloading 1920-pixel photos

      In an 800-pixel window, Chrome downloads w=1920 for each of these three photos, though each box is about 240 pixels wide. Which prop do you add?

      Starter codeTSX
      import Image from "next/image";
      import styles from "./teas.module.css";
      
      const teas = [
        { name: "Mint tea", photo: "/teas/mint-tea.jpg" },
        { name: "Green tea", photo: "/teas/green-tea.jpg" },
        { name: "Oolong", photo: "/teas/oolong.jpg" },
      ];
      
      export default function TeasPage() {
        return (
          <main>
            <h1>Our teas</h1>
            <ul className={styles.grid}>
              {teas.map((tea) => (
                <li key={tea.name}>
                  <div className={styles.photo}>
                    <Image src={tea.photo} alt={tea.name} fill style={{ objectFit: "cover" }} />
                  </div>
                  <h2>{tea.name}</h2>
                </li>
              ))}
            </ul>
          </main>
        );
      }
      

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeWhich photo gets preload?

        Only one photo on this page should get preload. Which one?

        Starter codeTSX
        import Image from "next/image";
        import mintTea from "./mint-tea.jpg";
        
        const teas = [
          { name: "Green tea", photo: "/teas/green-tea.jpg" },
          { name: "Oolong", photo: "/teas/oolong.jpg" },
          { name: "Chamomile", photo: "/teas/chamomile.jpg" },
          { name: "Jasmine", photo: "/teas/jasmine.jpg" },
          { name: "Hibiscus", photo: "/teas/hibiscus.jpg" },
          { name: "Black tea", photo: "/teas/black-tea.jpg" },
          { name: "Rooibos", photo: "/teas/rooibos.jpg" },
        ];
        
        export default function HomePage() {
          return (
            <main>
              <Image src={mintTea} alt="A cup of mint tea on a saucer" sizes="100vw" style={{ width: "100%", height: "auto" }} />
              <h1>Tea shop</h1>
              {teas.map((tea) => (
                <section key={tea.name}>
                  <h2>{tea.name}</h2>
                  <Image src={tea.photo} alt={tea.name} width={600} height={400} />
                </section>
              ))}
            </main>
          );
        }
        

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeAllow the tea farm's photos

          The shop adds photos from https://cdn.teafarm.example/photos/sencha.jpg, and every one is broken: the optimizer answers 400. What do you add to next.config.ts?

          Starter codeTSX
          import type { NextConfig } from "next";
          
          const nextConfig: NextConfig = {
            /* config options here */
            cacheComponents: true,
            partialPrefetching: true,
            images: {
              remotePatterns: [
                new URL("https://images.example.com/teas/**"),
                { protocol: "http", hostname: "localhost", port: "4000", pathname: "/teas/**" },
              ],
            },
          };
          
          export default nextConfig;
          

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeA quality that isn't allowed

            Line 8 asks for quality={90}, and next.config.ts sets no qualities. Which q does the photo's srcset use?

            Starter codeTSX
            import Image from "next/image";
            
            export default function HomePage() {
              return (
                <main>
                  <h1>Tea shop</h1>
                  <Image src="https://images.example.com/teas/oolong.jpg" alt="A cup of oolong" width={400} height={267} />
                  <Image src="/teas/green-tea.jpg" alt="A cup of green tea" width={400} height={267} quality={90} />
                </main>
              );
            }
            

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask what is in the HTML, and what the optimizer will do with the request.

              The images quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does the <img> that next/image renders point at?

                Choose an answer to see the explanation.

              2. Question 2 of 7This tea page's photos fill boxes a third of the window wide. In an 800-pixel window, which width does Chrome ask for?

                Read the code, then predictTSX
                import Image from "next/image";
                import styles from "./teas.module.css";
                
                const teas = [
                  { name: "Mint tea", photo: "/teas/mint-tea.jpg" },
                  { name: "Green tea", photo: "/teas/green-tea.jpg" },
                  { name: "Oolong", photo: "/teas/oolong.jpg" },
                ];
                
                export default function TeasPage() {
                  return (
                    <main>
                      <h1>Our teas</h1>
                      <ul className={styles.grid}>
                        {teas.map((tea) => (
                          <li key={tea.name}>
                            <div className={styles.photo}>
                              <Image src={tea.photo} alt={tea.name} fill style={{ objectFit: "cover" }} />
                            </div>
                            <h2>{tea.name}</h2>
                          </li>
                        ))}
                      </ul>
                    </main>
                  );
                }
                

                Choose an answer to see the explanation.

              3. Question 3 of 7A photo in public is shown by its path. What do the docs require on its <Image>?

                Choose an answer to see the explanation.

              4. Question 4 of 7Which Accept header gets a WebP back from the optimizer with the default settings?

                Choose an answer to see the explanation.

              5. Question 5 of 7What does preload change in the HTML?

                Choose an answer to see the explanation.

              6. Question 6 of 7In this page, with the default settings, which q does the green tea photo's srcset use?

                Read the code, then predictTSX
                import Image from "next/image";
                
                export default function HomePage() {
                  return (
                    <main>
                      <h1>Tea shop</h1>
                      <Image src="https://images.example.com/teas/oolong.jpg" alt="A cup of oolong" width={400} height={267} />
                      <Image src="/teas/green-tea.jpg" alt="A cup of green tea" width={400} height={267} quality={90} />
                    </main>
                  );
                }
                

                Choose an answer to see the explanation.

              7. Question 7 of 7remotePatterns allows http://localhost:4000/teas/**. What does the optimizer do with a photo from there?

                Choose an answer to see the explanation.

              Key takeaways

              • next/image renders an <img> whose srcset points at /_next/image, which resizes the photo, converts it to WebP (or AVIF) for browsers that accept it, and keeps the result.
              • A static import gives the size and a blur placeholder for free. A path in public or a remote address needs width and height, or fill in a positioned box.
              • With fill or responsive CSS, write sizes, or the browser downloads a file as wide as the window.
              • Photos are lazy by default. Give the LCP photo preload (formerly priority), or one of the alternatives the docs suggest in most cases: loading="eager" or fetchPriority="high".
              • The optimizer refuses a first address outside remotePatterns, widths and qualities outside its lists, and private addresses, with a 400; redirects are limited by maximumRedirects.

              Remember the one-liner.
              Give every photo its shape, tell the browser its width with sizes, preload the one that matters, and allow only the servers you trust.

              Coming next: Optimizing fonts. next/font downloads the shop's fonts at build time and serves them from your own server.

              CompleteFrontend Clear concepts. Working examples.