Skip to content

fix(image): emit preload hints for priority images#1266

Open
NathanDrake2406 wants to merge 1 commit into
cloudflare:mainfrom
NathanDrake2406:nathan/image-priority-preload
Open

fix(image): emit preload hints for priority images#1266
NathanDrake2406 wants to merge 1 commit into
cloudflare:mainfrom
NathanDrake2406:nathan/image-priority-preload

Conversation

@NathanDrake2406
Copy link
Copy Markdown
Contributor

Overview

Field Detail
Goal Restore Next.js-compatible image preload hints for important images.
Core change next/image now calls ReactDOM.preload with the final image URL, srcset, sizes, and fetch priority metadata when priority or preload is set.
Main boundary The resource hint is emitted from the image shim at the point where the optimized URL and srcset are known.
Primary files packages/vinext/src/shims/image.tsx, tests/image-component.test.ts
Expected impact Priority hero images can be discovered earlier by the browser, improving LCP parity with Next.js.

Why

Next.js treats image preloading as a resource-discovery contract, not just an <img> loading-mode tweak. priority is now a deprecated alias for preload, and both paths cause Next.js to emit an image preload hint. Vinext already translated priority to eager loading and high fetch priority, but it did not emit the preload resource hint, so the optimized image stayed undiscoverable until the parser reached the <img>.

Area Principle / invariant What this PR changes
Image SSR output A prioritized image should expose an early browser resource hint. Adds ReactDOM.preload for priority and preload.
Optimized local images The preload must point at the same optimized URL and candidate set as the rendered image. Uses optimizedSrc, srcSet, and computed sizes.
Next.js compatibility priority remains supported, while current preload semantics should also work. Adds preload?: boolean and avoids default lazy loading when only preload is set.

What changed

Scenario Before After
<Image priority /> Rendered eager/high-priority <img> only. Emits <link rel="preload" as="image"> via React DOM and keeps eager/high-priority <img>.
<Image preload /> Prop was forwarded through rest and no preload hint was emitted. Prop is consumed by the shim and emits an image preload hint without default lazy loading.
Local optimized image No early hint for /_vinext/image?... candidates. Preload includes the optimized srcset candidates.
Maintainer review path
  1. packages/vinext/src/shims/image.tsx: review preloadImageResource, the shouldPreload/imageLoading derivation, and the local optimized image branch where optimizedSrc, srcSet, and sizes are known.
  2. tests/image-component.test.ts: review the SSR assertions for priority and the current preload prop.
Validation
  • vp test run tests/image-component.test.ts -t "preload|priority"
  • vp test run tests/image-component.test.ts
  • vp check tests/image-component.test.ts
  • vp check
Risk / compatibility
  • Public API: adds support for the current Next.js preload prop and preserves existing priority behaviour.
  • Runtime: relies on React DOM preload when available, matching current Next.js. The helper no-ops if ReactDOM.preload is unavailable.
  • Compatibility: remote images rendered through @unpic/react are preloaded by their source URL because vinext does not currently expose Unpic-generated srcset data before rendering. Local optimized images get exact optimized URL/srcset coverage.
Non-goals
  • This does not rework remote image generation around @unpic/react.
  • This does not add Next.js dev warnings for invalid priority, preload, and loading="lazy" combinations.
  • This does not change image optimizer request handling.

References

Reference Why it matters
Next.js ImagePreload source Shows ReactDOM.preload / <link rel="preload" as="image"> semantics for next/image.
Next.js getImgProps meta Shows preload and deprecated priority are both accepted image props.
Next.js preload metadata Shows meta.preload is computed as `preload
Next.js app router image preload test Asserts priority images produce an image preload in document head.

Priority images currently only render eager loading and high fetch priority. That misses the resource hint Next.js emits, so the browser cannot discover the optimized image until it reaches the img element.

The image shim now calls ReactDOM.preload with the final optimized image URL, srcset, sizes, and fetch priority metadata for priority and preload images. Focused image component coverage asserts the SSR preload hint and the modern preload prop behaviour.
@NathanDrake2406 NathanDrake2406 marked this pull request as ready for review May 16, 2026 12:33
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 16, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@1266

commit: d4ac175

@james-elicx
Copy link
Copy Markdown
Collaborator

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean implementation that matches Next.js behavior well. Verified against Next.js source (get-img-props.ts and image-component.tsx on canary). Tests pass.

What I checked:

  1. loading derivation matches Next.js. In Next.js: isLazy = !priority && !preload && (loading === 'lazy' || typeof loading === 'undefined'), then loadingFinal = isLazy ? 'lazy' : loading. The vinext equivalent imageLoading = priority ? 'eager' : shouldPreload ? loading : (loading ?? 'lazy') produces identical results for all combinations of priority, preload, and loading.

  2. ReactDOM.preload call structure matches Next.js. Next.js passes as: 'image', imageSrcSet, imageSizes, and fetchPriority — vinext does the same. Next.js additionally passes crossOrigin and referrerPolicy, but those props aren't supported on vinext's Image component at all, so that's a pre-existing gap.

  3. preload prop is destructured out of the component props, so it won't leak through ...rest to the DOM.

  4. getImageProps correctly computes shouldPreload but only uses it for the loading derivation (no preload hint emission), which is correct since getImageProps returns props, not JSX.

  5. No duplicate preload calls — the remote URL paths return early, so the local image preload at the bottom of the function is only reached for local images.

  6. All 53 tests pass, including the two new ones.

Two minor nits below, neither blocking.

fetchPriority?: ReactDOM.PreloadOptions["fetchPriority"];
}): void {
if (!input.shouldPreload) return;
if (typeof ReactDOM.preload !== "function") return;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: The typeof ReactDOM.preload !== 'function' guard is good, but in Next.js the ImagePreload component also passes crossOrigin and referrerPolicy to the preload options (since those affect CORS and the browser's preload matching). vinext's Image component doesn't accept these props today, so nothing to fix here, but worth noting as a follow-up gap — if crossOrigin or referrerPolicy support is added to the Image component later, the preload call should be updated to pass them too.

expect(html).toContain('<link rel="preload"');
expect(html).toContain('as="image"');
expect(html).toContain(`imageSrcSet="${optUrlHtml("/hero-preload.png", 640)} 640w`);
expect(html).not.toContain('loading="lazy"');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This assertion confirms loading="lazy" is absent, which is correct (when preload=true without explicit loading, both Next.js and vinext omit the loading attribute entirely). Consider also asserting the positive case — that loading is not rendered at all — to make the intent clearer and catch regressions where someone might accidentally default to loading="eager" for preload-only images:

Suggested change
expect(html).not.toContain('loading="lazy"');
expect(html).not.toContain('loading="lazy"');
expect(html).not.toContain('loading="eager"');

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 16, 2026

Review submitted with approval. Here's a summary of what I found:

Verdict: Approved

The PR correctly adds ReactDOM.preload hints for priority/preload images, matching Next.js behavior. I verified the implementation against Next.js canary source (get-img-props.ts and image-component.tsx):

  • The loading attribute derivation produces identical results to Next.js for all combinations of priority, preload, and loading
  • The ReactDOM.preload call matches Next.js's ImagePreload component structure
  • The preload prop is properly destructured to avoid DOM leaks
  • No duplicate preload calls due to early returns in remote image paths
  • All 53 tests pass

Two minor nits posted:

  1. Note about missing crossOrigin/referrerPolicy in preload options (pre-existing gap, not introduced here)
  2. Suggestion to also assert loading="eager" is absent in the preload-only test, to catch accidental defaults

github run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants