Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add images.unoptimized: true for easy next export #37698

Merged
merged 13 commits into from
Jun 16, 2022
14 changes: 13 additions & 1 deletion docs/api-reference/next/image.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ description: Enable Image Optimization with the built-in Image component.

| Version | Changes |
| --------- | ----------------------------------------------------------------------------------------------------- |
| `v12.1.7` | Experimental `remotePatterns` configuration added. |
| `v12.2.0` | Experimental `remotePatterns` and experimental `unoptimized` configuration added. |
| `v12.1.1` | `style` prop added. Experimental[\*](#experimental-raw-layout-mode) support for `layout="raw"` added. |
| `v12.1.0` | `dangerouslyAllowSVG` and `contentSecurityPolicy` configuration added. |
| `v12.0.9` | `lazyRoot` prop added. |
Expand Down Expand Up @@ -301,6 +301,18 @@ const Example = () => {
When true, the source image will be served as-is instead of changing quality,
size, or format. Defaults to `false`.

This prop can be assigned to all images by updating `next.config.js` with the following experimental configuration:

```js
module.exports = {
experimental: {
images: {
unoptimized: true,
},
},
}
```

## Other Props

Other properties on the `<Image />` component will be passed to the underlying
Expand Down
4 changes: 2 additions & 2 deletions errors/export-image-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ This is because Next.js optimizes images on-demand, as users request them (not a

- Use [`next start`](https://nextjs.org/docs/api-reference/cli#production) to run a server, which includes the Image Optimization API.
- Use any provider which supports Image Optimization (such as [Vercel](https://vercel.com)).
- [Configure the loader](https://nextjs.org/docs/api-reference/next/image#loader-configuration) in `next.config.js`.
- Use the [`loader`](https://nextjs.org/docs/api-reference/next/image#loader) prop for each instance of `next/image`.
- [Configure `loader`](https://nextjs.org/docs/api-reference/next/image#loader-configuration) in `next.config.js`.
- [Configure `unoptimized`](https://nextjs.org/docs/api-reference/next/image#unoptimized) in `next.config.js`.

### Useful Links

Expand Down
9 changes: 7 additions & 2 deletions errors/invalid-images-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ module.exports = {
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
// limit of 50 domains values
domains: [],
// limit of 50 objects
remotePatterns: [],
// path prefix for Image Optimization API, useful with `loader`
path: '/_next/image',
// loader can be 'default', 'imgix', 'cloudinary', 'akamai', or 'custom'
Expand All @@ -33,6 +31,13 @@ module.exports = {
dangerouslyAllowSVG: false,
// set the Content-Security-Policy header
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
// the following are experimental features, and may cause breaking changes
experimental: {
// limit of 50 objects
remotePatterns: [],
// when true, every image will be unoptimized
unoptimized: false,
},
},
ijjk marked this conversation as resolved.
Show resolved Hide resolved
}
```
Expand Down
1 change: 1 addition & 0 deletions packages/next/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1521,6 +1521,7 @@ export default async function getBaseWebpackConfig(
imageSizes: config.images.imageSizes,
path: config.images.path,
loader: config.images.loader,
experimentalUnoptimized: config?.experimental?.images?.unoptimized,
experimentalLayoutRaw: config.experimental?.images?.layoutRaw,
...(dev
? {
Expand Down
10 changes: 8 additions & 2 deletions packages/next/client/image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ import { ImageConfigContext } from '../shared/lib/image-config-context'
import { warnOnce } from '../shared/lib/utils'
import { normalizePathTrailingSlash } from './normalize-trailing-slash'

const { experimentalLayoutRaw = false, experimentalRemotePatterns = [] } =
(process.env.__NEXT_IMAGE_OPTS as any) || {}
const {
experimentalLayoutRaw = false,
experimentalRemotePatterns = [],
experimentalUnoptimized,
} = (process.env.__NEXT_IMAGE_OPTS as any) || {}
const configEnv = process.env.__NEXT_IMAGE_OPTS as any as ImageConfigComplete
const loadedImageURLs = new Set<string>()
const allImgs = new Map<
Expand Down Expand Up @@ -464,6 +467,9 @@ export default function Image({
) {
isLazy = false
}
if (experimentalUnoptimized) {
unoptimized = true
}

const [blurComplete, setBlurComplete] = useState(false)
const [setIntersection, isIntersected, resetIntersected] =
Expand Down
12 changes: 8 additions & 4 deletions packages/next/export/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ export default async function exportApp(
const {
i18n,
images: { loader = 'default' },
experimental,
} = nextConfig

if (i18n && !options.buildExport) {
Expand All @@ -344,14 +345,17 @@ export default async function exportApp(
.catch(() => ({}))
)

if (isNextImageImported && loader === 'default' && !hasNextSupport) {
if (
isNextImageImported &&
loader === 'default' &&
!experimental?.images?.unoptimized &&
!hasNextSupport
) {
throw new Error(
`Image Optimization using Next.js' default loader is not compatible with \`next export\`.
Possible solutions:
- Use \`next start\` to run a server, which includes the Image Optimization API.
- Use any provider which supports Image Optimization (like Vercel).
- Configure a third-party loader in \`next.config.js\`.
- Use the \`loader\` prop for \`next/image\`.
- Configure \`images.unoptimized = true\` in \`next.config.js\` to disable the Image Optimization API.
Read more: https://nextjs.org/docs/messages/export-image-api`
)
}
Expand Down
1 change: 1 addition & 0 deletions packages/next/server/config-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export interface ExperimentalConfig {
images?: {
layoutRaw: boolean
remotePatterns: RemotePattern[]
unoptimized?: boolean
}
middlewareSourceMaps?: boolean
modularizeImports?: Record<
Expand Down
10 changes: 10 additions & 0 deletions packages/next/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,16 @@ function assignDefaults(userConfig: { [key: string]: any }) {
)}), received (${images.contentSecurityPolicy}).\nSee more info here: https://nextjs.org/docs/messages/invalid-images-config`
)
}

const unoptimized = result.experimental?.images?.unoptimized
if (
typeof unoptimized !== 'undefined' &&
typeof unoptimized !== 'boolean'
) {
throw new Error(
`Specified images.unoptimized should be a boolean, received (${unoptimized}).\nSee more info here: https://nextjs.org/docs/messages/invalid-images-config`
)
}
}

if (result.webpack5 === false) {
Expand Down
35 changes: 35 additions & 0 deletions test/integration/export-image-loader/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,38 @@ describe('Export with custom loader next/image component', () => {
await pagesIndexJs.restore()
})
})

describe('Export with unoptimized next/image component', () => {
beforeAll(async () => {
await nextConfig.replace(
'{ /* replaceme */ }',
JSON.stringify({
experimental: {
images: {
unoptimized: true,
},
},
})
)
})
it('should build successfully', async () => {
await fs.remove(join(appDir, '.next'))
const { code } = await nextBuild(appDir)
if (code !== 0) throw new Error(`build failed with status ${code}`)
})

it('should export successfully', async () => {
const { code } = await nextExport(appDir, { outdir })
if (code !== 0) throw new Error(`export failed with status ${code}`)
})

it('should contain img element with same src in html output', async () => {
const html = await fs.readFile(join(outdir, 'index.html'))
const $ = cheerio.load(html)
expect($('img[src="/o.png"]')).toBeDefined()
})

afterAll(async () => {
await nextConfig.restore()
})
})
7 changes: 7 additions & 0 deletions test/integration/image-component/unoptimized/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
experimental: {
images: {
unoptimized: true,
},
},
}
33 changes: 33 additions & 0 deletions test/integration/image-component/unoptimized/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react'
import Image from 'next/image'
import testJpg from '../public/test.jpg'

const Page = () => {
return (
<div>
<h1>Unoptimized Config</h1>
<p>Scroll down...</p>
<div style={{ height: '1000vh' }} />
<Image id="internal-image" src="/test.png" width={400} height={400} />
<br />
<Image id="static-image" src={testJpg} width={400} height={400} />
<br />
<Image
id="external-image"
src="https://via.placeholder.com/800/000/FFF.png?text=test"
width={400}
height={400}
/>
<div style={{ height: '1000vh' }} />
<Image
id="eager-image"
src="/test.webp"
width={400}
height={400}
loading="eager"
/>
</div>
)
}

export default Page
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
120 changes: 120 additions & 0 deletions test/integration/image-component/unoptimized/test/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/* eslint-env jest */

import { join } from 'path'
import {
check,
findPort,
killApp,
launchApp,
nextBuild,
nextStart,
} from 'next-test-utils'
import webdriver from 'next-webdriver'

const appDir = join(__dirname, '../')
let appPort
let app

function runTests() {
it('should not optimize any image', async () => {
const browser = await webdriver(appPort, '/')

expect(
await browser.elementById('internal-image').getAttribute('src')
).toMatch('data:')
expect(
await browser.elementById('static-image').getAttribute('src')
).toMatch('data:')
expect(
await browser.elementById('external-image').getAttribute('src')
).toMatch('data:')
expect(await browser.elementById('eager-image').getAttribute('src')).toBe(
'/test.webp'
)

expect(
await browser.elementById('internal-image').getAttribute('srcset')
).toBeNull()
expect(
await browser.elementById('static-image').getAttribute('srcset')
).toBeNull()
expect(
await browser.elementById('external-image').getAttribute('srcset')
).toBeNull()
expect(
await browser.elementById('eager-image').getAttribute('srcset')
).toBeNull()

await browser.eval(
`document.getElementById("internal-image").scrollIntoView({behavior: "smooth"})`
)
await browser.eval(
`document.getElementById("static-image").scrollIntoView({behavior: "smooth"})`
)
await browser.eval(
`document.getElementById("external-image").scrollIntoView({behavior: "smooth"})`
)
await browser.eval(
`document.getElementById("eager-image").scrollIntoView({behavior: "smooth"})`
)

await check(
() =>
browser.eval(`document.getElementById("external-image").currentSrc`),
/placeholder.com/
)

expect(
await browser.elementById('internal-image').getAttribute('src')
).toBe('/test.png')
expect(
await browser.elementById('static-image').getAttribute('src')
).toMatch(/test(.*)jpg/)
expect(
await browser.elementById('external-image').getAttribute('src')
).toBe('https://via.placeholder.com/800/000/FFF.png?text=test')
expect(await browser.elementById('eager-image').getAttribute('src')).toBe(
'/test.webp'
)

expect(
await browser.elementById('internal-image').getAttribute('srcset')
).toBeNull()
expect(
await browser.elementById('static-image').getAttribute('srcset')
).toBeNull()
expect(
await browser.elementById('external-image').getAttribute('srcset')
).toBeNull()
expect(
await browser.elementById('eager-image').getAttribute('srcset')
).toBeNull()
})
}

describe('Unoptimized Image Tests', () => {
describe('dev mode', () => {
beforeAll(async () => {
appPort = await findPort()
app = await launchApp(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
})

runTests('dev')
})

describe('server mode', () => {
beforeAll(async () => {
await nextBuild(appDir)
appPort = await findPort()
app = await nextStart(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
})

runTests('server')
})
})