Remove experimental image optimization feature (#34349)

This PR removes the experimental `optimizeImages` flag. This feature was designed to automatically add preload tags for images, but I was never able to get it to do a very good job of selecting the images that actually need preloading.

This feature never graduated from experimental and in fact we never even publicized it as an experimental feature for people to try.

Additionally, even if someone was using this feature, it wouldn't have a functional effect, only a performance effect (removal of some preloads).

For those reasons, I believe it is safe to remove this functionality and that it is not a breaking change.
This commit is contained in:
Alex Castle 2022-02-14 17:36:51 -08:00 committed by GitHub
parent c20e8297bf
commit f516304649
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 2 additions and 342 deletions

View file

@ -1346,9 +1346,6 @@ export default async function getBaseWebpackConfig(
'process.env.__NEXT_OPTIMIZE_FONTS': JSON.stringify( 'process.env.__NEXT_OPTIMIZE_FONTS': JSON.stringify(
config.optimizeFonts && !dev config.optimizeFonts && !dev
), ),
'process.env.__NEXT_OPTIMIZE_IMAGES': JSON.stringify(
config.experimental.optimizeImages
),
'process.env.__NEXT_OPTIMIZE_CSS': JSON.stringify( 'process.env.__NEXT_OPTIMIZE_CSS': JSON.stringify(
config.experimental.optimizeCss && !dev config.experimental.optimizeCss && !dev
), ),
@ -1611,7 +1608,6 @@ export default async function getBaseWebpackConfig(
reactStrictMode: config.reactStrictMode, reactStrictMode: config.reactStrictMode,
reactMode: config.experimental.reactMode, reactMode: config.experimental.reactMode,
optimizeFonts: config.optimizeFonts, optimizeFonts: config.optimizeFonts,
optimizeImages: config.experimental.optimizeImages,
optimizeCss: config.experimental.optimizeCss, optimizeCss: config.experimental.optimizeCss,
scrollRestoration: config.experimental.scrollRestoration, scrollRestoration: config.experimental.scrollRestoration,
basePath: config.basePath, basePath: config.basePath,

View file

@ -191,7 +191,6 @@ export function getPageHandler(ctx: ServerlessHandlerCtx) {
locale: detectedLocale, locale: detectedLocale,
defaultLocale, defaultLocale,
domainLocales: i18n?.domains, domainLocales: i18n?.domains,
optimizeImages: process.env.__NEXT_OPTIMIZE_IMAGES,
optimizeCss: process.env.__NEXT_OPTIMIZE_CSS, optimizeCss: process.env.__NEXT_OPTIMIZE_CSS,
crossOrigin: process.env.__NEXT_CROSS_ORIGIN, crossOrigin: process.env.__NEXT_CROSS_ORIGIN,
}, },

View file

@ -385,7 +385,6 @@ export default async function exportApp(
crossOrigin: nextConfig.crossOrigin, crossOrigin: nextConfig.crossOrigin,
optimizeCss: nextConfig.experimental.optimizeCss, optimizeCss: nextConfig.experimental.optimizeCss,
optimizeFonts: nextConfig.optimizeFonts, optimizeFonts: nextConfig.optimizeFonts,
optimizeImages: nextConfig.experimental.optimizeImages,
reactRoot: nextConfig.experimental.reactRoot || false, reactRoot: nextConfig.experimental.reactRoot || false,
} }
@ -583,7 +582,6 @@ export default async function exportApp(
buildExport: options.buildExport, buildExport: options.buildExport,
serverless: isTargetLikeServerless(nextConfig.target), serverless: isTargetLikeServerless(nextConfig.target),
optimizeFonts: nextConfig.optimizeFonts, optimizeFonts: nextConfig.optimizeFonts,
optimizeImages: nextConfig.experimental.optimizeImages,
optimizeCss: nextConfig.experimental.optimizeCss, optimizeCss: nextConfig.experimental.optimizeCss,
disableOptimizedLoading: disableOptimizedLoading:
nextConfig.experimental.disableOptimizedLoading, nextConfig.experimental.disableOptimizedLoading,

View file

@ -55,7 +55,6 @@ interface ExportPageInput {
subFolders?: boolean subFolders?: boolean
serverless: boolean serverless: boolean
optimizeFonts: boolean optimizeFonts: boolean
optimizeImages?: boolean
optimizeCss: any optimizeCss: any
disableOptimizedLoading: any disableOptimizedLoading: any
parentSpanId: any parentSpanId: any
@ -77,7 +76,6 @@ interface RenderOpts {
ampValidatorPath?: string ampValidatorPath?: string
ampSkipValidation?: boolean ampSkipValidation?: boolean
optimizeFonts?: boolean optimizeFonts?: boolean
optimizeImages?: boolean
disableOptimizedLoading?: boolean disableOptimizedLoading?: boolean
optimizeCss?: any optimizeCss?: any
fontManifest?: FontManifest fontManifest?: FontManifest
@ -105,7 +103,6 @@ export default async function exportPage({
subFolders, subFolders,
serverless, serverless,
optimizeFonts, optimizeFonts,
optimizeImages,
optimizeCss, optimizeCss,
disableOptimizedLoading, disableOptimizedLoading,
httpAgentOptions, httpAgentOptions,
@ -304,8 +301,6 @@ export default async function exportPage({
/// @ts-ignore /// @ts-ignore
optimizeFonts, optimizeFonts,
/// @ts-ignore /// @ts-ignore
optimizeImages,
/// @ts-ignore
optimizeCss, optimizeCss,
disableOptimizedLoading, disableOptimizedLoading,
distDir, distDir,
@ -367,9 +362,6 @@ export default async function exportPage({
if (optimizeFonts) { if (optimizeFonts) {
process.env.__NEXT_OPTIMIZE_FONTS = JSON.stringify(true) process.env.__NEXT_OPTIMIZE_FONTS = JSON.stringify(true)
} }
if (optimizeImages) {
process.env.__NEXT_OPTIMIZE_IMAGES = JSON.stringify(true)
}
if (optimizeCss) { if (optimizeCss) {
process.env.__NEXT_OPTIMIZE_CSS = JSON.stringify(true) process.env.__NEXT_OPTIMIZE_CSS = JSON.stringify(true)
} }
@ -379,7 +371,6 @@ export default async function exportPage({
ampPath: renderAmpPath, ampPath: renderAmpPath,
params, params,
optimizeFonts, optimizeFonts,
optimizeImages,
optimizeCss, optimizeCss,
disableOptimizedLoading, disableOptimizedLoading,
fontManifest: optimizeFonts fontManifest: optimizeFonts

View file

@ -494,7 +494,6 @@ export class Head extends Component<
useMaybeDeferContent, useMaybeDeferContent,
optimizeCss, optimizeCss,
optimizeFonts, optimizeFonts,
optimizeImages,
runtime, runtime,
} = this.context } = this.context
@ -737,7 +736,6 @@ export class Head extends Component<
)} )}
{!optimizeCss && this.getCssLinks(files)} {!optimizeCss && this.getCssLinks(files)}
{!optimizeCss && <noscript data-n-css={this.props.nonce ?? ''} />} {!optimizeCss && <noscript data-n-css={this.props.nonce ?? ''} />}
{optimizeImages && <meta name="next-image-preload" />}
{!isDeferred && getDynamicScriptPreloads()} {!isDeferred && getDynamicScriptPreloads()}

View file

@ -152,7 +152,6 @@ export default abstract class Server {
optimizeFonts: boolean optimizeFonts: boolean
images: ImageConfigComplete images: ImageConfigComplete
fontManifest?: FontManifest fontManifest?: FontManifest
optimizeImages: boolean
disableOptimizedLoading?: boolean disableOptimizedLoading?: boolean
optimizeCss: any optimizeCss: any
locale?: string locale?: string
@ -314,7 +313,6 @@ export default abstract class Server {
this.nextConfig.optimizeFonts && !dev this.nextConfig.optimizeFonts && !dev
? this.getFontManifest() ? this.getFontManifest()
: undefined, : undefined,
optimizeImages: !!this.nextConfig.experimental.optimizeImages,
optimizeCss: this.nextConfig.experimental.optimizeCss, optimizeCss: this.nextConfig.experimental.optimizeCss,
disableOptimizedLoading: this.nextConfig.experimental.runtime disableOptimizedLoading: this.nextConfig.experimental.runtime
? true ? true

View file

@ -85,7 +85,6 @@ export interface ExperimentalConfig {
reactMode?: 'legacy' | 'concurrent' | 'blocking' reactMode?: 'legacy' | 'concurrent' | 'blocking'
workerThreads?: boolean workerThreads?: boolean
pageEnv?: boolean pageEnv?: boolean
optimizeImages?: boolean
optimizeCss?: boolean optimizeCss?: boolean
scrollRestoration?: boolean scrollRestoration?: boolean
externalDir?: boolean externalDir?: boolean
@ -454,7 +453,6 @@ export const defaultConfig: NextConfig = {
isrFlushToDisk: true, isrFlushToDisk: true,
workerThreads: false, workerThreads: false,
pageEnv: false, pageEnv: false,
optimizeImages: false,
optimizeCss: false, optimizeCss: false,
scrollRestoration: false, scrollRestoration: false,
externalDir: false, externalDir: false,

View file

@ -100,15 +100,11 @@ export default class NextNodeServer extends BaseServer {
/** /**
* This sets environment variable to be used at the time of SSR by head.tsx. * This sets environment variable to be used at the time of SSR by head.tsx.
* Using this from process.env allows targeting both serverless and SSR by calling * Using this from process.env allows targeting both serverless and SSR by calling
* `process.env.__NEXT_OPTIMIZE_IMAGES`. * `process.env.__NEXT_OPTIMIZE_CSS`.
* TODO(atcastle@): Remove this when experimental.optimizeImages are being cleaned up.
*/ */
if (this.renderOpts.optimizeFonts) { if (this.renderOpts.optimizeFonts) {
process.env.__NEXT_OPTIMIZE_FONTS = JSON.stringify(true) process.env.__NEXT_OPTIMIZE_FONTS = JSON.stringify(true)
} }
if (this.renderOpts.optimizeImages) {
process.env.__NEXT_OPTIMIZE_IMAGES = JSON.stringify(true)
}
if (this.renderOpts.optimizeCss) { if (this.renderOpts.optimizeCss) {
process.env.__NEXT_OPTIMIZE_CSS = JSON.stringify(true) process.env.__NEXT_OPTIMIZE_CSS = JSON.stringify(true)
} }

View file

@ -215,7 +215,6 @@ export type RenderOptsPartial = {
unstable_JsPreload?: false unstable_JsPreload?: false
optimizeFonts: boolean optimizeFonts: boolean
fontManifest?: FontManifest fontManifest?: FontManifest
optimizeImages: boolean
optimizeCss: any optimizeCss: any
devOnlyCacheBusterQueryString?: string devOnlyCacheBusterQueryString?: string
resolvedUrl?: string resolvedUrl?: string
@ -1406,7 +1405,6 @@ export async function renderToHTML(
crossOrigin: renderOpts.crossOrigin, crossOrigin: renderOpts.crossOrigin,
optimizeCss: renderOpts.optimizeCss, optimizeCss: renderOpts.optimizeCss,
optimizeFonts: renderOpts.optimizeFonts, optimizeFonts: renderOpts.optimizeFonts,
optimizeImages: renderOpts.optimizeImages,
runtime, runtime,
} }
@ -1485,16 +1483,13 @@ export async function renderToHTML(
return html return html
} }
: null, : null,
!process.browser && !process.browser && process.env.__NEXT_OPTIMIZE_FONTS
(process.env.__NEXT_OPTIMIZE_FONTS ||
process.env.__NEXT_OPTIMIZE_IMAGES)
? async (html: string) => { ? async (html: string) => {
return await postProcess( return await postProcess(
html, html,
{ getFontDefinition }, { getFontDefinition },
{ {
optimizeFonts: renderOpts.optimizeFonts, optimizeFonts: renderOpts.optimizeFonts,
optimizeImages: renderOpts.optimizeImages,
} }
) )
} }

View file

@ -1,14 +1,10 @@
import { escapeStringRegexp } from './escape-regexp'
import { parse, HTMLElement } from 'next/dist/compiled/node-html-parser' import { parse, HTMLElement } from 'next/dist/compiled/node-html-parser'
import { OPTIMIZED_FONT_PROVIDERS } from './constants' import { OPTIMIZED_FONT_PROVIDERS } from './constants'
// const MIDDLEWARE_TIME_BUDGET = parseInt(process.env.__POST_PROCESS_MIDDLEWARE_TIME_BUDGET || '', 10) || 10 // const MIDDLEWARE_TIME_BUDGET = parseInt(process.env.__POST_PROCESS_MIDDLEWARE_TIME_BUDGET || '', 10) || 10
const MAXIMUM_IMAGE_PRELOADS = 2
const IMAGE_PRELOAD_SIZE_THRESHOLD = 2500
type postProcessOptions = { type postProcessOptions = {
optimizeFonts: boolean optimizeFonts: boolean
optimizeImages: boolean
} }
type renderOptions = { type renderOptions = {
@ -169,103 +165,6 @@ class FontOptimizerMiddleware implements PostProcessMiddleware {
} }
} }
class ImageOptimizerMiddleware implements PostProcessMiddleware {
inspect(originalDom: HTMLElement) {
const imgPreloads = []
const imgElements = originalDom.querySelectorAll('img')
let eligibleImages: Array<HTMLElement> = []
for (let i = 0; i < imgElements.length; i++) {
if (isImgEligible(imgElements[i])) {
eligibleImages.push(imgElements[i])
}
if (eligibleImages.length >= MAXIMUM_IMAGE_PRELOADS) {
break
}
}
for (const imgEl of eligibleImages) {
const src = imgEl.getAttribute('src')
if (src) {
imgPreloads.push(src)
}
}
return imgPreloads
}
mutate = async (markup: string, imgPreloads: string[]) => {
let result = markup
let imagePreloadTags = imgPreloads
.filter((imgHref) => !preloadTagAlreadyExists(markup, imgHref))
.reduce(
(acc, imgHref) =>
acc + `<link rel="preload" href="${imgHref}" as="image"/>`,
''
)
return result.replace('<meta name="next-image-preload"/>', imagePreloadTags)
}
}
function isImgEligible(imgElement: HTMLElement): boolean {
let imgSrc = imgElement.getAttribute('src')
return (
!!imgSrc &&
sourceIsSupportedType(imgSrc) &&
imageIsNotTooSmall(imgElement) &&
imageIsNotHidden(imgElement)
)
}
function preloadTagAlreadyExists(html: string, href: string) {
const escapedHref = escapeStringRegexp(href)
const regex = new RegExp(`<link[^>]*href[^>]*${escapedHref}`)
return html.match(regex)
}
function imageIsNotTooSmall(imgElement: HTMLElement): boolean {
// Skip images without both height and width--we don't know enough to say if
// they are too small
if (
!(imgElement.hasAttribute('height') && imgElement.hasAttribute('width'))
) {
return true
}
try {
const heightAttr = imgElement.getAttribute('height')
const widthAttr = imgElement.getAttribute('width')
if (!heightAttr || !widthAttr) {
return true
}
if (
parseInt(heightAttr) * parseInt(widthAttr) <=
IMAGE_PRELOAD_SIZE_THRESHOLD
) {
return false
}
} catch (err) {
return true
}
return true
}
// Traverse up the dom from each image to see if it or any of it's
// ancestors have the hidden attribute.
function imageIsNotHidden(imgElement: HTMLElement): boolean {
let activeElement = imgElement
while (activeElement.parentNode) {
if (activeElement.hasAttribute('hidden')) {
return false
}
activeElement = activeElement.parentNode as HTMLElement
}
return true
}
// Currently only filters out svg images--could be made more specific in the future.
function sourceIsSupportedType(imgSrc: string): boolean {
return !imgSrc.includes('.svg')
}
// Initialization // Initialization
registerPostProcessor( registerPostProcessor(
'Inline-Fonts', 'Inline-Fonts',
@ -275,11 +174,4 @@ registerPostProcessor(
(options) => options.optimizeFonts || process.env.__NEXT_OPTIMIZE_FONTS (options) => options.optimizeFonts || process.env.__NEXT_OPTIMIZE_FONTS
) )
registerPostProcessor(
'Preload Images',
new ImageOptimizerMiddleware(),
// @ts-ignore
(options) => options.optimizeImages || process.env.__NEXT_OPTIMIZE_IMAGES
)
export default processHTML export default processHTML

View file

@ -224,7 +224,6 @@ export type HtmlProps = {
crossOrigin?: string crossOrigin?: string
optimizeCss?: boolean optimizeCss?: boolean
optimizeFonts?: boolean optimizeFonts?: boolean
optimizeImages?: boolean
runtime?: 'edge' | 'nodejs' runtime?: 'edge' | 'nodejs'
} }

View file

@ -1,4 +0,0 @@
module.exports = {
target: 'serverless',
experimental: { optimizeImages: true },
}

View file

@ -1,25 +0,0 @@
import React from 'react'
const Page = () => {
return (
<div>
<link rel="preload" href="already-preloaded.jpg" />
<img src="already-preloaded.jpg" />
<img src="tiny-image.jpg" width="20" height="20" />
<img src="vector-image.svg" />
<img src="hidden-image-1.jpg" hidden />
<div hidden>
<img src="hidden-image-2.jpg" />
</div>
<img src="main-image-1.jpg" />
<div>
<img src="main-image-2.jpg" />
</div>
<img src="main-image-3.jpg" />
<img src="main-image-4.jpg" />
<img src="main-image-5.jpg" />
</div>
)
}
export default Page

View file

@ -1,30 +0,0 @@
function Home({ stars }) {
return (
<div className="container">
<main>
<div>
<link rel="preload" href="already-preloaded.jpg" />
<img src="already-preloaded.jpg" />
<img src="tiny-image.jpg" width="20" height="20" />
<img src="vector-image.svg" />
<img src="hidden-image-1.jpg" hidden />
<div hidden>
<img src="hidden-image-2.jpg" />
</div>
<img src="main-image-1.jpg" />
<img src="main-image-2.jpg" />
<img src="main-image-3.jpg" />
<img src="main-image-4.jpg" />
<img src="main-image-5.jpg" />
</div>
<div>Next stars: {stars}</div>
</main>
</div>
)
}
Home.getInitialProps = async () => {
return { stars: Math.random() * 1000 }
}
export default Home

View file

@ -1,18 +0,0 @@
import React from 'react'
import Head from 'next/head'
const Page = () => {
return (
<>
<Head>
<link
href="https://fonts.googleapis.com/css2?family=Modak"
rel="stylesheet"
/>
</Head>
<div>Hi!</div>
</>
)
}
export default Page

View file

@ -1,12 +0,0 @@
import React from 'react'
const Page = () => {
return (
<div>
<img src="https://image.example.org/?lang[]=c++" />
<img src="/api/image?lang[]=c++" />
</div>
)
}
export default Page

View file

@ -1,111 +0,0 @@
/* eslint-env jest */
import { join } from 'path'
import {
killApp,
findPort,
nextStart,
nextBuild,
renderViaHTTP,
} from 'next-test-utils'
import fs from 'fs-extra'
const appDir = join(__dirname, '../')
const nextConfig = join(appDir, 'next.config.js')
let appPort
let app
function runTests() {
describe('On a static page', () => {
checkImagesOnPage('/')
})
describe('On an SSR page', () => {
checkImagesOnPage('/stars')
})
describe('On a static page with querystring ', () => {
it('should preload exactly eligible image', async () => {
const html = await renderViaHTTP(appPort, '/with-querystring')
expect(html).toContain(
'<link rel="preload" href="https://image.example.org/?lang[]=c++" as="image"/>'
)
expect(html).toContain(
'<link rel="preload" href="/api/image?lang[]=c++" as="image"/>'
)
})
})
}
function checkImagesOnPage(path) {
it('should not preload tiny images', async () => {
const html = await renderViaHTTP(appPort, path)
expect(html).not.toContain(
'<link rel="preload" href="tiny-image.jpg" as="image"/>'
)
})
it('should not add a preload if one already exists', async () => {
let html = await renderViaHTTP(appPort, path)
html = html.replace(
'<link rel="preload" href="already-preloaded.jpg" as="image"/>',
''
)
expect(html).not.toContain(
'<link rel="preload" href="already-preloaded.jpg" as="image"/>'
)
})
it('should not preload hidden images', async () => {
const html = await renderViaHTTP(appPort, path)
expect(html).not.toContain(
'<link rel="preload" href="hidden-image-1.jpg" as="image"/>'
)
expect(html).not.toContain(
'<link rel="preload" href="hidden-image-2.jpg" as="image"/>'
)
})
it('should not preload SVG images', async () => {
const html = await renderViaHTTP(appPort, path)
expect(html).not.toContain(
'<link rel="preload" href="vector-image.svg" as="image"/>'
)
})
it('should preload exactly two eligible images', async () => {
const html = await renderViaHTTP(appPort, path)
expect(html).toContain(
'<link rel="preload" href="main-image-1.jpg" as="image"/>'
)
expect(html).not.toContain(
'<link rel="preload" href="main-image-2.jpg" as="image"/>'
)
})
}
describe('Image optimization for SSR apps', () => {
beforeAll(async () => {
await fs.writeFile(
nextConfig,
`module.exports = { experimental: {optimizeImages: true} }`,
'utf8'
)
await nextBuild(appDir)
appPort = await findPort()
app = await nextStart(appDir, appPort)
})
afterAll(() => killApp(app))
runTests()
})
describe('Image optimization for serverless apps', () => {
beforeAll(async () => {
await fs.writeFile(
nextConfig,
`module.exports = { target: 'serverless', experimental: {optimizeImages: true} }`,
'utf8'
)
await nextBuild(appDir)
appPort = await findPort()
app = await nextStart(appDir, appPort)
})
afterAll(() => killApp(app))
runTests()
})