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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -85,7 +85,6 @@ export interface ExperimentalConfig {
reactMode?: 'legacy' | 'concurrent' | 'blocking'
workerThreads?: boolean
pageEnv?: boolean
optimizeImages?: boolean
optimizeCss?: boolean
scrollRestoration?: boolean
externalDir?: boolean
@ -454,7 +453,6 @@ export const defaultConfig: NextConfig = {
isrFlushToDisk: true,
workerThreads: false,
pageEnv: false,
optimizeImages: false,
optimizeCss: false,
scrollRestoration: 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.
* Using this from process.env allows targeting both serverless and SSR by calling
* `process.env.__NEXT_OPTIMIZE_IMAGES`.
* TODO(atcastle@): Remove this when experimental.optimizeImages are being cleaned up.
* `process.env.__NEXT_OPTIMIZE_CSS`.
*/
if (this.renderOpts.optimizeFonts) {
process.env.__NEXT_OPTIMIZE_FONTS = JSON.stringify(true)
}
if (this.renderOpts.optimizeImages) {
process.env.__NEXT_OPTIMIZE_IMAGES = JSON.stringify(true)
}
if (this.renderOpts.optimizeCss) {
process.env.__NEXT_OPTIMIZE_CSS = JSON.stringify(true)
}

View file

@ -215,7 +215,6 @@ export type RenderOptsPartial = {
unstable_JsPreload?: false
optimizeFonts: boolean
fontManifest?: FontManifest
optimizeImages: boolean
optimizeCss: any
devOnlyCacheBusterQueryString?: string
resolvedUrl?: string
@ -1406,7 +1405,6 @@ export async function renderToHTML(
crossOrigin: renderOpts.crossOrigin,
optimizeCss: renderOpts.optimizeCss,
optimizeFonts: renderOpts.optimizeFonts,
optimizeImages: renderOpts.optimizeImages,
runtime,
}
@ -1485,16 +1483,13 @@ export async function renderToHTML(
return html
}
: null,
!process.browser &&
(process.env.__NEXT_OPTIMIZE_FONTS ||
process.env.__NEXT_OPTIMIZE_IMAGES)
!process.browser && process.env.__NEXT_OPTIMIZE_FONTS
? async (html: string) => {
return await postProcess(
html,
{ getFontDefinition },
{
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 { OPTIMIZED_FONT_PROVIDERS } from './constants'
// 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 = {
optimizeFonts: boolean
optimizeImages: boolean
}
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
registerPostProcessor(
'Inline-Fonts',
@ -275,11 +174,4 @@ registerPostProcessor(
(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

View file

@ -224,7 +224,6 @@ export type HtmlProps = {
crossOrigin?: string
optimizeCss?: boolean
optimizeFonts?: boolean
optimizeImages?: boolean
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()
})