shipping/JOG — 03:41 GMT+7/48.3 °C CPU/v 07.0 · build 2026.05.12
lat −7.7956 · lon 110.3695/ntwk · online/open for projects
back to notesnotes2026next-image-not-overkill
[ essay no. 26 ]engineeringjuly 1, 20265 min964 wordsrevision 1live

next/image isn't overkill

PageSpeed once told me my portfolio shipped 31 MB of unoptimized PNGs. The migration to next/image, the LCP win, the trade-offs people skip when they reach for plain background-image.

d
devrangga hazza mahiswaracreative engineer · jogja, id
share on 𝕏

next/image isn't overkill

31.41 MBraw PNG payload before
1.32 MBafter AVIF conversion
96%byte reduction
35 → 75+PageSpeed mobile delta

The moment the case for next/image became unarguable for me was opening the PageSpeed Insights report for my own portfolio and seeing 31 MB of images flagged as the LCP killer. My own portfolio. The site I'd been telling clients was performance-optimized. The numbers were honest in a way I hadn't been with myself.

Every project hero. PNG. 4 to 6 megabytes each. Loaded as CSS background-image, bypassing every Next.js optimization the framework was already doing for me. The fix was three hours of work. The credibility cost of having shipped it that way in the first place is harder to recover.

This post is the diagnosis, the fix, and the case for next/image even on the surfaces where it feels like overkill.

01

why background-image is the trap

The seductive case for background-image is its terseness:

.hero {
  background-image: url('/projects/oracle.png');
  background-size: cover;
}

Three lines, the image loads, the layout cooperates. What's the problem.

The problem is everything next/image does that this version doesn't.

  • No format negotiation. next/image serves AVIF or WebP based on browser support, falling back to PNG. background-image serves whatever file you specified.
  • No resolution negotiation. next/image serves a size appropriate for the user's viewport. background-image serves the full file every time.
  • No lazy loading. next/image defers offscreen images. background-image doesn't.
  • No layout-shift prevention. next/image reserves space via the width / height props. background-image doesn't (the element is sized by CSS, but the image inside it can still pop in awkwardly).

The cost of not using next/image is invisible until it shows up as a 71-second LCP on a PageSpeed report. By that point you're explaining the number to a client.

02

the migration

The work, end-to-end:

Step 1: convert source images. PNG → AVIF via a sharp-backed script. AVIF at quality 60, max width 1600px. The 31 MB collapses to ~1.3 MB. Single biggest win.

Step 2: replace background-image with next/image. Every place a background-image: url(...) lived, convert to a <Image fill> inside a positioned container.

// Before
<div style={{ backgroundImage: 'url("/projects/oracle.png")', backgroundSize: 'cover' }} />
 
// After
<div className="relative aspect-[16/9] overflow-hidden">
  <Image
    src="/projects/oracle.avif"
    alt="Hino workflow platform — overview"
    fill
    sizes="(max-width: 768px) 100vw, 720px"
    className="object-cover"
  />
</div>

Five files in my portfolio's case. The sizes directive is the bit most engineers skip — it tells next/image which resolution to serve. Without it, next/image ships the full-size image to mobile, which defeats half the optimization.

Step 3: handle layout shift. The container needs an explicit aspect-ratio or a fixed height so the layout doesn't shift when the image loads. The aspect-[16/9] class above is the cheapest way; explicit width / height props work too.

Step 4: add a skeleton. Image loads, even AVIF ones, take some time. A placeholder shimmer prevents the blank-rectangle moment between layout and load. Optional but worth the 30 lines.

03

the win

The numbers before / after, measured on the actual portfolio:

MetricBeforeAfter
Total image payload31.41 MB1.32 MB
Image content-typePNGAVIF (WebP fallback)
Lighthouse LCP (mobile slow 4G)71.6 s~5.8 s
Lighthouse Performance score (mobile slow 4G)35~78
Lighthouse Performance score (desktop)~65~96

The 96-byte-reduction is the most dramatic single change to the site I've made. Faster than any code optimization, any chunk-splitting, any caching strategy. Pure asset hygiene.

04

when next/image is actually wrong

In the interest of being honest about the trade-offs:

Decorative tiny PNGs and SVGs. A 2KB icon doesn't benefit from format negotiation. The next/image overhead — the runtime layout management, the slight server-side cost — is wasted. Use <img> or inline SVG for these.

CSS gradients and patterns. No image, no next/image needed. CSS-native gradients ship as a few bytes of stylesheet.

Pixel-perfect critical hero images. Sometimes you genuinely need to control exactly which file loads at exactly which resolution. next/image will sometimes pick a resolution that's slightly different from what you intended. Rare but real.

Backgrounds that don't change with viewport. A repeating pattern at full opacity behind everything; background-image is fine here because there's no version of better content for mobile.

Even in those cases, the default should be next/image. The exceptions are small, well-defined, and you should be able to articulate why you're skipping the optimization. Because it feels like overkill is not a valid reason. It's a tell that the engineer doesn't know what the optimization is doing.

05

the meta-lesson

The 31 MB of PNGs hadn't been there because I didn't know better. They were there because the portfolio had been shipped at a stage where making it look good in dev was the priority, and optimizing for the user's network was deferred. The deferred work compounded into a publicly-visible perf failure.

The general form of the lesson:

Every "we'll optimize later" decision is a decision that you accept the cost of not optimizing for some duration. The duration is usually longer than you expect, and the cost is paid by users on slower devices and networks who you'd never have shipped to deliberately.

next/image is the default. Skip it only when you can articulate why. The cost of using it when you didn't need to: nearly zero. The cost of not using it when you did need to: the LCP that ate my mobile PageSpeed score.

— end of essay · published july 1, 2026 · 964 words · 5 min
[ if this moved you ]

keep reading.

three essays in the same key · pick one