Skip to content

Commit

Permalink
Add minimumCacheTTL config for Image Optimization (#27200)
Browse files Browse the repository at this point in the history
- Closes #23328  
- Related to #19914 
- Related to #22319 


## Feature

- [x] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [x] Related issues linked using `fixes #number`
- [x] Integration tests added
- [x] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [x] Errors have helpful link attached, see `contributing.md`
  • Loading branch information
styfle committed Jul 15, 2021
1 parent 71665b2 commit 8151a7e
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 45 deletions.
18 changes: 16 additions & 2 deletions docs/basic-features/image-optimization.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,11 @@ Images are optimized dynamically upon request and stored in the `<distDir>/cache

The expiration (or rather Max Age) is defined by the upstream server's `Cache-Control` header.

If `s-maxage` is found in `Cache-Control`, it is used. If no `s-maxage` is found, then `max-age` is used. If no `max-age` is found, then 60 seconds is used.
If `s-maxage` is found in `Cache-Control`, it is used. If no `s-maxage` is found, then `max-age` is used. If no `max-age` is found, then [`minimumCacheTTL`](#minimum-cache-ttl) is used.

You can configure [`deviceSizes`](#device-sizes) and [`imageSizes`](#device-sizes) to reduce the total number of possible generated images.
You can configure [`minimumCacheTTL`](#minimum-cache-ttl) to increase the cache duration when the upstream image does not include `max-age`.

You can also configure [`deviceSizes`](#device-sizes) and [`imageSizes`](#device-sizes) to reduce the total number of possible generated images.

## Advanced

Expand Down Expand Up @@ -172,6 +174,18 @@ module.exports = {
}
```

### Minimum Cache TTL

You can configure the time to live (TTL) in seconds for cached optimized images. This will configure the server's image cache as well as the `Cache-Control` header sent to the browser. This is a global setting that will affect all images. In most cases, its better to use a [Static Image Import](#image-Imports) which will handle hashing file contents and caching the file. You can also configure the `Cache-Control` header on an individual upstream image instead.

```js
module.exports = {
images: {
minimumCacheTTL: 60,
},
}
```

### Disable Static Imports

The default behavior allows you to import static files such as `import icon from './icon.png` and then pass that to the `src` property.
Expand Down
6 changes: 5 additions & 1 deletion errors/invalid-images-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ module.exports = {
// limit of 50 domains values
domains: [],
path: '/_next/image',
// loader can be 'default', 'imgix', 'cloudinary', or 'akamai'
// loader can be 'default', 'imgix', 'cloudinary', 'akamai', or 'custom'
loader: 'default',
// disable static imports for image files
disableStaticImages: false,
// minimumCacheTTL is in seconds, must be integer 0 or more
minimumCacheTTL: 60,
},
}
```
Expand Down
11 changes: 11 additions & 0 deletions packages/next/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,17 @@ function assignDefaults(userConfig: { [key: string]: any }) {
if (images.path === imageConfigDefault.path && result.basePath) {
images.path = `${result.basePath}${images.path}`
}

if (
images.minimumCacheTTL &&
(!Number.isInteger(images.minimumCacheTTL) || images.minimumCacheTTL < 0)
) {
throw new Error(
`Specified images.minimumCacheTTL should be an integer 0 or more
', '
)}), received (${images.minimumCacheTTL}).\nSee more info here: https://nextjs.org/docs/messages/invalid-images-config`
)
}
}

