{"id":5153,"date":"2025-02-10T09:33:55","date_gmt":"2025-02-10T14:33:55","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=5153"},"modified":"2025-02-10T09:33:56","modified_gmt":"2025-02-10T14:33:56","slug":"optimizing-images-for-web-performance","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/optimizing-images-for-web-performance\/","title":{"rendered":"Optimizing Images for Web Performance"},"content":{"rendered":"\n<p>Images make websites look great, but they\u2019re&nbsp;<strong>also the biggest performance bottleneck<\/strong>. They add&nbsp;<strong>huge file sizes<\/strong>, delay&nbsp;<strong>Largest Contentful Paint (LCP)<\/strong>, and can even mess with&nbsp;<strong>Cumulative Layout Shift (CLS)<\/strong>&nbsp;if they aren\u2019t handled properly. And while developers are quick to optimize JavaScript and CSS, images are&nbsp;<strong>often ignored<\/strong>\u2014despite being the&nbsp;<strong>heaviest<\/strong>&nbsp;assets on most pages.<\/p>\n\n\n\n<p>So, how do we make images&nbsp;<strong>fast, responsive, and efficient<\/strong>? Here\u2019s how:<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"choose-the-right-image-format\">Choose the Right Image Format<\/h2>\n\n\n\n<p>The\u00a0<strong>format<\/strong>\u00a0of an image has a massive impact on its size and loading speed. Picking the wrong one can easily\u00a0<strong>double or triple<\/strong>\u00a0your image payload. Check out this illustration:\u00a0<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"422\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/02\/image_formats.png?resize=1024%2C422&#038;ssl=1\" alt=\"Size comparison of images in formats\" class=\"wp-image-5158\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/02\/image_formats.png?resize=1024%2C422&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/02\/image_formats.png?resize=300%2C124&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/02\/image_formats.png?resize=768%2C317&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/02\/image_formats.png?resize=1536%2C634&amp;ssl=1 1536w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/02\/image_formats.png?resize=2048%2C845&amp;ssl=1 2048w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<p>To the user, it\u2019s the exact same image, but the browser has to download 2x-10x more data depending on the format you pick. <\/p>\n\n\n\n<p>Have a look at this photograph:\u00a0<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"422\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/02\/photo_formats.png?resize=1024%2C422&#038;ssl=1\" alt=\"Size comparison of photo in formats\" class=\"wp-image-5159\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/02\/photo_formats.png?resize=1024%2C422&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/02\/photo_formats.png?resize=300%2C124&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/02\/photo_formats.png?resize=768%2C317&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/02\/photo_formats.png?resize=1536%2C634&amp;ssl=1 1536w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/02\/photo_formats.png?resize=2048%2C845&amp;ssl=1 2048w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<p>Photographs are quite a bit more complex than illustrations (usually), and the best formats can change. Notice how JPG is smaller than PNG this time, but modern formats like WebP and AVIF are still way smaller.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>JPG<\/strong>&nbsp;\u2013 Best for photos with lots of colors and gradients. Uses&nbsp;<strong>lossy compression<\/strong>&nbsp;to keep file sizes small.<\/li>\n\n\n\n<li><strong>PNG<\/strong>&nbsp;\u2013 Best for graphics, logos, and transparency. Uses&nbsp;<strong>lossless compression<\/strong>, but files can be&nbsp;<strong>huge<\/strong>.<\/li>\n\n\n\n<li><strong>WebP<\/strong>&nbsp;\u2013 A modern format that\u2019s often&nbsp;<strong>smaller<\/strong>&nbsp;than JPG and PNG while keeping quality high.<\/li>\n\n\n\n<li><strong>AVIF<\/strong>\u00a0\u2013 Even better compression than WebP, but\u00a0<strong>not universally supported<\/strong>\u00a0yet.<\/li>\n<\/ul>\n\n\n\n<p>A\u00a0<strong>good rule of thumb<\/strong>:\u00a0<strong>JPG for photos, PNG for graphics<\/strong>, and use\u00a0<strong>WebP or AVIF where possible<\/strong>\u00a0for modern browsers.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"use-responsive-images\">Use Responsive Images<\/h2>\n\n\n\n<p>Not all users have the same screen size, so why serve the same\u00a0<strong>massive<\/strong>\u00a0image to everyone? Responsive images let you\u00a0<strong>deliver the right image size<\/strong>\u00a0based on the user\u2019s device, reducing unnecessary downloads and improving load times.<\/p>\n\n\n\n<p>Instead of a single\u00a0<code>&lt;img><\/code>\u00a0tag, try using a\u00a0<code>&lt;picture><\/code>\u00a0with the\u00a0<strong><code>srcset<\/code>\u00a0and\u00a0<code>sizes<\/code>\u00a0attributes<\/strong>\u00a0to tell the browser which image to load:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_NPKZVKL\" src=\"\/\/codepen.io\/anon\/embed\/NPKZVKL?height=450&amp;theme-id=47434&amp;slug-hash=NPKZVKL&amp;default-tab=html,result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed NPKZVKL\" title=\"CodePen Embed NPKZVKL\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>In this example, any screen less than <strong>1400px<\/strong> wide will use an image from the <code>srcset<\/code> that is at least 100% of the viewport\u2019s width. So if the screen is <strong>1100px<\/strong> wide, the browser will select and download the <code>hero-desktop-1024<\/code> version. This <strong>automatically scales images<\/strong> to match different devices, <strong>saving bandwidth<\/strong> and improving loading speed for smaller screens. <\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Lazy-Load Below the Fold <\/h2>\n\n\n\n<p>One of the worst offenders for slow performance? <strong>Loading every image on the page at once<\/strong>\u2014even ones that aren\u2019t visible. This is where <strong>lazy-loading<\/strong> comes in. Adding <code>loading=\"lazy\"<\/code> to an <code>&lt;img><\/code> <strong>prevents it from downloading<\/strong> until it\u2019s about to be seen.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-code-table\"><span class='shcb-loc'><span><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">img<\/span> <\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">  <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">\"downpage-callout.jpg\"<\/span> <\/span>\n<\/span><\/span><mark class='shcb-loc'><span><span class=\"hljs-tag\">  <span class=\"hljs-attr\">loading<\/span>=<span class=\"hljs-string\">\"lazy\"<\/span> <\/span>\n<\/span><\/mark><span class='shcb-loc'><span><span class=\"hljs-tag\">  <span class=\"hljs-attr\">height<\/span>=<span class=\"hljs-string\">\"300\"<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">  <span class=\"hljs-attr\">width<\/span>=<span class=\"hljs-string\">\"300\"<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">&gt;<\/span>  \n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>It\u2019s very important to specify the\u00a0<code>height<\/code>\u00a0and\u00a0<code>width<\/code>\u00a0attributes of images, especially if they are going to be lazy-loaded. Setting these dimensions let\u2019s the browser reserve space in your layout and prevent\u00a0<strong>layout shifts<\/strong>\u00a0when the content loads. For more about layout shifts and how to prevent them, check out this\u00a0<a href=\"https:\/\/requestmetrics.com\/web-performance\/cumulative-layout-shift\/\">deep dive on Cumulative Layout Shift<\/a>. <\/p>\n\n\n\n<p>For images that are\u00a0<strong>critical for rendering<\/strong>, like your\u00a0<strong>LCP element<\/strong>, you should\u00a0<strong>override lazy-loading<\/strong>\u00a0with\u00a0<code>fetchpriority=\"high\"<\/code>. This tells the browser to\u00a0<strong>load it ASAP<\/strong>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-code-table\"><span class='shcb-loc'><span><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">img<\/span> <\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">  <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">\"downpage-callout.jpg\"<\/span> <\/span>\n<\/span><\/span><mark class='shcb-loc'><span><span class=\"hljs-tag\">  <span class=\"hljs-attr\">fetchpriority<\/span>=<span class=\"hljs-string\">\"high\"<\/span> <\/span>\n<\/span><\/mark><span class='shcb-loc'><span><span class=\"hljs-tag\">  <span class=\"hljs-attr\">height<\/span>=<span class=\"hljs-string\">\"300\"<\/span> <\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">  <span class=\"hljs-attr\">width<\/span>=<span class=\"hljs-string\">\"300\"<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">&gt;<\/span>\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h2 class=\"wp-block-heading\" id=\"serve-images-from-a-cdn\">Serve Images from a CDN<\/h2>\n\n\n\n<p>A&nbsp;<strong>Content Delivery Network (CDN)<\/strong>&nbsp;stores images in multiple locations worldwide, so they load from the nearest server instead of your origin. This speeds up delivery and reduces bandwidth costs.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"cdns-use-modern-http-protocols\">CDNs use modern HTTP Protocols<\/h3>\n\n\n\n<p>Most CDNs will also speed up your images by serving them with modern protocols like HTTP\/3, which has significant performance improvements over both HTTP\/1 and HTTP\/2. Check out this&nbsp;<a href=\"https:\/\/requestmetrics.com\/web-performance\/http3-is-fast\/\">case study on HTTP\/3 performance<\/a>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"http-caching-headers\">HTTP Caching headers<\/h3>\n\n\n\n<p>Users always have to download an image at least once, but HTTP caching headers can help repeat visitors load them much faster. HTTP Caching headers instruct the browser to hold onto the image and use it again, rather than asking for it from the CDN again on the next visit. Here\u2019s an example:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">Cache-Control: public, max-age=31536000, immutable<\/pre>\n\n\n\n<p>This tells the browser that this image won\u2019t change, and that it can be kept locally for 1 year without needing to be requested again. Caching isn\u2019t just for images\u2014it speeds up&nbsp;<strong>all static assets<\/strong>. If you\u2019re not sure if your caching is set up correctly, there\u2019s a&nbsp;<strong><a href=\"https:\/\/requestmetrics.com\/web-performance\/http-caching\/\">full guide on HTTP caching<\/a><\/strong>&nbsp;that explains how to check and optimize it.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"wrapping-up\">Wrapping Up<\/h2>\n\n\n\n<p>Images are one of the biggest opportunities for improving performance. By&nbsp;<strong>choosing the right format, compressing efficiently, lazy-loading, and leveraging CDNs with modern protocols<\/strong>, you can massively speed up your site.<\/p>\n\n\n\n<p>If you\u2019re looking for more image optimization tips, with detailed breakdown and real-world examples, check out&nbsp;<a href=\"https:\/\/requestmetrics.com\/web-performance\/high-performance-images\/\">the complete guide to optimizing website images<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>There is some low-hanging web performance fruit with images. Serving them in the right format, from a CDN, with the right HTML can be a big perf win.<\/p>\n","protected":false},"author":33,"featured_media":5163,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[31,133,70],"class_list":["post-5153","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-html","tag-images","tag-performance"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/02\/pexels-photo-3584994.jpeg?fit=1880%2C1256&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/5153","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/33"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=5153"}],"version-history":[{"count":5,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/5153\/revisions"}],"predecessor-version":[{"id":5165,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/5153\/revisions\/5165"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/5163"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=5153"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=5153"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=5153"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}