惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
Netflix TechBlog - Medium
IT之家
IT之家
博客园_首页
Hugging Face - Blog
Hugging Face - Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
美团技术团队
小众软件
小众软件
博客园 - 叶小钗
WordPress大学
WordPress大学
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 三生石上(FineUI控件)
罗磊的独立博客
博客园 - Franky
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Last Week in AI
Last Week in AI
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
有赞技术团队
有赞技术团队
T
Tailwind CSS Blog
宝玉的分享
宝玉的分享
博客园 - 【当耐特】
月光博客
月光博客
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
酷 壳 – CoolShell
酷 壳 – CoolShell
人人都是产品经理
人人都是产品经理

The Practical Developer

The Libuv Thread Pool Trap: Why Node.js Async APIs Stall Under Load Postgres Covering Indexes with INCLUDE: Eliminate Heap Fetches on Read-Heavy Workloads Postgres DISTINCT ON: The Fastest Way to Get the Latest Row Per Group Postgres Transaction Isolation: The Anomalies Your App Actually Faces in Production Linux TCP Tuning for Node.js Microservices: The Kernel Settings That Stop Silent Connection Drops Under Load Postgres HOT Updates and Fillfactor: Why Not All Writes Are Created Equal Database Connection Pool Leaks: Finding the Promise That Never Returns Its Seat Linux OOM Killer in Production: Why Your Node.js Containers Die Without a Stack Trace Postgres Materialized Views: Refresh Strategies That Do Not Lock Your Dashboards API Dependency Health Checks: Why /health Is Not Enough Authorization with Zanzibar Tuples: How Google Manages Permissions and How To Build the Same Check in Node.js Postgres Advisory Locks: The 20-Character Primitive That Replaces Redis for Coordination Dead Letter Queues: The Message Queue Pattern That Saves You at 2 a.m. File Descriptor Exhaustion: The Kernel Limit That Silently Drops Node.js Connections Graceful Degradation: The Pattern That Turns Total Outages into Partial Success PostgreSQL Full-Text Search: Dropping Elasticsearch for 90% of Use Cases S3 Presigned Multipart Uploads: Stop Your API Server from Being a File Upload Bottleneck MessagePack vs JSON: The Binary Serialization Switch That Cut Our Internal RPC Overhead by 40% DNS Caching in Node.js: The Silent Cause of Production Latency Spikes Reliable Cron Jobs: The Pattern That Stops Double Runs, Missed Executions, And The 2 AM Page GraphQL Query Complexity: Stop the OOM Query Before It Reaches Your Resolver Node.js Event Loop Lag: The Hidden Metric Behind Random Latency Spikes API Request Validation with Zod: The Schema That Catches Bad Input Before It Corrupts Your Database Load Shedding in Node.js: How to Reject Traffic Before You Drown Request Hedging: Cut Tail Latency In Half Without Overprovisioning Git Bisect: The Automated Binary Search That Finds Breaking Commits in Minutes Node.js Garbage Collection Tuning: Stop Letting V8 Pause Your Event Loop Node.js Server Timeouts: The Settings That Stop Slow Clients from Holding Sockets Hostage Postgres BRIN Indexes: The Time-Series Secret That Shrinks Indexes by 99% Event Sourcing with PostgreSQL: The Pragmatic 80% Solution
Image Optimization For The Web In 2023: srcset, AVIF, And...
The Practica · 2023-07-21 · via The Practical Developer

The dashboard ships a hero image that is 2.4 MB. The slot renders at 600 px wide on the average device. The browser downloads a 4000-px-wide JPEG, decodes it, scales it down, and discards the rest. Every visitor pays for the difference. Lighthouse is yelling about LCP and “properly size images” and “serve images in next-gen formats.” The fix is not “compress harder.” It is to ask the CDN for the right size in the right format, and to teach the <img> tag to pick the right variant for the actual device.

Image optimization in 2023 is mostly about three things: an image CDN that does the work, srcset so the browser picks the right size, and the four <img> attributes that PageSpeed cares about. This post is the working pattern.

The image CDN does the heavy lifting

A modern image CDN (Cloudinary, Imgix, ImageKit, Cloudflare Images, AWS CloudFront with Lambda@Edge, or your own with imgproxy) accepts URL parameters and returns the variant you ask for:

https://cdn.example.com/path/to/image.jpg?w=800&q=75&fm=auto

The host caches on (URL, headers) so the same variant is generated once and served forever. The benefit is enormous:

  • One source image; many derived variants.
  • Format negotiation: serve AVIF / WebP / JPEG based on Accept header.
  • On-the-fly resize: no need to pre-render every size.

Without a CDN, image optimization is a build-time pipeline that can never quite keep up with new content. With a CDN, every image is automatically right-sized.

The four URL params worth knowing

(Specifics vary by CDN; the concepts are universal.)

  • w= width. The pixel width to render at. Match this to the slot the image fills, not the source resolution.
  • q= quality. 75 is the sweet spot. Above 85 the file size doubles for no perceptual gain. Below 70 starts to look bad on photos.
  • fm=auto or auto=format. Negotiate format based on Accept header: AVIF for newer browsers, WebP for older, JPEG for ancient. Strictly better than picking one format yourself.
  • fit=crop / fit=contain. How to handle aspect ratios that don’t match.

Avoid q=100; every byte over q=85 is wasted. Avoid fm=webp (or fm=avif) directly; auto handles the fallback for old browsers correctly.

srcset: the browser picks the size

A single fixed-width URL is wrong because devices differ. A 600px slot on a retina iPhone 14 needs a 1200px image (2x DPR); a 600px slot on a 1080p laptop needs a 600px image. srcset lets the browser pick:

<img
  src="https://cdn.example.com/photo.jpg?w=600&q=75&fm=auto"
  srcset="
    https://cdn.example.com/photo.jpg?w=600&q=75&fm=auto 600w,
    https://cdn.example.com/photo.jpg?w=1200&q=75&fm=auto 1200w,
    https://cdn.example.com/photo.jpg?w=2000&q=75&fm=auto 2000w
  "
  sizes="(max-width: 768px) 100vw, 600px"
  width="600"
  height="400"
  alt="..."
  loading="lazy"
  decoding="async"
/>

srcset lists candidates with their intrinsic widths (600w, 1200w). sizes tells the browser the rendered width of the slot under different conditions. The browser computes (DPR × rendered width) and picks the smallest candidate ≥ that target.

The biggest mistake: omitting sizes. Without it, browsers may pick the largest candidate to be safe, defeating the point.

The four <img> attributes that move scores

Every PSI hint about images comes back to these four:

width and height. Required to prevent CLS (Cumulative Layout Shift). The browser reserves space before the image loads. Match the aspect ratio; the actual rendered size can still be CSS-controlled.

loading="lazy" for everything below the fold. The browser defers loading until the image is near the viewport. Do not lazy-load the LCP image. It is the largest contentful paint, which is exactly what you want to load fast.

decoding="async". Decoding a 2 MB JPEG can block the main thread for 30 ms. decoding="async" tells the browser to do it off-thread.

alt. Required for accessibility. PSI flags missing alts. Be descriptive (alt="developer at a keyboard"), not generic (alt="image").

The LCP image needs special handling

The hero image at the top of the page is almost always the LCP element. Treat it differently:

<img
  src="https://cdn.example.com/hero.jpg?w=1200&q=75&fm=auto"
  srcset="..."
  sizes="..."
  width="1200" height="675"
  alt="..."
  loading="eager"
  fetchpriority="high"
  decoding="async"
/>

loading="eager" (or omitted, since eager is the default) forces immediate load. fetchpriority="high" (Chromium-only but widely supported) tells the browser to prioritize this fetch over others.

