Skip to content

feat: make edge image opt-in instead of opt-out #1935

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

Merged
merged 15 commits into from
Mar 1, 2023
52 changes: 33 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,23 @@ commands separately will not work, because the Next.js Runtime will not generate

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

### Enabling Edge Images

It is possible to run image content negotiation on the edge. This allows images to be processed on the first request,
and then in future loads, it will be served from cache on the edge.

In order to deliver the correct format to a visitor's browser, this uses a Netlify Edge Function. In some cases your
site may not support Edge Functions, in which case it will instead fall back to delivering the original file format. You
may also manually disable the Edge Function by setting the environment variable `NEXT_DISABLE_EDGE_IMAGES` to `true`.
site may not support Edge Functions, in which case it will instead fall back to delivering the original file format.

To turn on Edge image handling for Next/Image, set the environment variable `NEXT_FORCE_EDGE_IMAGES` to `true`

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

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

## Disabling included image loader

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`.
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`.

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

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

## Next.js Middleware on Netlify

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

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

### Limitations

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`.
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`.

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

Expand Down Expand Up @@ -112,8 +123,9 @@ following ways:

### From the UI (Recommended):

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
is recommended because you will benefit from auto-upgrades to important fixes and feature updates.
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 is recommended because you will benefit from auto-upgrades to important fixes and
feature updates.

### From `npm`:

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

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

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

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

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

