Skip to content

Commit

Permalink
Fix image cache for vector and animated images (#20480)
Browse files Browse the repository at this point in the history
Fixes: #19100
> According to https://nextjs.org/docs/basic-features/image-optimization#caching Next.js populates a cache dir when using the new <Image /> component. This is not the case when using SVG files. This results in a performance penalty.

I created a function for writing images to cache directory (`wrirteToCacheDir`) and it is called for all images.
However, vector and animated images are not optimized before writing them to cache dir

Related to #18179
  • Loading branch information
kaykdm committed Jan 13, 2021
1 parent 69ff95f commit e1184fb
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 6 deletions.
23 changes: 17 additions & 6 deletions packages/next/next-server/server/image-optimizer.ts
Expand Up @@ -218,17 +218,19 @@ export async function imageOptimizer(
}
}

const expireAt = maxAge * 1000 + now

if (upstreamType) {
const vector = VECTOR_TYPES.includes(upstreamType)
const animate =
ANIMATABLE_TYPES.includes(upstreamType) && isAnimated(upstreamBuffer)
if (vector || animate) {
await writeToCacheDir(hashDir, upstreamType, expireAt, upstreamBuffer)
sendResponse(req, res, upstreamType, upstreamBuffer)
return { finished: true }
}
}

const expireAt = maxAge * 1000 + now
let contentType: string

if (mimeType) {
Expand Down Expand Up @@ -276,11 +278,7 @@ export async function imageOptimizer(
}

const optimizedBuffer = await transformer.toBuffer()
await promises.mkdir(hashDir, { recursive: true })
const extension = getExtension(contentType)
const etag = getHash([optimizedBuffer])
const filename = join(hashDir, `${expireAt}.${etag}.${extension}`)
await promises.writeFile(filename, optimizedBuffer)
await writeToCacheDir(hashDir, contentType, expireAt, optimizedBuffer)
sendResponse(req, res, contentType, optimizedBuffer)
} catch (error) {
sendResponse(req, res, upstreamType, upstreamBuffer)
Expand All @@ -289,6 +287,19 @@ export async function imageOptimizer(
return { finished: true }
}

async function writeToCacheDir(
dir: string,
contentType: string,
expireAt: number,
buffer: Buffer
) {
await promises.mkdir(dir, { recursive: true })
const extension = getExtension(contentType)
const etag = getHash([buffer])
const filename = join(dir, `${expireAt}.${etag}.${extension}`)
await promises.writeFile(filename, buffer)
}

function sendResponse(
req: IncomingMessage,
res: ServerResponse,
Expand Down
38 changes: 38 additions & 0 deletions test/integration/image-optimizer/test/index.test.js
Expand Up @@ -380,6 +380,44 @@ function runTests({ w, isDev, domains }) {
expect(json2).toStrictEqual(json1)
})

it('should use cached image file when parameters are the same for svg', async () => {
await fs.remove(imagesDir)

const query = { url: '/test.svg', w, q: 80 }
const opts = { headers: { accept: 'image/webp' } }

const res1 = await fetchViaHTTP(appPort, '/_next/image', query, opts)
expect(res1.status).toBe(200)
expect(res1.headers.get('Content-Type')).toBe('image/svg+xml')
const json1 = await fsToJson(imagesDir)
expect(Object.keys(json1).length).toBe(1)

const res2 = await fetchViaHTTP(appPort, '/_next/image', query, opts)
expect(res2.status).toBe(200)
expect(res2.headers.get('Content-Type')).toBe('image/svg+xml')
const json2 = await fsToJson(imagesDir)
expect(json2).toStrictEqual(json1)
})

it('should use cached image file when parameters are the same for animated gif', async () => {
await fs.remove(imagesDir)

const query = { url: '/animated.gif', w, q: 80 }
const opts = { headers: { accept: 'image/webp' } }

const res1 = await fetchViaHTTP(appPort, '/_next/image', query, opts)
expect(res1.status).toBe(200)
expect(res1.headers.get('Content-Type')).toBe('image/gif')
const json1 = await fsToJson(imagesDir)
expect(Object.keys(json1).length).toBe(1)

const res2 = await fetchViaHTTP(appPort, '/_next/image', query, opts)
expect(res2.status).toBe(200)
expect(res2.headers.get('Content-Type')).toBe('image/gif')
const json2 = await fsToJson(imagesDir)
expect(json2).toStrictEqual(json1)
})

it('should set 304 status without body when etag matches if-none-match', async () => {
const query = { url: '/test.jpg', w, q: 80 }
const opts1 = { headers: { accept: 'image/webp' } }
Expand Down

0 comments on commit e1184fb

Please sign in to comment.