For an extra speedup, add a <link rel="preload"> in the head:

<link
  rel="preload"
  as="image"
  href="https://cdn.example.com/hero.jpg?w=1200&q=75&fm=auto"
  imagesrcset="..."
  imagesizes="..."
/>

This starts the fetch the moment the HTML is parsed, earlier than the parser would discover the <img> tag.

Network hints in the head

Set up the CDN connection early:

<link rel="preconnect" href="https://cdn.example.com" crossorigin />
<link rel="dns-prefetch" href="https://cdn.example.com" />

preconnect does DNS, TCP, and TLS up front. Save 100–300 ms on first image fetch. dns-prefetch is a fallback for browsers that don’t support preconnect.

AVIF, WebP, and the format wars

In 2023, the browser support is:

  • AVIF: ~90% of users (Chrome, Firefox, Safari 16+). 30-50% smaller than WebP at equivalent quality.
  • WebP: ~97% of users. 25-35% smaller than JPEG.
  • JPEG: 100%. The fallback.

auto=format (or fm=auto) on the CDN side handles the negotiation. Don’t hard-code a format or you cut yourself off from future improvements.

For large hero images, AVIF saves real bytes. A 1200×675 JPEG at q=75 is ~110 KB; AVIF at q=75 is ~50 KB. For thumbnails (~600 KB), the difference is smaller in absolute terms but still meaningful.

What to watch in Lighthouse

The image-related audits to make pass:

  • “Properly size images”: pass with srcset + sizes.
  • “Serve images in next-gen formats”: pass with auto=format.
  • “Efficiently encode images”: pass with q=75-80.
  • “Defer offscreen images”: pass with loading="lazy".
  • “Image elements have explicit width and height”: pass with the attributes.
  • “Largest Contentful Paint”: pass with eager + fetchpriority + preload on hero.

A site that gets all of these green typically has a Performance score above 90 on real devices.

Common mistakes

Hard-coding fm=webp. Safari ate up the WebP cost; AVIF is now better. fm=auto keeps you current.

Lazy-loading the hero. Lighthouse will mark LCP as eager-loaded even if the attribute is lazy. Use loading="eager".

Tiny placeholders that block layout. A 1×1 placeholder image with width="1" height="1" causes the same CLS as missing dimensions. Use the real width/height for the slot.

Pasting a Unsplash page URL. https://unsplash.com/photos/... is the website, not the CDN. The CDN host is images.unsplash.com. Always extract the photo URL.

Build-time vs runtime optimization

Two camps:

  • Build-time: Eleventy Image, Next.js next/image, Astro @astrojs/image. Generates variants at build, ships them as static files. Fast, deterministic, but doesn’t optimize user-uploaded content.
  • Runtime: Cloudinary, Imgix, etc. Generates variants on demand. Handles user uploads, but costs money per transformation.

Most production sites use both: build-time for editorial content, runtime CDN for user uploads. The framework you choose usually has an answer for one of these; pick the matching tool.

The takeaway

Image performance is solved. The pieces are: an image CDN that does format and size on the fly, srcset + sizes so the browser picks the right variant, the four <img> attributes that PSI checks, eager + preload for the LCP image. Implement once, document the convention, never think about it again.

The next time PageSpeed complains about images, check the four attributes. Nine times out of ten the fix is one missing sizes or one hardcoded fm=webp.


A note from Yojji

The kind of front-end performance work that takes a heavy site to a 90+ Lighthouse score (image CDN config, srcset disciplines, preload hints in the right places) is the kind of polish Yojji’s frontend teams build into the products they ship for clients.

Yojji is an international custom software development company founded in 2016, with teams across Europe, the US, and the UK. They specialize in the JavaScript ecosystem (React, Node.js, TypeScript), cloud platforms (AWS, Azure, GCP), and full-cycle frontend engineering, including the image and asset optimization that decides whether a site feels slow or instant.