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:
parent
c20e8297bf
commit
f516304649
17 changed files with 2 additions and 342 deletions
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -224,7 +224,6 @@ export type HtmlProps = {
|
|||
crossOrigin?: string
|
||||
optimizeCss?: boolean
|
||||
optimizeFonts?: boolean
|
||||
optimizeImages?: boolean
|
||||
runtime?: 'edge' | 'nodejs'
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
module.exports = {
|
||||
target: 'serverless',
|
||||
experimental: { optimizeImages: true },
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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()
|
||||
})
|
Loading…
Reference in a new issue