Skip to content

Commit 7a63d2f

Browse files
sarahetterstephmarie17kodiakhq[bot]
authored
feat: make edge image opt-in instead of opt-out (#1935)
* feat: make edge image opt in instead of opt out * chore: lint * chore: prettier * feat: add to Readme * chore: add log line on how to enable edge images * chore: adding log message for early exit * chore: format * fix: unskip test * Update README.md Co-authored-by: Stephanie <[email protected]> * Update README.md Co-authored-by: Stephanie <[email protected]> * Update README.md Co-authored-by: Stephanie <[email protected]> * chore: Update README.md Co-authored-by: Stephanie <[email protected]> * chore: Update README.md Co-authored-by: Stephanie <[email protected]> --------- Co-authored-by: Stephanie <[email protected]> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
1 parent 8144348 commit 7a63d2f

File tree

3 files changed

+153
-113
lines changed

3 files changed

+153
-113
lines changed

README.md

+33-19
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,23 @@ commands separately will not work, because the Next.js Runtime will not generate
1515

1616
If you use [`next/image`](https://nextjs.org/docs/basic-features/image-optimization), your images will be automatically
1717
optimized at runtime, ensuring that they are served at the best size and format. The image will be processed on the
18-
first request which means it may take longer to load, but the generated image is then cached at the edge and served as a
19-
static file to future visitors. By default, Next.js will deliver WebP images if the browser supports it. WebP is a new
20-
image format with wide browser support that will usually generate smaller files than png or jpg. You can additionally
21-
enable the AVIF format, which is often even smaller in filesize than WebP. The drawback is that with particularly large
22-
images AVIF may take too long to generate, meaning the function times-out. You can configure
18+
first request which means it may take longer to load, but the generated image is then cached and served as a static file
19+
to future visitors. By default, Next.js will deliver WebP images if the browser supports it. WebP is a modern image format
20+
with wide browser support that will usually generate smaller files than PNG or JPG. Additionally, you can enable AVIF
21+
format, which is often even smaller in filesize than WebP. The drawback is that with particularly large images, AVIF images may
22+
take too long to generate, and the function times-out. You can configure
2323
[the supported image formats](https://nextjs.org/docs/api-reference/next/image#acceptable-formats) in your
2424
`next.config.js` file.
2525

26+
### Enabling Edge Images
27+
28+
It is possible to run image content negotiation on the edge. This allows images to be processed on the first request,
29+
and then, in future loads, served from cache on the edge.
30+
2631
In order to deliver the correct format to a visitor's browser, this uses a Netlify Edge Function. In some cases your
27-
site may not support Edge Functions, in which case it will instead fall back to delivering the original file format. You
28-
may also manually disable the Edge Function by setting the environment variable `NEXT_DISABLE_EDGE_IMAGES` to `true`.
32+
site may not support Edge Functions, in which case it will instead fall back to delivering the original file format.
33+
34+
To turn on Edge image handling for Next/Image, set the environment variable `NEXT_FORCE_EDGE_IMAGES` to `true`
2935

3036
## Returning custom response headers on images handled by `ipx`
3137

@@ -44,23 +50,28 @@ by targeting the `/_next/image/*` route:
4450

4551
## Disabling included image loader
4652

47-
If you wish to disable the use of the image loader which is bundled into the runtime by default, set the `DISABLE_IPX` environment variable to `true`.
53+
If you wish to disable the use of the image loader which is bundled into the runtime by default, set the `DISABLE_IPX`
54+
environment variable to `true`.
4855

49-
This should only be done if the site is not using `next/image` or is using a different loader (such as Cloudinary or Imgix).
56+
This should only be done if the site is not using `next/image` or is using a different loader (such as Cloudinary or
57+
Imgix).
5058

51-
See the [Next.js documentation](https://nextjs.org/docs/api-reference/next/image#built-in-loaders) for image loader options.
59+
See the [Next.js documentation](https://nextjs.org/docs/api-reference/next/image#built-in-loaders) for image loader
60+
options.
5261

5362
## Next.js Middleware on Netlify
5463

5564
Next.js Middleware works out of the box on Netlify. By default, middleware runs using Netlify Edge Functions. For legacy
5665
support for running Middleware at the origin, set the environment variable `NEXT_DISABLE_NETLIFY_EDGE` to `true`. Be
5766
aware that this will result in slower performance, as all pages that match middleware must use SSR.
5867

59-
For more details on Next.js Middleware with Netlify, see the [middleware docs](https://docs.netlify.com/integrations/frameworks/next-js/middleware/).
68+
For more details on Next.js Middleware with Netlify, see the
69+
[middleware docs](https://docs.netlify.com/integrations/frameworks/next-js/middleware/).
6070

6171
### Limitations
6272

63-
Due to how the site configuration is handled when it's run using Netlify Edge Functions, data such as `locale` and `defaultLocale` will be missing on the `req.nextUrl` object when running `netlify dev`.
73+
Due to how the site configuration is handled when it's run using Netlify Edge Functions, data such as `locale` and
74+
`defaultLocale` will be missing on the `req.nextUrl` object when running `netlify dev`.
6475

6576
However, this data is available on `req.nextUrl` in a production environment.
6677

@@ -112,8 +123,9 @@ following ways:
112123

113124
### From the UI (Recommended):
114125

115-
You can go to the [UI](https://app.netlify.com/plugins/@netlify/plugin-nextjs/install) and choose the site to install the Next.js Runtime on. This method
116-
is recommended because you will benefit from auto-upgrades to important fixes and feature updates.
126+
You can go to the [UI](https://app.netlify.com/plugins/@netlify/plugin-nextjs/install) and choose the site to install
127+
the Next.js Runtime on. This method is recommended because you will benefit from auto-upgrades to important fixes and
128+
feature updates.
117129

118130
### From `npm`:
119131

@@ -139,18 +151,20 @@ If you previously set these values, they're no longer needed and should be remov
139151
- `external_node_modules` in `netlify.toml`
140152
- The environment variable `NEXT_USE_NETLIFY_EDGE` can be removed as this is now the default
141153

142-
The `serverless` and `experimental-serverless-trace` targets are deprecated in Next.js 12, and all builds with this Next.js
143-
Runtime will now use the default `server` target. If you previously set the target in your `next.config.js`, you should
144-
remove it.
154+
The `serverless` and `experimental-serverless-trace` targets are deprecated in Next.js 12, and all builds with this
155+
Next.js Runtime will now use the default `server` target. If you previously set the target in your `next.config.js`, you
156+
should remove it.
145157

146158
If you currently use redirects or rewrites on your site, see
147159
[the Rewrites and Redirects guide](https://docs.netlify.com/integrations/frameworks/next-js/redirects-and-rewrites/) for
148160
information on changes to how they are handled in this version. In particular, note that `_redirects` and `_headers`
149161
files must be placed in `public`, not in the root of the site.
150162

151163
## Using with pnpm
152-
If your site uses pnpm to manage dependencies, currently you must [enable public hoisting](https://pnpm.io/npmrc#public-hoist-pattern).
153-
The simplest way to do this is to create a `.npmrc` file in the root of your project with the content:
164+
165+
If your site uses pnpm to manage dependencies, currently you must
166+
[enable public hoisting](https://pnpm.io/npmrc#public-hoist-pattern). The simplest way to do this is to create a
167+
`.npmrc` file in the root of your project with the content:
154168

155169
```ini
156170
public-hoist-pattern[]=*

packages/runtime/src/helpers/edge.ts

+106-94
Original file line numberDiff line numberDiff line change
@@ -358,11 +358,106 @@ export const writeEdgeFunctions = async ({
358358
await writeJSON(join(edgeFunctionRoot, 'edge-shared', 'nextConfig.json'), nextConfig)
359359
await copy(join(publish, 'prerender-manifest.json'), join(edgeFunctionRoot, 'edge-shared', 'prerender-manifest.json'))
360360

361+
// early return if edge is disabled
362+
if (destr(process.env.NEXT_DISABLE_NETLIFY_EDGE)) {
363+
console.log('Environment variable NEXT_DISABLE_NETLIFY_EDGE has been set, skipping Netlify Edge Function creation.')
364+
return
365+
}
366+
367+
const rscFunctions = await writeRscDataEdgeFunction({
368+
prerenderManifest: await loadPrerenderManifest(netlifyConfig),
369+
appPathRoutesManifest: await loadAppPathRoutesManifest(netlifyConfig),
370+
})
371+
372+
manifest.functions.push(...rscFunctions)
373+
374+
const middlewareManifest = await loadMiddlewareManifest(netlifyConfig)
375+
if (!middlewareManifest) {
376+
console.error("Couldn't find the middleware manifest")
377+
return
378+
}
379+
380+
let usesEdge = false
381+
382+
for (const middleware of middlewareManifest.sortedMiddleware) {
383+
usesEdge = true
384+
const edgeFunctionDefinition = middlewareManifest.middleware[middleware]
385+
const functionName = sanitizeName(edgeFunctionDefinition.name)
386+
const matchers = generateEdgeFunctionMiddlewareMatchers({
387+
edgeFunctionDefinition,
388+
edgeFunctionRoot,
389+
nextConfig,
390+
})
391+
await writeEdgeFunction({
392+
edgeFunctionDefinition,
393+
edgeFunctionRoot,
394+
netlifyConfig,
395+
functionName,
396+
matchers,
397+
middleware: true,
398+
})
399+
400+
manifest.functions.push(
401+
...matchers.map((matcher) => middlewareMatcherToEdgeFunctionDefinition(matcher, functionName)),
402+
)
403+
}
404+
// Functions (i.e. not middleware, but edge SSR and API routes)
405+
if (typeof middlewareManifest.functions === 'object') {
406+
// When using the app dir, we also need to check if the EF matches a page
407+
const appPathRoutesManifest = await loadAppPathRoutesManifest(netlifyConfig)
408+
409+
// A map of all route pages to their page regex. This is used for pages dir and appDir.
410+
const pageRegexMap = new Map(
411+
[...(routesManifest.dynamicRoutes || []), ...(routesManifest.staticRoutes || [])].map((route) => [
412+
route.page,
413+
route.regex,
414+
]),
415+
)
416+
// Create a map of pages-dir routes to their data route regex (appDir uses the same route as the HTML)
417+
const dataRoutesMap = new Map(
418+
[...(routesManifest.dataRoutes || [])].map((route) => [route.page, route.dataRouteRegex]),
419+
)
420+
421+
for (const edgeFunctionDefinition of Object.values(middlewareManifest.functions)) {
422+
usesEdge = true
423+
const functionName = sanitizeName(edgeFunctionDefinition.name)
424+
await writeEdgeFunction({
425+
edgeFunctionDefinition,
426+
edgeFunctionRoot,
427+
netlifyConfig,
428+
functionName,
429+
})
430+
const pattern = getEdgeFunctionPatternForPage({
431+
edgeFunctionDefinition,
432+
pageRegexMap,
433+
appPathRoutesManifest,
434+
})
435+
manifest.functions.push({
436+
function: functionName,
437+
name: edgeFunctionDefinition.name,
438+
pattern,
439+
// cache: "manual" is currently experimental, so we restrict it to sites that use experimental appDir
440+
cache: usesAppDir ? 'manual' : undefined,
441+
})
442+
// pages-dir page routes also have a data route. If there's a match, add an entry mapping that to the function too
443+
const dataRoute = dataRoutesMap.get(edgeFunctionDefinition.page)
444+
if (dataRoute) {
445+
manifest.functions.push({
446+
function: functionName,
447+
name: edgeFunctionDefinition.name,
448+
pattern: dataRoute,
449+
cache: usesAppDir ? 'manual' : undefined,
450+
})
451+
}
452+
}
453+
}
454+
361455
if (
456+
destr(process.env.NEXT_FORCE_EDGE_IMAGES) &&
362457
!destr(process.env.NEXT_DISABLE_EDGE_IMAGES) &&
363-
!destr(process.env.NEXT_DISABLE_NETLIFY_EDGE) &&
364458
!destr(process.env.DISABLE_IPX)
365459
) {
460+
usesEdge = true
366461
console.log(
367462
'Using Netlify Edge Functions for image format detection. Set env var "NEXT_DISABLE_EDGE_IMAGES=true" to disable.',
368463
)
@@ -378,101 +473,18 @@ export const writeEdgeFunctions = async ({
378473
name: 'next/image handler',
379474
path: '/_next/image*',
380475
})
476+
} else {
477+
console.log(
478+
'You are not using Netlify Edge Functions for image format detection. Set env var "NEXT_FORCE_EDGE_IMAGES=true" to enable.',
479+
)
381480
}
382-
if (!destr(process.env.NEXT_DISABLE_NETLIFY_EDGE)) {
383-
const rscFunctions = await writeRscDataEdgeFunction({
384-
prerenderManifest: await loadPrerenderManifest(netlifyConfig),
385-
appPathRoutesManifest: await loadAppPathRoutesManifest(netlifyConfig),
386-
})
387-
388-
manifest.functions.push(...rscFunctions)
389-
390-
const middlewareManifest = await loadMiddlewareManifest(netlifyConfig)
391-
if (!middlewareManifest) {
392-
console.error("Couldn't find the middleware manifest")
393-
return
394-
}
395-
396-
let usesEdge = false
397481

398-
for (const middleware of middlewareManifest.sortedMiddleware) {
399-
usesEdge = true
400-
const edgeFunctionDefinition = middlewareManifest.middleware[middleware]
401-
const functionName = sanitizeName(edgeFunctionDefinition.name)
402-
const matchers = generateEdgeFunctionMiddlewareMatchers({
403-
edgeFunctionDefinition,
404-
edgeFunctionRoot,
405-
nextConfig,
406-
})
407-
await writeEdgeFunction({
408-
edgeFunctionDefinition,
409-
edgeFunctionRoot,
410-
netlifyConfig,
411-
functionName,
412-
matchers,
413-
middleware: true,
414-
})
415-
416-
manifest.functions.push(
417-
...matchers.map((matcher) => middlewareMatcherToEdgeFunctionDefinition(matcher, functionName)),
418-
)
419-
}
420-
// Functions (i.e. not middleware, but edge SSR and API routes)
421-
if (typeof middlewareManifest.functions === 'object') {
422-
// When using the app dir, we also need to check if the EF matches a page
423-
const appPathRoutesManifest = await loadAppPathRoutesManifest(netlifyConfig)
424-
425-
// A map of all route pages to their page regex. This is used for pages dir and appDir.
426-
const pageRegexMap = new Map(
427-
[...(routesManifest.dynamicRoutes || []), ...(routesManifest.staticRoutes || [])].map((route) => [
428-
route.page,
429-
route.regex,
430-
]),
431-
)
432-
// Create a map of pages-dir routes to their data route regex (appDir uses the same route as the HTML)
433-
const dataRoutesMap = new Map(
434-
[...(routesManifest.dataRoutes || [])].map((route) => [route.page, route.dataRouteRegex]),
435-
)
436-
437-
for (const edgeFunctionDefinition of Object.values(middlewareManifest.functions)) {
438-
usesEdge = true
439-
const functionName = sanitizeName(edgeFunctionDefinition.name)
440-
await writeEdgeFunction({
441-
edgeFunctionDefinition,
442-
edgeFunctionRoot,
443-
netlifyConfig,
444-
functionName,
445-
})
446-
const pattern = getEdgeFunctionPatternForPage({
447-
edgeFunctionDefinition,
448-
pageRegexMap,
449-
appPathRoutesManifest,
450-
})
451-
manifest.functions.push({
452-
function: functionName,
453-
name: edgeFunctionDefinition.name,
454-
pattern,
455-
// cache: "manual" is currently experimental, so we restrict it to sites that use experimental appDir
456-
cache: usesAppDir ? 'manual' : undefined,
457-
})
458-
// pages-dir page routes also have a data route. If there's a match, add an entry mapping that to the function too
459-
const dataRoute = dataRoutesMap.get(edgeFunctionDefinition.page)
460-
if (dataRoute) {
461-
manifest.functions.push({
462-
function: functionName,
463-
name: edgeFunctionDefinition.name,
464-
pattern: dataRoute,
465-
cache: usesAppDir ? 'manual' : undefined,
466-
})
467-
}
468-
}
469-
}
470-
if (usesEdge) {
471-
console.log(outdent`
472-
✨ Deploying middleware and functions to ${greenBright`Netlify Edge Functions`} ✨
473-
This feature is in beta. Please share your feedback here: https://ntl.fyi/next-netlify-edge
474-
`)
475-
}
482+
if (usesEdge) {
483+
console.log(outdent`
484+
✨ Deploying middleware and functions to ${greenBright`Netlify Edge Functions`}
485+
This feature is in beta. Please share your feedback here: https://ntl.fyi/next-netlify-edge
486+
`)
476487
}
488+
477489
await writeJson(join(edgeFunctionRoot, 'manifest.json'), manifest)
478490
}

test/index.spec.js

+14
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,20 @@ describe('onBuild()', () => {
690690
expect(existsSync(path.join('.netlify', 'functions-internal', '_ipx', '_ipx.js'))).toBeTruthy()
691691
})
692692

693+
// Enabled while edge images are off by default
694+
test('does not generate an ipx edge function by default', async () => {
695+
await moveNextDist()
696+
await nextRuntime.onBuild(defaultArgs)
697+
expect(existsSync(path.join('.netlify', 'edge-functions', 'ipx', 'index.ts'))).toBeFalsy()
698+
})
699+
700+
test('generates an ipx edge function if force is set', async () => {
701+
process.env.NEXT_FORCE_EDGE_IMAGES = '1'
702+
await moveNextDist()
703+
await nextRuntime.onBuild(defaultArgs)
704+
expect(existsSync(path.join('.netlify', 'edge-functions', 'ipx', 'index.ts'))).toBeTruthy()
705+
})
706+
693707
test('does not generate an ipx function when DISABLE_IPX is set', async () => {
694708
process.env.DISABLE_IPX = '1'
695709
await moveNextDist()

0 commit comments

Comments
 (0)