if (result.i18n) {
Expand Down
2 changes: 2 additions & 0 deletions packages/next/server/image-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type ImageConfig = {
path: string
domains?: string[]
disableStaticImages?: boolean
minimumCacheTTL?: number
}

export const imageConfigDefault: ImageConfig = {
Expand All @@ -24,4 +25,5 @@ export const imageConfigDefault: ImageConfig = {
loader: 'default',
domains: [],
disableStaticImages: false,
minimumCacheTTL: 60,
}
22 changes: 15 additions & 7 deletions packages/next/server/image-optimizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,13 @@ export async function imageOptimizer(
isDev = false
) {
const imageData: ImageConfig = nextConfig.images || imageConfigDefault
const { deviceSizes = [], imageSizes = [], domains = [], loader } = imageData
const {
deviceSizes = [],
imageSizes = [],
domains = [],
loader,
minimumCacheTTL = 60,
} = imageData

if (loader !== 'default') {
await server.render404(req, res, parsedUrl)
Expand Down Expand Up @@ -206,7 +212,10 @@ export async function imageOptimizer(
upstreamType =
detectContentType(upstreamBuffer) ||
upstreamRes.headers.get('Content-Type')
maxAge = getMaxAge(upstreamRes.headers.get('Cache-Control'))
maxAge = getMaxAge(
upstreamRes.headers.get('Cache-Control'),
minimumCacheTTL
)
} else {
try {
const resBuffers: Buffer[] = []
Expand Down Expand Up @@ -261,7 +270,7 @@ export async function imageOptimizer(
upstreamBuffer = Buffer.concat(resBuffers)
upstreamType =
detectContentType(upstreamBuffer) || mockRes.getHeader('Content-Type')
maxAge = getMaxAge(mockRes.getHeader('Cache-Control'))
maxAge = getMaxAge(mockRes.getHeader('Cache-Control'), minimumCacheTTL)
} catch (err) {
res.statusCode = 500
res.end('"url" parameter is valid but upstream response is invalid')
Expand Down Expand Up @@ -529,8 +538,7 @@ export function detectContentType(buffer: Buffer) {
return null
}

export function getMaxAge(str: string | null): number {
const minimum = 60
export function getMaxAge(str: string | null, minimumCacheTTL: number): number {
const map = parseCacheControl(str)
if (map) {
let age = map.get('s-maxage') || map.get('max-age') || ''
Expand All @@ -539,8 +547,8 @@ export function getMaxAge(str: string | null): number {
}
const n = parseInt(age, 10)
if (!isNaN(n)) {
return Math.max(n, minimum)
return Math.max(n, minimumCacheTTL)
}
}
return minimum
return minimumCacheTTL
}
26 changes: 13 additions & 13 deletions test/integration/image-optimizer/test/get-max-age.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,42 @@ import { getMaxAge } from '../../../../packages/next/dist/server/image-optimizer

describe('getMaxAge', () => {
it('should return default when no cache-control provided', () => {
expect(getMaxAge()).toBe(60)
expect(getMaxAge(undefined, 60)).toBe(60)
})
it('should return default when cache-control is null', () => {
expect(getMaxAge(null)).toBe(60)
expect(getMaxAge(null, 60)).toBe(60)
})
it('should return default when cache-control is empty string', () => {
expect(getMaxAge('')).toBe(60)
expect(getMaxAge('', 60)).toBe(60)
})
it('should return default when cache-control max-age is less than default', () => {
expect(getMaxAge('max-age=30')).toBe(60)
expect(getMaxAge('max-age=30', 60)).toBe(60)
})
it('should return default when cache-control max-age is not a number', () => {
expect(getMaxAge('max-age=foo')).toBe(60)
expect(getMaxAge('max-age=foo', 60)).toBe(60)
})
it('should return default when cache-control is no-cache', () => {
expect(getMaxAge('no-cache')).toBe(60)
expect(getMaxAge('no-cache', 60)).toBe(60)
})
it('should return cache-control max-age lowercase', () => {
expect(getMaxAge('max-age=9999')).toBe(9999)
expect(getMaxAge('max-age=9999', 60)).toBe(9999)
})
it('should return cache-control MAX-AGE uppercase', () => {
expect(getMaxAge('MAX-AGE=9999')).toBe(9999)
expect(getMaxAge('MAX-AGE=9999', 60)).toBe(9999)
})
it('should return cache-control s-maxage lowercase', () => {
expect(getMaxAge('s-maxage=9999')).toBe(9999)
expect(getMaxAge('s-maxage=9999', 60)).toBe(9999)
})
it('should return cache-control S-MAXAGE', () => {
expect(getMaxAge('S-MAXAGE=9999')).toBe(9999)
expect(getMaxAge('S-MAXAGE=9999', 60)).toBe(9999)
})
it('should return cache-control s-maxage with spaces', () => {
expect(getMaxAge('public, max-age=5555, s-maxage=9999')).toBe(9999)
expect(getMaxAge('public, max-age=5555, s-maxage=9999', 60)).toBe(9999)
})
it('should return cache-control s-maxage without spaces', () => {
expect(getMaxAge('public,s-maxage=9999,max-age=5555')).toBe(9999)
expect(getMaxAge('public,s-maxage=9999,max-age=5555', 60)).toBe(9999)
})
it('should return cache-control for a quoted value', () => {
expect(getMaxAge('public, s-maxage="9999", max-age="5555"')).toBe(9999)
expect(getMaxAge('public, s-maxage="9999", max-age="5555"', 60)).toBe(9999)
})
})

0 comments on commit 8151a7e

Please sign in to comment.