INP-focused Image Delivery 2025 — Protecting Perceived Performance with decode/priority/script coordination
Published: Sep 21, 2025 · Reading time: 8 min · By Unified Image Tools Editorial
Introduction
Improving LCP is central to image optimization, but in 2024–2025 many teams overlook INP (Interaction to Next Paint). Heavy image decoding or main-thread initialization right around the first user interaction can delay responsiveness. When “lazy-load bursts,” image decoding, and script initialization pile up immediately after the first tap/scroll, frames drop and input gets sluggish.
This guide puts “eliminate overserving from layout” first, then organizes practices that protect INP across decode/priority control/lazy-load and script coordination. We’ll show how to split roles between hero and below-the-fold images, how to use Next.js (App Router) and browser APIs effectively, and how to verify with RUM.
TL;DR
- Cut overserving first (design
sizes/srcset
from layout) - Use
priority
/fetchPriority="high"
only for LCP candidates; usedecoding="async"
+ lazy for non-LCP - Avoid racing image load/decoding/script init around first input (coordinate timing)
- Placeholder should not cause CLS (LQIP/BlurHash etc.)
For detailed resizing and sizes/srcset
, see Responsive Images in 2025 — Srcset/Sizes Patterns That Actually Match Layout and Right-Size Images in 2025 — Layout-Backed Resizing That Cuts 30–70% Bytes.
Why images can worsen INP (mental model)
The browser goes “network fetch → decode → layout/paint.” INP measures latency from input to next paint. If heavy image decode or re-layout runs right before/after input handling, INP gets worse.
- If lazy-loads fire in bulk right after input, the main thread fills with decode/layout and event processing stalls
- Oversized images take longer to decode (overserving harms both LCP and INP)
- Script initialization (parallax, filters, Canvas) collides with decoding
The remedy is a three-in-one: “right size,” “selective priority,” and “timing separation (coordination).”
Patterns that harm INP
- Oversized image decode happens alongside input processing
- Immediately after first input, a burst of lazy-loaded images triggers (frames drop)
- Image initialization (Canvas/filters/animations) blocks the main thread
It’s more about timing conflicts than raw weight.
Anti-patterns and quick fixes
- Converting offscreen images to
eager
right when scrolling starts → use rootMargin and stage loads 200–400px ahead - Abusing
priority
beyond the hero on slow devices → strictly limit to LCP candidates (checklist below) - Decoding many images while CSS animations/Canvas run → shift image work using idle/scheduler
Next.js (App Router) implementation patterns
// Hero: LCP candidate (in-view on initial render)
<Image
src="/hero-1536.avif"
alt="Product hero"
fill
sizes="(max-width: 768px) 100vw, 768px"
priority
fetchPriority="high"
decoding="sync"
/>
// Below the fold: non-LCP (just before entering viewport)
<Image
src="/gallery-640.webp"
alt="Gallery"
width={640}
height={360}
sizes="(max-width: 768px) 100vw, 768px"
loading="lazy"
decoding="async"
/>
Notes:
- Only LCP candidates use sync/priority/high. Others stay lazy/async to free the main thread
- If
sizes
is correct, browsers choose optimal sources (overserving harms LCP and INP at once)
Extra hints (resource hints/connection)
// <head> hints (App Router: layout.tsx etc.)
export const metadata = {
other: {
link: [
{ rel: 'preconnect', href: 'https://cdn.example.com', crossOrigin: 'anonymous' },
],
},
};
- Preconnecting to the image CDN reduces RTT (don’t overuse)
- Combine with
priority
only for LCP candidates (hero/first-view key imagery)
Granular lazy and script coordination
- Avoid heavy decode/init 300–500ms around user input
- Use IntersectionObserver to preload slightly ahead of view, avoiding collisions right after input
- Use
requestIdleCallback
and scheduling for long inits (Canvas/filters)
const io = new IntersectionObserver((entries) => {
for (const e of entries) {
if (e.isIntersecting) {
// Prioritize only what must paint now
e.target.setAttribute('loading', 'eager');
io.unobserve(e.target);
}
}
}, { rootMargin: '200px 0px' });
Additionally, add a simple scheduler to skip heavy work within 300ms after input.
let lastInput = 0;
['pointerdown','keydown','wheel','touchstart'].forEach((t) => {
window.addEventListener(t, () => (lastInput = performance.now()), { passive: true });
});
export function scheduleAfterInput<T>(task: () => T) {
const dt = performance.now() - lastInput;
if (dt < 300) {
setTimeout(task, 300 - dt);
} else {
requestIdleCallback(() => task());
}
}
Placeholders and CLS
Specify dimensions (width/height or aspect-ratio) to avoid CLS, and pair with lightweight LQIP/SQIP/BlurHash. See Placeholder Design LQIP/SQIP/BlurHash — Practical 2025.
Overly heavy blur on the hero rarely helps INP. Keep placeholders simple and light.
Measurement and guardrails
- Collect field INP via RUM (web-vitals)
- Monitor Long Tasks after input; revisit image decode/init timing if collisions occur
- Use Lighthouse Timespan to score interaction scenarios
RUM example (simplified)
import { onINP, onLCP } from 'web-vitals/attribution';
onINP((metric) => {
fetch('/rum', {
method: 'POST',
keepalive: true,
body: JSON.stringify({
name: metric.name,
value: metric.value,
entries: metric.entries?.length,
}),
headers: { 'content-type': 'application/json' },
});
});
onLCP((metric) => {
// Capture correlation between LCP and image priority
});
Manual verification (Timespan)
- Scenario: Load page → scroll after 0.5s → right before images enter view
- Inspect Performance/Long Task in DevTools; ensure decode isn’t colliding with input
Case studies
Case 1: Landing page hero + gallery below the fold
- Symptom: Right after the first scroll, 20 thumbnails load/decode at once; INP p75 worsens 280→360ms
- Cause: Strict viewport thresholds make lazy-loads fire in bulk;
sizes
too small causes overserving - Fix:
- Correct
sizes
to match layout (cut overserving by 35%) - Use
rootMargin: '300px 0px'
to stagger preloads - Defer thumbnail init (unblur/animation) with
requestIdleCallback
to 300ms after input
- Correct
- Result: INP p75 improved 360→250ms; LCP +3%
Case 2: E‑commerce product list (filter UI + image swap)
- Symptom: After pressing a filter, image swap and Canvas processing collide; response feels sluggish
- Cause: Sync decode + immediate Canvas draw saturate the main thread
- Fix:
- Use
decoding="async"
consistently; fade-in the swap to keep perception acceptable - Move heavy Canvas work to a web worker or
OffscreenCanvas
- Delay non-critical processing until 300ms after input
- Use
- Result: No more dropped frames after interactions; INP p75 improved 280→210ms
Implementation patterns to avoid races
1) Control decode timing
// Pre-create image and predecode on idle (limit to a few images)
export function predecodeOnIdle(src: string) {
const img = new Image();
img.src = src;
requestIdleCallback(async () => {
try { await img.decode(); } catch {}
});
}
Use sparingly — predecoding many images backfires. Limit to 1–3 just before view.
2) Separate priorities with scheduler.postTask (where available)
// @ts-ignore experimental
const schedulerAny: any = (globalThis as any).scheduler;
export async function afterInputLow(task: () => void) {
const dt = performance.now() - ((window as any).__lastInputTs ?? 0);
if (dt < 300 && schedulerAny?.postTask) {
await schedulerAny.postTask(task, { priority: 'background', delay: 300 - dt });
} else {
requestIdleCallback(task);
}
}
Hook input timestamp:
['pointerdown','keydown','wheel','touchstart'].forEach((t) => {
addEventListener(t, () => ((window as any).__lastInputTs = performance.now()), { passive: true });
});
3) Staged loading with IntersectionObserver
const io = new IntersectionObserver((entries) => {
for (const e of entries) {
if (e.isIntersecting) {
const el = e.target as HTMLImageElement;
el.loading = 'eager';
io.unobserve(el);
}
}
}, { rootMargin: '300px 0px' });
export function watch(img: HTMLImageElement) { io.observe(img); }
4) Adjust rootMargin by network conditions
function chooseRootMargin() {
const n = (navigator as any).connection;
if (!n) return '200px 0px';
if (n.saveData) return '150px 0px';
if (n.effectiveType?.includes('2g')) return '400px 0px';
return '300px 0px';
}
Next.js application patterns (App Router)
Server/client split
- Server: compute accurate
sizes
from layout - Client: observe inputs and suppress init tasks in the 300ms window
- Image CDN presets; fingerprinted URLs for stability
Template hero and gallery components
// components/HeroImage.tsx
export function HeroImage(props: Omit<React.ComponentProps<'img'>, 'decoding'|'loading'>) {
return (
<img {...props} decoding="sync" fetchPriority="high" />
);
}
// components/GalleryImage.tsx
export function GalleryImage(props: Omit<React.ComponentProps<'img'>, 'decoding'>) {
return (
<img {...props} loading="lazy" decoding="async" />
);
}
Adopt HeroImage
/GalleryImage
across the app and lint against ad‑hoc priority
usage.
Monitoring (Long Task / Event Timing)
new PerformanceObserver((list) => {
for (const e of list.getEntries()) {
const lt = e as PerformanceEntry & { duration: number };
if (lt.duration > 50) {
// check overlap within the 300ms window after input
}
}
}).observe({ type: 'longtask', buffered: true });
new PerformanceObserver((list) => {
for (const e of list.getEntries()) {
// e.name (click, keydown …), processingStart, duration
}
}).observe({ type: 'event', buffered: true });
In RUM, store p50/p75/p95 and detect regressions per deploy. Break down by UA/network type for reproducibility.
FAQ
Q. Can I apply priority
to multiple images?
A. Generally no. Restrict to one hero (or first slide). Multiple priorities risk saturating network and decode resources.
Q. When to use decoding="sync"
?
A. Only for LCP candidates that are visually part of initial content. Use async
otherwise.
Q. Is preconnect
to the image CDN helpful?
A. Yes when the first view definitely uses it, but limit to 1–2 domains to avoid diminishing returns.
Q. Do blur placeholders help INP?
A. Not directly. They help CLS and perceived loading; keep them lightweight.
Extended checklist
- [ ] Accurate
sizes
on all images (eliminate overserving) - [ ] Only LCP candidates use
priority
/fetchPriority="high"
/decoding="sync"
- [ ] Non‑LCP images =
loading="lazy"
+decoding="async"
- [ ] No heavy decode/init in the 300–500ms window around input
- [ ] Staged loading via rootMargin (200–400px; adjust by connection)
- [ ] RUM logs INP/LCP correlation; p75 as primary metric
- [ ] Lint/CI catches
priority
abuse and missingsizes
Summary
Protecting INP comes down to “eliminate overserving,” “assign priority correctly,” and “keep heavy work away from input windows.” Add timing coordination to your LCP work and you’ll see big perceived gains. Finally, monitor in RUM and enforce a lint/CI rule to forbid priority
outside the hero.
Delivery checklist:
- [ ] Only LCP candidates use
priority
/fetchPriority="high"
- [ ] Non‑LCP use
loading="lazy"
+decoding="async"
- [ ]
sizes
matches layout (no overserving) - [ ] Stage with rootMargin; avoid heavy work within 300ms after input
- [ ] Specify width/height or aspect-ratio to keep CLS≈0
Related Articles
Image Optimization Basics 2025 — Foundations That Scale Without Guesswork
A practical, up-to-date starting guide covering the essential concepts that keep image delivery fast and beautiful across devices and locales.
Image SEO 2025 — Practical alt text, structured data, and sitemaps
A practical, 2025-proof implementation for image SEO to capture search traffic: alt text, filenames, structured data, image sitemaps, and LCP optimization under one unified policy.
Responsive Images in 2025 — Srcset/Sizes Patterns That Actually Match Layout
Proven patterns to wire srcset/sizes so the browser picks the correct resource without overserving; includes LCP hints and lazy loading.
Ultimate Image Compression Strategy 2025 – A Practical Guide to Preserving Quality While Optimizing Perceived Speed
A comprehensive, field-tested image compression and delivery strategy for Core Web Vitals: format choices, presets, responsive workflows, build/CDN automation, and troubleshooting.
Accessible Images in Practice — alt/Decorative/Diagram 2025
Implement images that work with screen readers. Decorative images should be silent (empty alt), informative ones concise, and diagrams summarized. Covers linked images and OGP specifics.
Favicon & PWA Assets Checklist 2025 — Manifests, Icons, and SEO Signals
A concise checklist to ship complete favicon/PWA assets with correct manifest wiring and locale support.