```ini
public-hoist-pattern[]=*
Expand Down
200 changes: 106 additions & 94 deletions packages/runtime/src/helpers/edge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,11 +358,106 @@ export const writeEdgeFunctions = async ({
await writeJSON(join(edgeFunctionRoot, 'edge-shared', 'nextConfig.json'), nextConfig)
await copy(join(publish, 'prerender-manifest.json'), join(edgeFunctionRoot, 'edge-shared', 'prerender-manifest.json'))

// early return if edge is disabled
if (destr(process.env.NEXT_DISABLE_NETLIFY_EDGE)) {
console.log('Environment variable NEXT_DISABLE_NETLIFY_EDGE has been set, skipping Netlify Edge Function creation.')
return
}

const rscFunctions = await writeRscDataEdgeFunction({
prerenderManifest: await loadPrerenderManifest(netlifyConfig),
appPathRoutesManifest: await loadAppPathRoutesManifest(netlifyConfig),
})

manifest.functions.push(...rscFunctions)

const middlewareManifest = await loadMiddlewareManifest(netlifyConfig)
if (!middlewareManifest) {
console.error("Couldn't find the middleware manifest")
return
}

let usesEdge = false

for (const middleware of middlewareManifest.sortedMiddleware) {
usesEdge = true
const edgeFunctionDefinition = middlewareManifest.middleware[middleware]
const functionName = sanitizeName(edgeFunctionDefinition.name)
const matchers = generateEdgeFunctionMiddlewareMatchers({
edgeFunctionDefinition,
edgeFunctionRoot,
nextConfig,
})
await writeEdgeFunction({
edgeFunctionDefinition,
edgeFunctionRoot,
netlifyConfig,
functionName,
matchers,
middleware: true,
})

manifest.functions.push(
...matchers.map((matcher) => middlewareMatcherToEdgeFunctionDefinition(matcher, functionName)),
)
}
// Functions (i.e. not middleware, but edge SSR and API routes)
if (typeof middlewareManifest.functions === 'object') {
// When using the app dir, we also need to check if the EF matches a page
const appPathRoutesManifest = await loadAppPathRoutesManifest(netlifyConfig)

// A map of all route pages to their page regex. This is used for pages dir and appDir.
const pageRegexMap = new Map(
[...(routesManifest.dynamicRoutes || []), ...(routesManifest.staticRoutes || [])].map((route) => [
route.page,
route.regex,
]),
)
// Create a map of pages-dir routes to their data route regex (appDir uses the same route as the HTML)
const dataRoutesMap = new Map(
[...(routesManifest.dataRoutes || [])].map((route) => [route.page, route.dataRouteRegex]),
)

for (const edgeFunctionDefinition of Object.values(middlewareManifest.functions)) {
usesEdge = true
const functionName = sanitizeName(edgeFunctionDefinition.name)
await writeEdgeFunction({
edgeFunctionDefinition,
edgeFunctionRoot,
netlifyConfig,
functionName,
})
const pattern = getEdgeFunctionPatternForPage({
edgeFunctionDefinition,
pageRegexMap,
appPathRoutesManifest,
})
manifest.functions.push({
function: functionName,
name: edgeFunctionDefinition.name,
pattern,
// cache: "manual" is currently experimental, so we restrict it to sites that use experimental appDir
cache: usesAppDir ? 'manual' : undefined,
})
// pages-dir page routes also have a data route. If there's a match, add an entry mapping that to the function too
const dataRoute = dataRoutesMap.get(edgeFunctionDefinition.page)
if (dataRoute) {
manifest.functions.push({
function: functionName,
name: edgeFunctionDefinition.name,
pattern: dataRoute,
cache: usesAppDir ? 'manual' : undefined,
})
}
}
}

if (
destr(process.env.NEXT_FORCE_EDGE_IMAGES) &&
!destr(process.env.NEXT_DISABLE_EDGE_IMAGES) &&
!destr(process.env.NEXT_DISABLE_NETLIFY_EDGE) &&
!destr(process.env.DISABLE_IPX)
) {
usesEdge = true
console.log(
'Using Netlify Edge Functions for image format detection. Set env var "NEXT_DISABLE_EDGE_IMAGES=true" to disable.',
)
Expand All @@ -378,101 +473,18 @@ export const writeEdgeFunctions = async ({
name: 'next/image handler',
path: '/_next/image*',
})
} else {
console.log(
'You are not using Netlify Edge Functions for image format detection. Set env var "NEXT_FORCE_EDGE_IMAGES=true" to enable.',
)
}
if (!destr(process.env.NEXT_DISABLE_NETLIFY_EDGE)) {
const rscFunctions = await writeRscDataEdgeFunction({
prerenderManifest: await loadPrerenderManifest(netlifyConfig),
appPathRoutesManifest: await loadAppPathRoutesManifest(netlifyConfig),
})

manifest.functions.push(...rscFunctions)

const middlewareManifest = await loadMiddlewareManifest(netlifyConfig)
if (!middlewareManifest) {
console.error("Couldn't find the middleware manifest")
return
}

let usesEdge = false

for (const middleware of middlewareManifest.sortedMiddleware) {
usesEdge = true
const edgeFunctionDefinition = middlewareManifest.middleware[middleware]
const functionName = sanitizeName(edgeFunctionDefinition.name)
const matchers = generateEdgeFunctionMiddlewareMatchers({
edgeFunctionDefinition,
edgeFunctionRoot,
nextConfig,
})
await writeEdgeFunction({
edgeFunctionDefinition,
edgeFunctionRoot,
netlifyConfig,
functionName,
matchers,
middleware: true,
})

manifest.functions.push(
...matchers.map((matcher) => middlewareMatcherToEdgeFunctionDefinition(matcher, functionName)),
)
}
// Functions (i.e. not middleware, but edge SSR and API routes)
if (typeof middlewareManifest.functions === 'object') {
// When using the app dir, we also need to check if the EF matches a page
const appPathRoutesManifest = await loadAppPathRoutesManifest(netlifyConfig)

// A map of all route pages to their page regex. This is used for pages dir and appDir.
const pageRegexMap = new Map(
[...(routesManifest.dynamicRoutes || []), ...(routesManifest.staticRoutes || [])].map((route) => [
route.page,
route.regex,
]),
)
// Create a map of pages-dir routes to their data route regex (appDir uses the same route as the HTML)
const dataRoutesMap = new Map(
[...(routesManifest.dataRoutes || [])].map((route) => [route.page, route.dataRouteRegex]),
)

for (const edgeFunctionDefinition of Object.values(middlewareManifest.functions)) {
usesEdge = true
const functionName = sanitizeName(edgeFunctionDefinition.name)
await writeEdgeFunction({
edgeFunctionDefinition,
edgeFunctionRoot,
netlifyConfig,
functionName,
})
const pattern = getEdgeFunctionPatternForPage({
edgeFunctionDefinition,
pageRegexMap,
appPathRoutesManifest,
})
manifest.functions.push({
function: functionName,
name: edgeFunctionDefinition.name,
pattern,
// cache: "manual" is currently experimental, so we restrict it to sites that use experimental appDir
cache: usesAppDir ? 'manual' : undefined,
})
// pages-dir page routes also have a data route. If there's a match, add an entry mapping that to the function too
const dataRoute = dataRoutesMap.get(edgeFunctionDefinition.page)
if (dataRoute) {
manifest.functions.push({
function: functionName,
name: edgeFunctionDefinition.name,
pattern: dataRoute,
cache: usesAppDir ? 'manual' : undefined,
})
}
}
}
if (usesEdge) {
console.log(outdent`
✨ Deploying middleware and functions to ${greenBright`Netlify Edge Functions`} ✨
This feature is in beta. Please share your feedback here: https://ntl.fyi/next-netlify-edge
`)
}
if (usesEdge) {
console.log(outdent`
✨ Deploying middleware and functions to ${greenBright`Netlify Edge Functions`} ✨
This feature is in beta. Please share your feedback here: https://ntl.fyi/next-netlify-edge
`)
}

await writeJson(join(edgeFunctionRoot, 'manifest.json'), manifest)
}
14 changes: 14 additions & 0 deletions test/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,20 @@ describe('onBuild()', () => {
expect(existsSync(path.join('.netlify', 'functions-internal', '_ipx', '_ipx.js'))).toBeTruthy()
})

// Enabled while edge images are off by default
test('does not generate an ipx edge function by default', async () => {
await moveNextDist()
await nextRuntime.onBuild(defaultArgs)
expect(existsSync(path.join('.netlify', 'edge-functions', 'ipx', 'index.ts'))).toBeFalsy()
})

test('generates an ipx edge function if force is set', async () => {
process.env.NEXT_FORCE_EDGE_IMAGES = '1'
await moveNextDist()
await nextRuntime.onBuild(defaultArgs)
expect(existsSync(path.join('.netlify', 'edge-functions', 'ipx', 'index.ts'))).toBeTruthy()
})

test('does not generate an ipx function when DISABLE_IPX is set', async () => {
process.env.DISABLE_IPX = '1'
await moveNextDist()
Expand Down