<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Muhaymin Bin Mehmood</title>
    <description>The latest articles on DEV Community by Muhaymin Bin Mehmood (@muhayminbinmehmood).</description>
    <link>https://dev.to/muhayminbinmehmood</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F862113%2F6e7ab150-0bd1-48ee-93a6-02227f19748c.webp</url>
      <title>DEV Community: Muhaymin Bin Mehmood</title>
      <link>https://dev.to/muhayminbinmehmood</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/muhayminbinmehmood"/>
    <language>en</language>
    <item>
      <title>Your Image CDN Picks the Format. Here's How to See What It Actually Sends</title>
      <dc:creator>Muhaymin Bin Mehmood</dc:creator>
      <pubDate>Mon, 12 Oct 2026 10:52:27 +0000</pubDate>
      <link>https://dev.to/muhayminbinmehmood/your-image-cdn-picks-the-format-heres-how-to-see-what-it-actually-sends-39ii</link>
      <guid>https://dev.to/muhayminbinmehmood/your-image-cdn-picks-the-format-heres-how-to-see-what-it-actually-sends-39ii</guid>
      <description>&lt;p&gt;Here is one image URL from a real Shopify store, requested three times. Same URL, same second, three different files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://jellinator.com/cdn/shop/files/ShotCups_100Pack.png?width=1200

  Chrome (AVIF+WebP)   image/webp     87 KB   Vary: Accept
  WebP only            image/webp     87 KB   Vary: Accept
  Old browser          image/png    1223 KB   Vary: Accept
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only thing that changed between the three requests is the &lt;code&gt;Accept&lt;/code&gt; header. A browser that says it understands WebP gets an 87 KB file. A client that doesn't gets the original 1.2 MB PNG.&lt;/p&gt;

&lt;p&gt;That is content negotiation, and most image CDNs do it now. It's great for visitors. It also means that "what format is this image?" has no single answer, and some tools will give you the wrong one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The header that decides it
&lt;/h2&gt;

&lt;p&gt;Every browser sends an &lt;code&gt;Accept&lt;/code&gt; header with image requests. Current Chrome sends:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Accept: image/avif,image/webp,image/apng,image/*,*/*;q=0.8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A negotiating CDN reads that list and picks the best format it can produce. It then adds &lt;code&gt;Vary: Accept&lt;/code&gt; to the response, which tells every cache in between: "this URL has more than one version, keyed by the Accept header".&lt;/p&gt;

&lt;p&gt;If you see &lt;code&gt;Vary: Accept&lt;/code&gt; on an image response, the server is negotiating. If you don't, you're getting the same file whatever you ask for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check it with curl
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;curl -I&lt;/code&gt; sends a HEAD request and prints the headers. Add the header a browser would send:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sI&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept: image/avif,image/webp,image/*,*/*;q=0.8"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://jellinator.com/cdn/shop/files/ShotCups_100Pack.png?width=1200"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s2"&gt;"content-type|content-length|vary"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run it again without &lt;code&gt;-H&lt;/code&gt;. curl's default is &lt;code&gt;Accept: */*&lt;/code&gt;, which is exactly the "old browser" case above. If the two answers differ, your CDN negotiates.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small script for a list of URLs
&lt;/h2&gt;

&lt;p&gt;When you want to check a batch of images, this does the three requests for each URL and prints one line per client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// check-formats.mjs: what format does each image URL send to different browsers?&lt;/span&gt;
&lt;span class="c1"&gt;// Usage: node check-formats.mjs &amp;lt;image-url&amp;gt; [more urls...]&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ACCEPTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Chrome (AVIF+WebP)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/avif,image/webp,image/apng,image/*,*/*;q=0.8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;WebP only&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/webp,image/*,*/*;q=0.8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Old browser&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/*,*/*;q=0.8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ACCEPTS&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HEAD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;accept&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-length&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`  &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padEnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;padEnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; KB&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;  Vary: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;vary&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It needs Node 18 or newer (for the built-in &lt;code&gt;fetch&lt;/code&gt;). Some servers answer HEAD differently from GET; if a line looks odd, try the same URL with GET.&lt;/p&gt;

&lt;h2&gt;
  
  
  The size can change the format too
&lt;/h2&gt;

&lt;p&gt;Run the same image at a smaller width and something else happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://jellinator.com/cdn/shop/files/ShotCups_100Pack.png?width=300

  Chrome (AVIF+WebP)   image/avif     14 KB   Vary: Accept
  WebP only            image/webp     16 KB   Vary: Accept
  Old browser          image/png      98 KB   Vary: Accept
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At 300px this CDN sent AVIF to Chrome. At 1200px it sent WebP, even though Chrome said it accepts AVIF.&lt;/p&gt;

&lt;p&gt;We saw the same pattern across a larger sample. In &lt;a href="https://batchset.com/research/shopify-product-images-2026" rel="noopener noreferrer"&gt;a study of 199 Shopify product pages&lt;/a&gt;, 94% of image files came back as WebP or AVIF, and 97% of those responses carried &lt;code&gt;Vary: Accept&lt;/code&gt;. But when we spot-checked the main product photo of 24 stores at 600px, 22 came back as WebP and none as AVIF. AVIF showed up on some small variants, and not consistently.&lt;/p&gt;

&lt;p&gt;So "my CDN supports AVIF" and "my product photos are served as AVIF" are two different claims. Check the second one directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why some audits get this wrong
&lt;/h2&gt;

&lt;p&gt;Any tool that fetches your images without a browser-like &lt;code&gt;Accept&lt;/code&gt; header sees the "old browser" version. For the image above, that's a 1.2 MB PNG instead of an 87 KB WebP, so the tool reports a problem your visitors never have.&lt;/p&gt;

&lt;p&gt;When you read an image audit, check one thing: does it say which &lt;code&gt;Accept&lt;/code&gt; header it used? If it doesn't, compare a couple of its numbers with the script above before you start re-exporting files.&lt;/p&gt;

&lt;p&gt;The reverse mistake happens too. If you test only with Chrome DevTools, you see the best case. Visitors on old browsers, some email clients and some social link previews may get the original file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching: don't break Vary
&lt;/h2&gt;

&lt;p&gt;Negotiation only works if every cache between the CDN and the visitor respects &lt;code&gt;Vary: Accept&lt;/code&gt;. If you put your own proxy or a second CDN in front of your images and it ignores &lt;code&gt;Vary&lt;/code&gt;, the first visitor's format gets cached for everyone. An old client's PNG can then be served to every visitor, or an AVIF can reach a client that can't decode it.&lt;/p&gt;

&lt;p&gt;If you add a layer in front of an image CDN, either pass &lt;code&gt;Vary: Accept&lt;/code&gt; through and key the cache on it, or let the original CDN handle images directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Request your main product or hero image with a browser &lt;code&gt;Accept&lt;/code&gt; header. Note the &lt;code&gt;Content-Type&lt;/code&gt; and size.&lt;/li&gt;
&lt;li&gt;Request it again without one. If both match, the server doesn't negotiate, and the format you uploaded is the format everyone gets.&lt;/li&gt;
&lt;li&gt;Look for &lt;code&gt;Vary: Accept&lt;/code&gt; on negotiated responses.&lt;/li&gt;
&lt;li&gt;Try more than one width. The format can change with size.&lt;/li&gt;
&lt;li&gt;When an audit flags a heavy image, check which &lt;code&gt;Accept&lt;/code&gt; header it sent before trusting the number.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you'd rather scan a whole page than individual URLs, our &lt;a href="https://batchset.com/tools/website-image-auditor" rel="noopener noreferrer"&gt;website image auditor&lt;/a&gt; requests the page's images with a current browser's &lt;code&gt;Accept&lt;/code&gt; header and shows the type and size it actually got.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was drafted with an AI assistant. Every command and output above was run against the live URLs shown, on 2026-10-12.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>performance</category>
      <category>http</category>
      <category>images</category>
    </item>
    <item>
      <title>Shopify Product Images in 2026: Formats Are Mostly Solved. Layout and Alt Text Are Not.</title>
      <dc:creator>Muhaymin Bin Mehmood</dc:creator>
      <pubDate>Mon, 12 Oct 2026 09:56:27 +0000</pubDate>
      <link>https://dev.to/muhayminbinmehmood/shopify-product-images-in-2026-formats-are-mostly-solved-layout-and-alt-text-are-not-4jjc</link>
      <guid>https://dev.to/muhayminbinmehmood/shopify-product-images-in-2026-formats-are-mostly-solved-layout-and-alt-text-are-not-4jjc</guid>
      <description>&lt;p&gt;&lt;em&gt;We checked one product page on each of 199 Shopify stores and 43 WooCommerce stores, sampled from the HTTP Archive crawl of September 2026. Here is what their images look like to a modern browser.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key findings
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;On Shopify, 94% of product image files came back as WebP or AVIF.&lt;/strong&gt; We asked for images the way a current browser does. AVIF showed up on 60% of pages (95% CI 53 to 66%).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WooCommerce is the opposite.&lt;/strong&gt; 82% of image files on WooCommerce product pages came back as JPEG or PNG, and on 78% of pages the first-loaded image was JPEG or PNG (CI 63 to 88%, smaller sample of 43).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;43% of Shopify product pages have at least one first-loaded image with no &lt;code&gt;width&lt;/code&gt; or &lt;code&gt;height&lt;/code&gt; attribute&lt;/strong&gt; (CI 36 to 50%). That is a layout-shift risk unless the theme reserves the space in CSS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1 in 5 Shopify product pages has a first-loaded image with no &lt;code&gt;alt&lt;/code&gt; attribute at all&lt;/strong&gt; (19.5%, CI 15 to 26%).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heavy files are rare.&lt;/strong&gt; The median image file on a Shopify product page is 42 KB. Only 3% of pages had a first-loaded image over 1 MB.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What we measured
&lt;/h2&gt;

&lt;p&gt;For each store we picked one real product page and read its HTML the way a crawler sees it, without running JavaScript. For every image the HTML references, we asked the server for the file with the same &lt;code&gt;Accept&lt;/code&gt; header a current Chrome sends (&lt;code&gt;image/avif,image/webp,...&lt;/code&gt;), and recorded what came back: the format (from the &lt;code&gt;Content-Type&lt;/code&gt; header) and the size (from &lt;code&gt;Content-Length&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Two kinds of numbers appear below:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per file:&lt;/strong&gt; every image file the page references, including each size variant listed in &lt;code&gt;srcset&lt;/code&gt;. Useful for "what format do servers send".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per page:&lt;/strong&gt; whether a page has at least one problem image. For layout, alt text and heavy files we only count &lt;strong&gt;first-loaded images&lt;/strong&gt;: &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; elements with a &lt;code&gt;src&lt;/code&gt; and no &lt;code&gt;loading="lazy"&lt;/code&gt;. These are the images a browser requests first, usually the main product photo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What we did &lt;strong&gt;not&lt;/strong&gt; measure: total page weight, which image a browser actually picks from a &lt;code&gt;srcset&lt;/code&gt;, rendered image sizes, Largest Contentful Paint, images injected by JavaScript, and CSS background images. More on that in &lt;a href="https://batchset.com/research/shopify-product-images-2026#limitations" rel="noopener noreferrer"&gt;Limitations&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Image formats: Shopify has mostly solved this
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Shopify, all image files (n = 2,764)&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WebP&lt;/td&gt;
&lt;td&gt;79.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AVIF&lt;/td&gt;
&lt;td&gt;15.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JPEG&lt;/td&gt;
&lt;td&gt;4.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PNG&lt;/td&gt;
&lt;td&gt;1.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is 94% modern formats, and it does not look like 199 store owners each converted their images. 97% of those WebP and AVIF responses carried a &lt;code&gt;Vary: Accept&lt;/code&gt; header, which means the server picked the format based on what the browser said it accepts. In other words, the platform's image CDN does this for every store, whether the owner uploaded a JPEG or not.&lt;/p&gt;

&lt;p&gt;AVIF needs a caveat. In a follow-up spot check on 12 October 2026, we requested the main product image of 24 stores from the sample at 600px wide, telling the server we accept AVIF. 22 came back as WebP and 2 as JPEG; none as AVIF. AVIF only showed up on some small size variants, and not consistently. So read "AVIF on 60% of pages" as "at least one AVIF file somewhere on the page", not as stores choosing AVIF for their product photos.&lt;/p&gt;

&lt;p&gt;The gap that is left is small but real: on &lt;strong&gt;10.8% of Shopify pages (CI 7 to 16%)&lt;/strong&gt; the first-loaded image still came back as JPEG or PNG. We stored only hashes of image URLs, so we cannot say where those files are hosted.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. WooCommerce: still mostly JPEG and PNG
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;WooCommerce, all image files (n = 1,124)&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JPEG&lt;/td&gt;
&lt;td&gt;52.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PNG&lt;/td&gt;
&lt;td&gt;29.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebP&lt;/td&gt;
&lt;td&gt;16.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AVIF&lt;/td&gt;
&lt;td&gt;1.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On &lt;strong&gt;77.5% of WooCommerce pages (CI 63 to 88%)&lt;/strong&gt; the first-loaded image was JPEG or PNG, and only 1 page in 43 served any AVIF. WordPress can serve WebP, but unless a plugin or CDN negotiates the format, the browser gets whatever file was uploaded.&lt;/p&gt;

&lt;p&gt;This is a smaller sample (43 stores), so treat the exact percentages as rough. The direction is not in doubt.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Missing width and height
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;First-loaded &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; with no &lt;code&gt;width&lt;/code&gt; or &lt;code&gt;height&lt;/code&gt; attribute&lt;/th&gt;
&lt;th&gt;Pages&lt;/th&gt;
&lt;th&gt;95% CI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Shopify (195 pages with a first-loaded image)&lt;/td&gt;
&lt;td&gt;42.6%&lt;/td&gt;
&lt;td&gt;36 to 50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WooCommerce (40 pages)&lt;/td&gt;
&lt;td&gt;37.5%&lt;/td&gt;
&lt;td&gt;24 to 53%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Without &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt;, the browser does not know an image's shape until it loads, so text below it can jump. Some themes prevent that with CSS (&lt;code&gt;aspect-ratio&lt;/code&gt; or a fixed-ratio wrapper), so a missing attribute is a &lt;strong&gt;risk&lt;/strong&gt;, not proof of layout shift. We read HTML only, so we cannot tell which themes cover it in CSS.&lt;/p&gt;

&lt;p&gt;If lazy-loaded images are included, the Shopify figure rises to 55%. We keep the narrower first-loaded number as the headline, because lazy-loading markup can include &lt;code&gt;&amp;lt;source&amp;gt;&lt;/code&gt; elements that never carry these attributes.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Missing alt attributes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;First-loaded &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; with no &lt;code&gt;alt&lt;/code&gt; attribute&lt;/th&gt;
&lt;th&gt;Pages&lt;/th&gt;
&lt;th&gt;95% CI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Shopify&lt;/td&gt;
&lt;td&gt;19.5%&lt;/td&gt;
&lt;td&gt;15 to 26%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WooCommerce&lt;/td&gt;
&lt;td&gt;12.5%&lt;/td&gt;
&lt;td&gt;6 to 26%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We only counted images where the &lt;code&gt;alt&lt;/code&gt; attribute is missing entirely. &lt;code&gt;alt=""&lt;/code&gt; is the correct markup for a decorative image, so we counted it as present. Whether the existing alt text is any good is a separate question we did not try to answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. File sizes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Shopify&lt;/th&gt;
&lt;th&gt;WooCommerce&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Median image file&lt;/td&gt;
&lt;td&gt;42 KB&lt;/td&gt;
&lt;td&gt;22 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;75th percentile&lt;/td&gt;
&lt;td&gt;117 KB&lt;/td&gt;
&lt;td&gt;59 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;90th percentile&lt;/td&gt;
&lt;td&gt;271 KB&lt;/td&gt;
&lt;td&gt;134 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Files over 1 MB&lt;/td&gt;
&lt;td&gt;1.2%&lt;/td&gt;
&lt;td&gt;0.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pages with a first-loaded image over 1 MB&lt;/td&gt;
&lt;td&gt;3.1%&lt;/td&gt;
&lt;td&gt;2.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are sizes of individual files, including every size variant a page lists in &lt;code&gt;srcset&lt;/code&gt;. A browser on a phone will not download the biggest variants, so do not add these up into a "page weight".&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do with this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On Shopify&lt;/strong&gt;, format is not your problem. Check your theme's main product image for &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; attributes, and make sure product photos have alt text. Both are template fixes, done once. Shopify's own &lt;a href="https://shopify.dev/docs/storefronts/themes/best-practices/performance/prevent-image-layout-shift" rel="noopener noreferrer"&gt;theme guidance&lt;/a&gt; recommends the &lt;code&gt;image_tag&lt;/code&gt; Liquid filter, which adds both size attributes for you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On WooCommerce&lt;/strong&gt;, serving WebP or AVIF is still the biggest easy win. A CDN or plugin that negotiates the format by &lt;code&gt;Accept&lt;/code&gt; header fixes it for every image at once. Converting files before upload works too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anywhere&lt;/strong&gt;, check what your own pages actually serve before you trust a score. You can scan a page with our &lt;a href="https://batchset.com/tools/website-image-auditor" rel="noopener noreferrer"&gt;Website Image Auditor&lt;/a&gt;, or look at the &lt;code&gt;Content-Type&lt;/code&gt; of your main image in DevTools.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How the sample was built
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source:&lt;/strong&gt; the HTTP Archive crawl of 1 September 2026 (mobile, root pages), filtered to e-commerce sites using the Wappalyzer "Ecommerce" category.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Draw:&lt;/strong&gt; a deterministic, seeded draw per platform, with fixed quotas (200 Shopify, 100 WooCommerce, 100 other). Domains we had used while building the tool were excluded before the draw. Failed stores were replaced only from a pre-ordered reserve list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collected:&lt;/strong&gt; 22 September 2026. One representative product page per store, chosen by a fixed ranking and confirmed to be a real product page (not a category or listing).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not completed:&lt;/strong&gt; the WooCommerce stratum stopped at 50 of 100 and the "other platforms" stratum was not collected. We report Shopify and WooCommerce separately and never combine them. Shopify is about 19% of e-commerce sites in the source crawl, so none of this describes "all online stores".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;robots.txt:&lt;/strong&gt; after the crawl we found that our collector's robots.txt parser was not fully standards-compliant. We re-checked every recoverable request against each site's robots.txt with a compliant parser and removed every store where the check failed or could not be completed, plus one site that turned out not to be a store. Details and counts are on the &lt;a href="https://batchset.com/research/shopify-product-images-2026#sample" rel="noopener noreferrer"&gt;full study page&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Final sample:&lt;/strong&gt; 199 Shopify and 43 WooCommerce product pages.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Static HTML only.&lt;/strong&gt; Images added by JavaScript and CSS background images are missing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Referenced, not downloaded.&lt;/strong&gt; We fetched every candidate in &lt;code&gt;srcset&lt;/code&gt;; a real browser picks one. That is why we publish per-file and per-page-presence facts, not page totals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First 40 image references per page.&lt;/strong&gt; Long galleries are cut off, and later images are not measured.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One product page per store.&lt;/strong&gt; Other templates on the same store can differ.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sizes from headers.&lt;/strong&gt; 2,790 of 2,863 Shopify sizes came from &lt;code&gt;Content-Length&lt;/code&gt; on a HEAD request, the rest from a one-byte range request. Files with no size from either were left out, never counted as zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confidence intervals&lt;/strong&gt; are 95% Wilson intervals on page-level shares. File-level shares have no interval, because files from the same page are not independent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;robots.txt checks&lt;/strong&gt; mostly used each site's file as of 12 October 2026, about three weeks after the crawl, because few archived copies existed for the crawl date.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Data
&lt;/h2&gt;

&lt;p&gt;This is a cross-post. The original, with the full sample description, FAQ and how to cite it, is on &lt;a href="https://batchset.com/research/shopify-product-images-2026" rel="noopener noreferrer"&gt;batchset.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This report was drafted with an AI assistant. Every number was computed by scripts from the collected data, and each one is traceable to them.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Find Oversized Images on Any Page With One Console Snippet</title>
      <dc:creator>Muhaymin Bin Mehmood</dc:creator>
      <pubDate>Mon, 12 Oct 2026 08:34:24 +0000</pubDate>
      <link>https://dev.to/muhayminbinmehmood/find-oversized-images-on-any-page-with-one-console-snippet-1bn6</link>
      <guid>https://dev.to/muhayminbinmehmood/find-oversized-images-on-any-page-with-one-console-snippet-1bn6</guid>
      <description>&lt;p&gt;Lighthouse will tell you to "properly size images". It won't always tell you which image, by how much, or why it matters on a phone but not on your laptop.&lt;/p&gt;

&lt;p&gt;You can get that answer yourself in about 40 lines of JavaScript, pasted into the DevTools console. No extension, no build step.&lt;/p&gt;

&lt;p&gt;The idea is simple. For every image on the page, compare:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;how many pixels the &lt;strong&gt;file&lt;/strong&gt; has (&lt;code&gt;naturalWidth&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;how many pixels the &lt;strong&gt;screen&lt;/strong&gt; can actually use for it (the rendered box, times &lt;code&gt;devicePixelRatio&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the file is three times wider than what the screen can use, the visitor downloaded and decoded roughly nine times the pixels they needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The snippet
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dpr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;devicePixelRatio&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Transfer sizes from Resource Timing, keyed by URL&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytesByUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEntriesByType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;resource&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;encodedBodySize&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;images&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentSrc&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;svg&lt;/span&gt;&lt;span class="se"&gt;(\?&lt;/span&gt;&lt;span class="sr"&gt;|#|$&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// hidden or display:none&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;complete&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;naturalWidth&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;not loaded yet (lazy?)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// How much the browser scales the file to fill the box.&lt;/span&gt;
    &lt;span class="c1"&gt;// object-fit: cover fills the box and crops, so the larger scale wins.&lt;/span&gt;
    &lt;span class="c1"&gt;// Everything else we treat as contain-like (the smaller scale wins).&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;naturalWidth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;naturalHeight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getComputedStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;objectFit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fit&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cover&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sy&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Pixels the screen can actually use, at this device pixel ratio&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;neededW&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;naturalWidth&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dpr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;extra&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;naturalWidth&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;neededW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 1 = right-sized, 2 = twice as wide as needed&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bytesByUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;naturalWidth&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;x&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;naturalHeight&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;shown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;x&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;needed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;neededW&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px wide @&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;dpr&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;x`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;extra&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;extra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;kb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;extra&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;extra&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste it into the console on any page and you get a table sorted by the worst offenders. &lt;code&gt;extra&lt;/code&gt; is the column to read. Anything around 1 is fine. Anything above 2 is worth a look.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three details that make the numbers honest
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;currentSrc&lt;/code&gt;, not &lt;code&gt;src&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;If the image uses &lt;code&gt;srcset&lt;/code&gt;, the browser picks one candidate and &lt;code&gt;img.src&lt;/code&gt; still points at the fallback. &lt;code&gt;currentSrc&lt;/code&gt; is the file that was actually downloaded, so that is the one to measure and the one to look up in Resource Timing.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;object-fit: cover&lt;/code&gt; changes the math
&lt;/h3&gt;

&lt;p&gt;I wrote about &lt;a href="https://dev.to/muhayminbinmehmood/cover-vs-contain-vs-stretch-the-image-resizing-math-frontend-developers-should-understand-58ln"&gt;cover vs contain&lt;/a&gt; before, and it matters here too. A wide hero image inside a short, wide box with &lt;code&gt;object-fit: cover&lt;/code&gt; gets scaled until it fills the box on both axes, and the overflow is cropped. So the scale is the &lt;strong&gt;larger&lt;/strong&gt; of the two ratios, not the smaller one.&lt;/p&gt;

&lt;p&gt;If you get this wrong, a cropped hero image looks massively oversized when it isn't, or the other way round.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Device pixel ratio
&lt;/h3&gt;

&lt;p&gt;On a laptop at DPR 1, a 400px box needs a 400px file. On a phone at DPR 3, the same 400px box can use 1200px. An image that looks oversized on your desktop might be exactly right on a phone.&lt;/p&gt;

&lt;p&gt;So run it at the sizes your visitors actually use. In DevTools, switch on device mode, pick a phone preset (that sets the DPR too), reload, then run the snippet again.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;kb&lt;/code&gt; can and can't tell you
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;kb&lt;/code&gt; column comes from &lt;code&gt;encodedBodySize&lt;/code&gt; in the Resource Timing API. Two catches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cross-origin images report 0&lt;/strong&gt; unless the image server sends a &lt;code&gt;Timing-Allow-Origin&lt;/code&gt; header. The snippet shows those as &lt;code&gt;hidden&lt;/code&gt;, not as 0 KB, because "the browser won't tell me" is not the same as "it's free". Many image CDNs do send the header, so you will often get real numbers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The timing buffer has a limit.&lt;/strong&gt; Browsers keep 250 resource entries by default. On a very heavy page, older entries may be missing and show up as &lt;code&gt;unknown&lt;/code&gt;. Calling &lt;code&gt;performance.setResourceTimingBufferSize(1000)&lt;/code&gt; early in the page load raises the limit, but that's for your own site, not for one you're inspecting.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Is the LCP image one of them?
&lt;/h2&gt;

&lt;p&gt;An oversized thumbnail in the footer costs bandwidth. An oversized LCP image costs you the metric Google actually looks at. This one-liner tells you which element was the Largest Contentful Paint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PerformanceObserver&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEntries&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;LCP:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ms&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;largest-contentful-paint&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;buffered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;buffered: true&lt;/code&gt; matters, because LCP has usually already happened by the time you open the console. Without it you get nothing.&lt;/p&gt;

&lt;p&gt;If the URL it prints is near the top of the &lt;code&gt;extra&lt;/code&gt; table, that is the first image to fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I found when I tested it
&lt;/h2&gt;

&lt;p&gt;I ran both snippets on the homepage of a large, well-known e-commerce brand, at a 765px-wide viewport and DPR 1:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;42 images on the page.&lt;/li&gt;
&lt;li&gt;The LCP element was the hero image: a 2560x1442 JPEG, about 559 KB, shown in a 765x235 box with &lt;code&gt;object-fit: cover&lt;/code&gt;. That's 3.3x wider than the viewport could use, which works out to around 11 times the pixels.&lt;/li&gt;
&lt;li&gt;Eight category tiles were 696x920 files shown at 184x239, so 3.8x wider than needed. Each was only 30 to 42 KB, so they hurt far less than the hero did.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is why the &lt;code&gt;kb&lt;/code&gt; column is there. &lt;code&gt;extra&lt;/code&gt; tells you an image is too big. &lt;code&gt;kb&lt;/code&gt; tells you whether it's worth your afternoon.&lt;/p&gt;

&lt;p&gt;To be fair to that site, it was a narrow viewport at DPR 1, which is the worst case for this test. At full desktop width some of these would come out much closer to right-sized. That's exactly why you should test at more than one size.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing what it finds
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Serve smaller files where the box is small.&lt;/strong&gt; &lt;code&gt;srcset&lt;/code&gt; with width descriptors plus an honest &lt;code&gt;sizes&lt;/code&gt; attribute lets the browser pick. If &lt;code&gt;sizes&lt;/code&gt; is missing, the browser assumes &lt;code&gt;100vw&lt;/code&gt; and will happily pick the largest candidate for a thumbnail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't fix the hero with lazy loading.&lt;/strong&gt; The LCP image should load early: no &lt;code&gt;loading="lazy"&lt;/code&gt;, and consider &lt;code&gt;fetchpriority="high"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resize at the source&lt;/strong&gt; when the CMS only stores one huge original. If you have a folder of product photos to bring down to a sane width, I built a &lt;a href="https://batchset.com/tools/image-resizer" rel="noopener noreferrer"&gt;free bulk image resizer&lt;/a&gt; that does it in the browser without uploading the files.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limits of a console snippet
&lt;/h2&gt;

&lt;p&gt;This only sees &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; elements. CSS &lt;code&gt;background-image&lt;/code&gt;, images inside shadow DOM and canvas drawings aren't covered. It also only sees images that have loaded, so scroll the page first if it uses lazy loading. And carousels often clone slides, so the same file can appear several times in the table.&lt;/p&gt;

&lt;p&gt;If you want this as a report instead of a console table, with transfer sizes measured from the server side and a list of fixes, I also built a &lt;a href="https://batchset.com/tools/website-image-auditor" rel="noopener noreferrer"&gt;website image auditor&lt;/a&gt; that scans a public page. The snippet above is the quick version you can run anywhere, including on staging and behind a login.&lt;/p&gt;

&lt;p&gt;Either way, start with the LCP image. It's usually one file, and it's usually the one that matters.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was drafted with an AI assistant. Both snippets were run in a real browser before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>performance</category>
      <category>images</category>
    </item>
    <item>
      <title>A Watermark That Fits One Photo Can Fail a Whole Batch: Design Placement as a Workflow</title>
      <dc:creator>Muhaymin Bin Mehmood</dc:creator>
      <pubDate>Sat, 10 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/muhayminbinmehmood/a-watermark-that-fits-one-photo-can-fail-a-whole-batch-design-placement-as-a-workflow-3e04</link>
      <guid>https://dev.to/muhayminbinmehmood/a-watermark-that-fits-one-photo-can-fail-a-whole-batch-design-placement-as-a-workflow-3e04</guid>
      <description>&lt;p&gt;A watermark looks balanced on a landscape photo. Apply the same settings to a portrait photo and it covers the product. Apply them to a small thumbnail and the text becomes unreadable.&lt;/p&gt;

&lt;p&gt;The problem is often the placement policy, not the logo.&lt;/p&gt;

&lt;p&gt;A batch watermark feature needs more than a text field and an opacity slider. It needs a clear purpose, a coordinate system that survives different dimensions, and a review step that catches cases a shared setting cannot handle.&lt;/p&gt;

&lt;p&gt;This article develops a reference workflow for visible watermarks. It is not a description of BatchSet's internal implementation, and it does not cover invisible watermarking or forensic identification.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Decide what the visible mark should do
&lt;/h2&gt;

&lt;p&gt;“Add a watermark” can mean several different things:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Likely design priority&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Brand attribution&lt;/td&gt;
&lt;td&gt;Readable identity without hiding the subject&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A draft or proof label&lt;/td&gt;
&lt;td&gt;An unmistakable status that survives a casual glance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A product-photo preview&lt;/td&gt;
&lt;td&gt;A useful view of the item with a visible mark&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Internal review identification&lt;/td&gt;
&lt;td&gt;A clear reference label or version&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Choose the purpose before tuning opacity.&lt;/p&gt;

&lt;p&gt;A subtle corner logo and a repeated “DRAFT” pattern solve different communication problems. Neither should be described as making a file impossible to copy or alter.&lt;/p&gt;

&lt;p&gt;For an attribution workflow, my acceptance criteria would include the logo remaining readable at the expected viewing size, important product details remaining visible, and the final crop retaining the mark.&lt;/p&gt;

&lt;p&gt;That last condition is easy to miss. The un-cropped export may look correct even though the receiving interface crops the corner away.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Keep clean sources and labeled derivatives
&lt;/h2&gt;

&lt;p&gt;A visible raster watermark becomes part of the exported pixels. Keep an untouched master so another destination can receive a clean image or a different mark.&lt;/p&gt;

&lt;p&gt;A simple output naming scheme might distinguish:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;product-042-master.png
product-042-preview-watermarked.jpg
product-042-approved-clean.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are workflow labels, not a recommendation to send all three files to every recipient.&lt;/p&gt;

&lt;p&gt;Avoid reopening the watermarked JPEG, moving the mark, and encoding again as the normal editing path. Generate each derivative from the clean source and the approved watermark asset.&lt;/p&gt;

&lt;p&gt;Keep the watermark asset version with your export settings. Otherwise, a repeated job can produce different branding even when the placement values are unchanged.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Choose coordinates relative to the output
&lt;/h2&gt;

&lt;p&gt;Fixed pixels are easy to implement and easy to misuse.&lt;/p&gt;

&lt;p&gt;A 200-pixel-wide logo occupies 20% of a 1000-pixel image and 5% of a 4000-pixel image. A 24-pixel inset similarly becomes much less visible on a larger export.&lt;/p&gt;

&lt;p&gt;Relative coordinates make the policy easier to explain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mark width: a fraction of output width.&lt;/li&gt;
&lt;li&gt;Edge inset: a fraction of the shorter output side.&lt;/li&gt;
&lt;li&gt;Placement: a named anchor, such as bottom-right.&lt;/li&gt;
&lt;li&gt;Shape: preserve the watermark asset's aspect ratio.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Relative placement is still a default, not an automatic understanding of the image's subject. A corner may contain a product label or a person's face. Include per-image review where that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Calculate a bounded placement box
&lt;/h2&gt;

&lt;p&gt;The following reference helper positions a logo at the bottom-right. It also limits height so a very tall watermark does not consume the entire image.&lt;/p&gt;

&lt;p&gt;The numbers are illustrative defaults for a workflow, not universal design rules.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;watermarkBox&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;outputWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;outputHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;markWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;markHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;widthRatio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;insetRatio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.03&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;maxHeightRatio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;outputWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outputHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;markWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;markHeight&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RangeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Dimensions must be positive finite numbers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;widthRatio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;widthRatio&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;widthRatio&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;maxHeightRatio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;maxHeightRatio&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;maxHeightRatio&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;insetRatio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;insetRatio&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;insetRatio&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RangeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid placement ratios&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outputWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outputHeight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;insetRatio&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;availableWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;outputWidth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;inset&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;availableHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;outputHeight&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;inset&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;outputWidth&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;widthRatio&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;markWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;outputHeight&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;maxHeightRatio&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;markHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;availableWidth&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;markWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;availableHeight&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;markHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;markWidth&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;markHeight&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;outputWidth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;inset&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;outputHeight&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;inset&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This function computes a box. It does not detect faces, evaluate contrast, enforce a minimum readable logo size, or predict a platform's crop.&lt;/p&gt;

&lt;p&gt;It permits upscaling. For a raster logo, you may choose a separate policy that rejects excessive enlargement or uses a higher-resolution asset. A large blurry logo can satisfy the geometry and still fail the design.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Keep watermark opacity local to the watermark
&lt;/h2&gt;

&lt;p&gt;Canvas drawing state can leak between operations if you change it and forget to restore it.&lt;/p&gt;

&lt;p&gt;MDN documents &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalAlpha" rel="noopener noreferrer"&gt;globalAlpha&lt;/a&gt;, while &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/save" rel="noopener noreferrer"&gt;save&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/restore" rel="noopener noreferrer"&gt;restore&lt;/a&gt; provide a state stack.&lt;/p&gt;

&lt;p&gt;For a context whose base image has already been drawn and whose transform is set to output-pixel coordinates, isolate temporary opacity like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;drawWatermark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;decodedMark&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.35&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;opacity&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;opacity&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RangeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Opacity must be between 0 and 1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;globalCompositeOperation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;source-over&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;globalAlpha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decodedMark&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;restore&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The placement box should come from validated dimensions, such as the helper above. The drawing context must not have an unintended clip or transform; saving it does not clear an existing clip.&lt;/p&gt;

&lt;p&gt;The mark's own alpha and the drawing opacity both affect the visible result. An asset whose pixels are already translucent can look fainter than an opaque asset at the same global opacity setting.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;finally&lt;/code&gt; block restores state even if drawing throws. It does not undo pixels already drawn or restore the original image. To retry with different settings, redraw the derivative from the clean source.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Place the mark after the intended crop and resize
&lt;/h2&gt;

&lt;p&gt;For a typical destination-specific preview, I would use this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Decode the clean source and the watermark asset.&lt;/li&gt;
&lt;li&gt;Calculate the intended crop, resize, and final canvas dimensions.&lt;/li&gt;
&lt;li&gt;Fill the output canvas with the approved background if it must be opaque.&lt;/li&gt;
&lt;li&gt;Draw the cropped and resized base image over that background.&lt;/li&gt;
&lt;li&gt;Composite the watermark at the final output dimensions.&lt;/li&gt;
&lt;li&gt;Encode and reopen the actual export for review.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The advantage of marking at final dimensions is that the selected relative size is visible in the export. Marking a very large source and then shrinking it can make the logo unnecessarily hard to evaluate.&lt;/p&gt;

&lt;p&gt;If a receiving site crops after upload, your final dimensions are not the last transformation. Preview representative crops or choose an inset that accommodates the actual destination. There is no universal safe zone for every placement.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Test contrast on difficult images
&lt;/h2&gt;

&lt;p&gt;One opacity value cannot make a white logo equally readable over every photograph.&lt;/p&gt;

&lt;p&gt;Review light images, dark images, busy textures, and subjects that occupy the chosen corner. Include portrait, landscape, square, and unusually narrow exports.&lt;/p&gt;

&lt;p&gt;Possible design responses include a darker mark variant, a light mark variant, a restrained backing plate, a different anchor, or an image-specific override. Each changes the appearance and should be deliberate.&lt;/p&gt;

&lt;p&gt;Do not infer success from the full-size preview alone. A mark readable at 1600 pixels may vanish in a 160-pixel product thumbnail. Inspect both the intended display size and a larger detail view.&lt;/p&gt;

&lt;p&gt;If the mark is a text label, use a font-loading policy that prevents export before the selected typeface is ready. Otherwise, a preview and download can disagree even when the text and coordinates match.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Make batch approval a visible step
&lt;/h2&gt;

&lt;p&gt;For a proposed batch interface, I would separate applying a shared watermark from approving every result.&lt;/p&gt;

&lt;p&gt;Show representative previews early, then let the user inspect individual outputs and move the mark where it obscures useful content. Record those overrides against stable item IDs rather than filenames alone.&lt;/p&gt;

&lt;p&gt;An export summary should include output count, watermark asset or text version, shared placement settings, and which items used overrides. This makes a later rerun easier to reproduce.&lt;/p&gt;

&lt;p&gt;Export failures still need explicit handling. A watermark preview is not evidence that the final encoder produced a downloadable file. Check the output format, bytes, and successful item count before offering the package.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add the mark with a clear output policy
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://batchset.com/tools/bulk-image-converter?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=engineering-workflows-3&amp;amp;utm_content=watermark-workflow" rel="noopener noreferrer"&gt;BatchSet's Bulk Image Converter&lt;/a&gt; provides browser-based image conversion and batch ZIP export. Its current advanced options list watermarking as a Pro feature; the ordinary free conversion workflow does not add a watermark.&lt;/p&gt;

&lt;p&gt;When using a watermark workflow, keep your clean sources and inspect the final exported images at their destination size. Consistency means applying the same intention across a batch, with room for the images that need a different placement.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>design</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>A ZIP Download Is Not Proof Your Batch Finished: Designing Reliable Browser Exports</title>
      <dc:creator>Muhaymin Bin Mehmood</dc:creator>
      <pubDate>Fri, 09 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/muhayminbinmehmood/a-zip-download-is-not-proof-your-batch-finished-designing-reliable-browser-exports-pke</link>
      <guid>https://dev.to/muhayminbinmehmood/a-zip-download-is-not-proof-your-batch-finished-designing-reliable-browser-exports-pke</guid>
      <description>&lt;p&gt;You select fifty product images, click Convert, and receive a ZIP.&lt;/p&gt;

&lt;p&gt;The download looks successful. After extraction, there are only forty-eight images. Two inputs had the same basename, and the export code reused their output path.&lt;/p&gt;

&lt;p&gt;Or perhaps two conversions failed and the interface quietly archived everything else.&lt;/p&gt;

&lt;p&gt;These are different failures, but they create the same experience: a complete-looking delivery that does not account for the user's original request.&lt;/p&gt;

&lt;p&gt;An archive is a container. It does not establish that every input produced a valid result, that filenames stayed distinct, or that the browser saved the download successfully.&lt;/p&gt;

&lt;p&gt;Here is a reference design for a browser batch export whose result can be checked. The proposed manifest and code below are engineering examples, not claims about features currently implemented in BatchSet.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Separate conversion, packaging, and delivery
&lt;/h2&gt;

&lt;p&gt;A single “Done” state often hides several stages:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Useful completion condition&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Conversion&lt;/td&gt;
&lt;td&gt;Every selected item has a settled success or failure status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Packaging&lt;/td&gt;
&lt;td&gt;The expected successful outputs were added and archive generation resolved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delivery&lt;/td&gt;
&lt;td&gt;A download was initiated or an explicit file-writing operation completed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recipient verification&lt;/td&gt;
&lt;td&gt;The archive opens and its contents match expectations&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a conventional browser download, the application should not assume that triggering an anchor proves a file was saved. The user can cancel, a browser can block the action, or storage can become unavailable.&lt;/p&gt;

&lt;p&gt;Use language that matches what the application knows. “Archive ready” and “Download started” are useful states even when the application cannot verify the final disk write.&lt;/p&gt;

&lt;p&gt;For conversion progress, count settled items and show how many succeeded. If packaging then takes several seconds, label that stage instead of leaving the interface at a mysterious 100%.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Give each input a stable identity
&lt;/h2&gt;

&lt;p&gt;Filenames are not unique identifiers.&lt;/p&gt;

&lt;p&gt;These can all appear in one batch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;camera-a/product.jpg
camera-b/product.jpg
product.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Converting each to &lt;code&gt;product.webp&lt;/code&gt; creates a collision even if every conversion succeeds.&lt;/p&gt;

&lt;p&gt;Assign an item ID and preserve the user's input order before starting asynchronous work. Store results against that identity, not whichever filename or completion index happens to be convenient.&lt;/p&gt;

&lt;p&gt;A proposed result record might include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"item-002"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputIndex"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"product.jpg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"outputName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0002-product.webp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"outputBytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;184230&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bytes are illustrative. In a real export, read them from the produced Blob.&lt;/p&gt;

&lt;p&gt;Keeping identity separate from display naming also helps with retries. A replacement result can update the same item without accidentally counting it as another input.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Choose output names before generating the archive
&lt;/h2&gt;

&lt;p&gt;Some archive APIs treat a name as a path. A slash can create directories; duplicate names can replace earlier entries. JSZip documents its entry behavior in the &lt;a href="https://stuk.github.io/jszip/documentation/api_jszip/file_data.html" rel="noopener noreferrer"&gt;file API&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For a simple flat export, generate names yourself rather than accepting arbitrary paths from the input.&lt;/p&gt;

&lt;p&gt;This example uses a unique input index, a conservative ASCII stem, and an extension selected from the known output MIME type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;archiveImageName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inputIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outputType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isSafeInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputIndex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;inputIndex&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RangeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid input index&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;extensions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/jpeg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jpg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/webp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;webp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;extension&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;extensions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outputType&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;extension&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unsupported output type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;leaf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputName&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[\\/]&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;leaf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.[^&lt;/span&gt;&lt;span class="sr"&gt;.&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;*$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NFKD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;a-zA-Z0-9_-&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^-+|-+$/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputIndex&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;stem&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;extension&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This policy intentionally sacrifices some filename fidelity for predictability. The original name can remain in a separate manifest. Unique indices prevent collisions even when different names normalize to the same stem.&lt;/p&gt;

&lt;p&gt;It is a naming policy for application-generated exports, not an archive-extraction security system. It does not prove compatibility with every filesystem or third-party uploader. Confirm your destination's naming rules.&lt;/p&gt;

&lt;p&gt;Only use a verified output type. An extension derived from the requested format can mislabel an encoder fallback.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Decide whether partial export is allowed
&lt;/h2&gt;

&lt;p&gt;There are two useful policies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Complete batch required:&lt;/strong&gt; block packaging until every item succeeds or the user resolves failures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partial export allowed:&lt;/strong&gt; offer successful files with a clearly visible list of failures and a partial label.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither should be implicit.&lt;/p&gt;

&lt;p&gt;For a product catalog, missing images may create broken listings. For an informal cleanup task, exporting the successful files and retrying the rest might save time.&lt;/p&gt;

&lt;p&gt;If partial export is allowed, explain the count before download:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Partial export: 48 of 50 images succeeded. Two failed inputs are listed in the report.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Use actionable failure categories where possible: unsupported input, decoding failure, export failure, or cancellation. Avoid turning every exception into a generic “something went wrong.”&lt;/p&gt;

&lt;p&gt;Do not mark a still-pending input as failed just to make the counts add up. Packaging should begin only after results have settled, or after cancellation has been handled according to an explicit policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Put accountability inside the package
&lt;/h2&gt;

&lt;p&gt;A UI warning disappears when a ZIP is forwarded to another person. A small manifest can travel with the files.&lt;/p&gt;

&lt;p&gt;For the proposed workflow, I would include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Selected, successful, and failed item counts.&lt;/li&gt;
&lt;li&gt;The output settings relevant to the transformation.&lt;/li&gt;
&lt;li&gt;One record per selected input.&lt;/li&gt;
&lt;li&gt;Each successful output's archive name and measured bytes.&lt;/li&gt;
&lt;li&gt;A bounded, user-facing failure category for failed items.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid raw stack traces, temporary object URLs, or source download URLs with tokens. The manifest is part of the deliverable and may be shared beyond the original user.&lt;/p&gt;

&lt;p&gt;JSON is convenient for software that checks packages. A short human-readable summary can help recipients who do not use developer tools. Choose what the workflow needs rather than adding sensitive information merely because it is available.&lt;/p&gt;

&lt;p&gt;Counts should reconcile: selected equals succeeded plus failed once all items are settled. The number of archived image entries should equal succeeded. Manifest and summary files are additional entries, so distinguish image count from total archive entry count.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Compression and packaging have separate costs
&lt;/h2&gt;

&lt;p&gt;JPEG, PNG, and WebP already use compression. ZIP is useful for packaging them together, but another compression layer does not guarantee substantial savings.&lt;/p&gt;

&lt;p&gt;For an image-heavy export, a sensible reference starting point is to store image entries without additional ZIP compression and then measure the trade-off on representative batches. Text manifests may compress well even when images do not.&lt;/p&gt;

&lt;p&gt;JSZip's &lt;a href="https://stuk.github.io/jszip/documentation/api_jszip/generate_async.html" rel="noopener noreferrer"&gt;generation options&lt;/a&gt; describe archive output types and compression choices. Its &lt;a href="https://stuk.github.io/jszip/documentation/limitations.html" rel="noopener noreferrer"&gt;limitations&lt;/a&gt; also explain that &lt;code&gt;generateAsync&lt;/code&gt; retains the generated result in memory.&lt;/p&gt;

&lt;p&gt;That means “the conversions finished” does not establish that packaging will fit within the browser's remaining memory. Completed output Blobs, decoded resources that were not released, and the archive result can overlap.&lt;/p&gt;

&lt;p&gt;Treat large-batch packaging as its own resource decision. Release unnecessary previews, avoid base64 copies, consider smaller groups, and investigate a suitable streaming architecture where the expected workload justifies it.&lt;/p&gt;

&lt;p&gt;An option named &lt;code&gt;streamFiles&lt;/code&gt; is not, by itself, a promise that the final Blob is written directly to disk without accumulation. Read the complete library behavior and test the actual path you ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Freeze an export snapshot
&lt;/h2&gt;

&lt;p&gt;If the user edits output settings while packaging runs, which settings should the archive use?&lt;/p&gt;

&lt;p&gt;My proposed rule is to package an immutable snapshot of settled results. Changes after that snapshot start a new export rather than mutating the current one halfway through.&lt;/p&gt;

&lt;p&gt;This prevents a package containing early files from one format and late files from another unless mixed output is an explicit feature.&lt;/p&gt;

&lt;p&gt;Retain the snapshot identity for retries of packaging. A ZIP generation failure should not require recompressing every image if valid outputs are still available. Conversely, do not reuse stale outputs after settings change.&lt;/p&gt;

&lt;p&gt;Allow a user to dismiss progress without implying that a running operation has stopped. Cancellation behavior must match the library and worker capabilities actually available.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Verify the artifact, not only the callback
&lt;/h2&gt;

&lt;p&gt;For release testing, use batches that include duplicate basenames, mixed extensions, non-ASCII names, one corrupt input, and an intentionally oversized workload.&lt;/p&gt;

&lt;p&gt;Then check:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The chosen complete-or-partial policy is visible.&lt;/li&gt;
&lt;li&gt;Every selected item is accounted for.&lt;/li&gt;
&lt;li&gt;Output names are distinct and use correct extensions.&lt;/li&gt;
&lt;li&gt;The archive extracts in representative recipient software.&lt;/li&gt;
&lt;li&gt;Extracted files decode and have expected dimensions.&lt;/li&gt;
&lt;li&gt;Manifest byte counts match extracted file sizes.&lt;/li&gt;
&lt;li&gt;Retrying packaging does not create missing or duplicated items.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A hash can additionally check that extracted bytes match the intended bytes. It does not establish that an image looks correct or that the image belongs to the right product. Those need other checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use the ZIP as a delivery container
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://batchset.com/tools/bulk-image-converter?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=engineering-workflows-3&amp;amp;utm_content=reliable-zip-export" rel="noopener noreferrer"&gt;BatchSet's Bulk Image Converter&lt;/a&gt; offers browser-based conversion of mixed image inputs and a ZIP download.&lt;/p&gt;

&lt;p&gt;When preparing a batch for handoff, check its successful count and inspect the extracted results before replacing your originals. A convenient container becomes a reliable delivery only when its contents match the work you intended to finish.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>ux</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Your Transparent PNG Needs a Background Before It Becomes a JPEG</title>
      <dc:creator>Muhaymin Bin Mehmood</dc:creator>
      <pubDate>Thu, 08 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/muhayminbinmehmood/your-transparent-png-needs-a-background-before-it-becomes-a-jpeg-30b3</link>
      <guid>https://dev.to/muhayminbinmehmood/your-transparent-png-needs-a-background-before-it-becomes-a-jpeg-30b3</guid>
      <description>&lt;p&gt;A product cutout looks perfect over a checkerboard preview. After conversion to JPEG, the surrounding area becomes a solid rectangle. On a dark website, that rectangle is suddenly obvious.&lt;/p&gt;

&lt;p&gt;The export may be technically valid. The workflow still failed to explain an important decision: which background replaces the transparent parts of the image?&lt;/p&gt;

&lt;p&gt;Transparency is part of the image's composition, not a cosmetic detail that can be ignored when choosing a format. Converting to JPEG means committing to an opaque result.&lt;/p&gt;

&lt;p&gt;Here is a practical way to make that commitment deliberate, whether you are building an image tool or preparing assets for a storefront.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A checkerboard is usually a preview background
&lt;/h2&gt;

&lt;p&gt;A transparency preview often places the image over alternating gray squares. Those squares help you see where the image has no opaque content. They are not necessarily stored in the image itself.&lt;/p&gt;

&lt;p&gt;That distinction matters when someone asks to “keep the checkerboard” or assumes the file already has a gray background.&lt;/p&gt;

&lt;p&gt;In a raster image with an alpha channel, pixels can be opaque, transparent, or partly transparent. Partly transparent pixels are common around anti-aliased edges and soft shadows.&lt;/p&gt;

&lt;p&gt;MDN's &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats/Image_types" rel="noopener noreferrer"&gt;image-format guide&lt;/a&gt; documents transparency support in formats such as PNG and WebP. Ordinary JPEG does not preserve an alpha channel.&lt;/p&gt;

&lt;p&gt;Changing a filename from &lt;code&gt;.png&lt;/code&gt; to &lt;code&gt;.jpg&lt;/code&gt; does not perform this conversion. An actual encoder must create JPEG bytes, and the resulting image must have an opaque appearance.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Select the destination before selecting the background
&lt;/h2&gt;

&lt;p&gt;Different destinations can require different decisions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Destination&lt;/th&gt;
&lt;th&gt;Useful starting decision&lt;/th&gt;
&lt;th&gt;What to inspect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A product card on a white page&lt;/td&gt;
&lt;td&gt;Flatten onto the agreed white&lt;/td&gt;
&lt;td&gt;Product edges and light shadows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A colored campaign banner&lt;/td&gt;
&lt;td&gt;Flatten onto the banner's exact background&lt;/td&gt;
&lt;td&gt;Seams around the image rectangle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A logo used across light and dark themes&lt;/td&gt;
&lt;td&gt;Preserve transparency if supported&lt;/td&gt;
&lt;td&gt;Contrast in both themes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A receiving system that only accepts JPEG&lt;/td&gt;
&lt;td&gt;Choose and preview an opaque matte&lt;/td&gt;
&lt;td&gt;Final uploaded appearance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;White is a common background, but it is still a choice. If the page uses a warm off-white and the image is flattened onto pure white, the rectangular boundary may remain visible.&lt;/p&gt;

&lt;p&gt;For assets intended for many backgrounds, retaining transparency is often more flexible. Confirm that the recipient accepts the format and that the rest of the workflow preserves alpha.&lt;/p&gt;

&lt;p&gt;For a known opaque destination, flattening can be appropriate. The question is whether the chosen matte matches the intended use, rather than whether transparent pixels can somehow survive a JPEG export.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Flattening is a composition operation
&lt;/h2&gt;

&lt;p&gt;For a partly transparent source pixel over an opaque background, a simplified channel calculation is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;result = alpha × foreground + (1 − alpha) × background
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suppose alpha is 0.5, the foreground channel is 40, and the background channel is 240. The simplified result is 140.&lt;/p&gt;

&lt;p&gt;That example explains why changing the background also changes the appearance of edge pixels. It is not a specification of every color-managed rendering pipeline; color spaces and implementation details matter in precise work.&lt;/p&gt;

&lt;p&gt;A soft shadow is similarly blended into the chosen background. After flattening, the exported pixels do not retain the original alpha values needed to recomposite that shadow over a different color.&lt;/p&gt;

&lt;p&gt;The opaque result should therefore be treated as a destination-specific derivative. Keep the transparent original if you may need another version later.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Make the drawing order explicit
&lt;/h2&gt;

&lt;p&gt;For a simple canvas export, the sequence is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create an output canvas at the desired dimensions.&lt;/li&gt;
&lt;li&gt;Fill it with the chosen opaque background.&lt;/li&gt;
&lt;li&gt;Draw the decoded source over that fill.&lt;/li&gt;
&lt;li&gt;Encode as JPEG.&lt;/li&gt;
&lt;li&gt;Check that encoding produced a nonempty JPEG Blob.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is a narrow reference implementation. It expects a valid decoded &lt;code&gt;ImageBitmap&lt;/code&gt;, uses its current dimensions, and allows only an opaque six-digit hex background color.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;flattenBitmapToJPEG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bitmap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;background&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#ffffff&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/^#&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;0-9a-f&lt;/span&gt;&lt;span class="se"&gt;]{6}&lt;/span&gt;&lt;span class="sr"&gt;$/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;background&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Use an opaque #RRGGBB background&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RangeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Quality must be between 0 and 1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bitmap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;bitmap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bitmap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;bitmap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RangeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bitmap dimensions must be positive integers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;canvas&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bitmap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bitmap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2d&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Canvas 2D is unavailable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;background&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillRect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bitmap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBlob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;JPEG export failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/jpeg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/jpeg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Unexpected export type: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The relevant browser APIs are documented in &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillRect" rel="noopener noreferrer"&gt;fillRect&lt;/a&gt;, &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage" rel="noopener noreferrer"&gt;drawImage&lt;/a&gt;, and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob" rel="noopener noreferrer"&gt;toBlob&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This example is not BatchSet's implementation. It does not handle decoding, resampling, animation, orientation policy, wide-gamut fidelity, or resource limits. The caller owns the bitmap and should close it when no longer needed.&lt;/p&gt;

&lt;p&gt;Creating a new canvas also avoids inheriting an earlier drawing operation's transform or opacity. In a reused canvas pipeline, those states must be controlled deliberately.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Do not let global opacity affect the base image
&lt;/h2&gt;

&lt;p&gt;A tool that draws several layers can accidentally carry state from one operation into the next.&lt;/p&gt;

&lt;p&gt;Imagine applying opacity 0.3 to a watermark, then drawing the next product image without restoring the context. Its transparent edge and even its opaque center can blend differently than intended.&lt;/p&gt;

&lt;p&gt;Treat each drawing stage as a defined operation with an expected state. Where a shared context is necessary, save the state before temporary changes and restore it afterward. The next layer should not depend on whether a previous image happened to use a watermark.&lt;/p&gt;

&lt;p&gt;This is also why a preview must use the same composition policy as export. A preview that draws on white while export uses another matte creates an avoidable mismatch.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. A halo can already exist in the source
&lt;/h2&gt;

&lt;p&gt;Flattening does not guarantee perfect edges.&lt;/p&gt;

&lt;p&gt;A cutout may contain partly transparent boundary pixels whose color was influenced by a previous background. It can look acceptable on white and show a light fringe on dark green.&lt;/p&gt;

&lt;p&gt;If a fringe exists, distinguish its likely cause before changing settings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does it appear when the original transparent file is shown over the destination color?&lt;/li&gt;
&lt;li&gt;Does it appear only after resizing?&lt;/li&gt;
&lt;li&gt;Does it become visible only after lossy encoding?&lt;/li&gt;
&lt;li&gt;Is the source itself a screenshot of a transparency preview?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each points to a different stage. Increasing JPEG quality will not reliably repair an incorrectly matted source edge. Changing the background will not restore detail that was already removed from a poor cutout.&lt;/p&gt;

&lt;p&gt;Review the original alpha-bearing file on several solid backgrounds. If the problem is present before export, return to the cutout or editing stage rather than repeatedly re-encoding the derivative.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Keep the preview and the downloadable file comparable
&lt;/h2&gt;

&lt;p&gt;For an export interface, I would show an explicit background control and preview the opaque result before download.&lt;/p&gt;

&lt;p&gt;The preview should explain whether it is showing original transparency or the proposed JPEG composite. A checkerboard can be useful for source inspection but misleading as the only preview of an opaque export.&lt;/p&gt;

&lt;p&gt;After encoding, reopen the actual output for the final preview where practical. That catches differences introduced by the encoder rather than showing only the uncompressed canvas.&lt;/p&gt;

&lt;p&gt;Inspect fine edges, semi-transparent shadows, small text, and the intended page background. For batch work, include representative images with both bright and dark subjects instead of checking only one easy example.&lt;/p&gt;

&lt;p&gt;Keep source filenames and derivatives distinct. Naming a white-matted JPEG as though it were the reusable transparent master makes future editing unnecessarily confusing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Export a deliberate background
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://batchset.com/tools/png-to-jpg?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=engineering-workflows-3&amp;amp;utm_content=transparency-jpeg" rel="noopener noreferrer"&gt;BatchSet's PNG to JPG converter&lt;/a&gt; processes conversion in the browser and flattens transparent areas onto white. That is a concrete output policy to consider before choosing the tool for a destination with another background.&lt;/p&gt;

&lt;p&gt;If you need transparency, retain an alpha-capable output such as PNG or an appropriate WebP workflow. If you need JPEG, treat the background as part of the exported image and verify the final composition where it will be used.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>design</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Quality 80 Is Not a File-Size Promise: Build Image Compression Around Measured Byte Budgets</title>
      <dc:creator>Muhaymin Bin Mehmood</dc:creator>
      <pubDate>Wed, 07 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/muhayminbinmehmood/quality-80-is-not-a-file-size-promise-build-image-compression-around-measured-byte-budgets-36e2</link>
      <guid>https://dev.to/muhayminbinmehmood/quality-80-is-not-a-file-size-promise-build-image-compression-around-measured-byte-budgets-36e2</guid>
      <description>&lt;p&gt;Two product photos go through the same export settings: WebP, the same dimensions, quality 80.&lt;/p&gt;

&lt;p&gt;One fits comfortably inside an upload limit. The other does not.&lt;/p&gt;

&lt;p&gt;That result is not necessarily a broken encoder. A quality setting is an input to compression, while file size is an output you measure afterward. Image content matters, too.&lt;/p&gt;

&lt;p&gt;This becomes a product problem when an interface turns “quality 80” into a promise that every file will be under 200 KB. The user does not care which parameter the encoder received. They care whether the exported file works in the receiving system and still looks acceptable.&lt;/p&gt;

&lt;p&gt;Here is a reference workflow for treating those as two separate requirements. The examples are proposed engineering patterns, not a description of BatchSet's internal compression algorithm.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Give every number a specific meaning
&lt;/h2&gt;

&lt;p&gt;A compression interface can expose several numbers that look related but measure different things:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Number&lt;/th&gt;
&lt;th&gt;What it describes&lt;/th&gt;
&lt;th&gt;What it does not establish&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Quality setting&lt;/td&gt;
&lt;td&gt;A control passed to a particular encoder&lt;/td&gt;
&lt;td&gt;A universal visual quality percentage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output bytes&lt;/td&gt;
&lt;td&gt;The encoded file's measured size&lt;/td&gt;
&lt;td&gt;Whether fine text remains readable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel dimensions&lt;/td&gt;
&lt;td&gt;The output raster's width and height&lt;/td&gt;
&lt;td&gt;A guaranteed byte count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Percentage saved&lt;/td&gt;
&lt;td&gt;Change relative to one input file&lt;/td&gt;
&lt;td&gt;Page-load time or upload acceptance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;“80% quality” does not mean that 80% of the original information is preserved. Nor should you expect quality 0.8 in two formats or two different encoders to produce equivalent images.&lt;/p&gt;

&lt;p&gt;MDN's &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob" rel="noopener noreferrer"&gt;canvas export reference&lt;/a&gt; describes a quality argument for formats that support lossy encoding. It is an encoder option, not a target-size argument.&lt;/p&gt;

&lt;p&gt;For a byte-limited upload, the final decision must use the encoded output. A preview or an estimate cannot replace that measurement.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Write down the acceptance criteria first
&lt;/h2&gt;

&lt;p&gt;Suppose a team agrees on this illustrative requirement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The receiving system accepts WebP.&lt;/li&gt;
&lt;li&gt;Every delivered file must be at most 200,000 bytes.&lt;/li&gt;
&lt;li&gt;Product labels must remain readable at the intended display size.&lt;/li&gt;
&lt;li&gt;The first pass keeps approved dimensions unchanged.&lt;/li&gt;
&lt;li&gt;If quality adjustments cannot satisfy both conditions, request review.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are project choices, not universal web standards.&lt;/p&gt;

&lt;p&gt;Also settle whether “200 KB” means 200,000 bytes or 200 × 1024 bytes. The difference can determine whether a file just over the boundary is accepted. Internally, keep the limit as an integer byte count and make the displayed unit clear.&lt;/p&gt;

&lt;p&gt;An uploader might have other requirements: allowed MIME types, minimum dimensions, or a server that recompresses files after upload. A local byte check addresses only the local file-size condition.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Measure a bounded set of candidates
&lt;/h2&gt;

&lt;p&gt;I would begin with a small, explicit set of quality candidates rather than an unbounded loop that keeps reducing quality until something fits.&lt;/p&gt;

&lt;p&gt;The following function accepts an injected encoder. It tries higher settings first and returns the first candidate inside the budget. If none fits, it returns the smallest tested candidate with an explicit failure status.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;chooseWithinBudget&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;expectedType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;maxBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;qualities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;encode&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;expectedType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;An encoder and expected MIME type are required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isSafeInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;maxBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;maxBytes&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RangeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;maxBytes must be a positive safe integer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;qualities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;qualities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RangeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid quality candidates&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;candidates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;qualities&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;smallest&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Encoder returned no usable output&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;expectedType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Unexpected export type: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;smallest&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;smallest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;smallest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;maxBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;metTarget&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;smallest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;metTarget&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a bounded search over selected settings. It does not find the best possible image among all settings, and a higher encoder setting does not prove better perceptual quality for every use case.&lt;/p&gt;

&lt;p&gt;It also does not assume that every successive output must be smaller. An unexpected increase is still a real measurement, so the smallest tested candidate is tracked separately.&lt;/p&gt;

&lt;p&gt;The application must supply an encoder, handle its errors, and perform visual review. Cancellation, decoding, resizing, and memory management are outside this small function.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Verify the output format before assigning a filename
&lt;/h2&gt;

&lt;p&gt;An application might request WebP and then name the result &lt;code&gt;product.webp&lt;/code&gt;. That filename is only correct if the export actually produced WebP.&lt;/p&gt;

&lt;p&gt;Canvas can fall back to PNG when a requested encoding is unsupported. Handle that before putting the result into an archive or sending it to a destination that only accepts certain formats.&lt;/p&gt;

&lt;p&gt;For a canvas integration, wrap its callback-based export and reject a null result. Then verify the returned Blob's type against the requested format, as the reference function does.&lt;/p&gt;

&lt;p&gt;Checking the type reported by your own browser encoder is a useful integration check. It is not a general-purpose file validator for arbitrary untrusted uploads. Those are different boundaries.&lt;/p&gt;

&lt;p&gt;Similarly, treat an encoding error as an error. Replacing it with an empty Blob can make the byte budget look satisfied while delivering an unusable image.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Use a quality floor that matches the image's purpose
&lt;/h2&gt;

&lt;p&gt;A file can meet its byte budget and still fail its job.&lt;/p&gt;

&lt;p&gt;For a photograph, I might inspect fabric texture, hair, gradients, and edges. For a screenshot, I would inspect small text and thin interface lines. For a product label, the identifying text may be the most valuable part of the image.&lt;/p&gt;

&lt;p&gt;That suggests a review sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Compare the original and candidate at the intended display size.&lt;/li&gt;
&lt;li&gt;Inspect important details at a larger zoom.&lt;/li&gt;
&lt;li&gt;Check the output on representative destination backgrounds.&lt;/li&gt;
&lt;li&gt;Reject candidates that lose information the recipient needs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The “quality floor” is a decision about acceptable output, not merely a minimum slider value. A numerical floor can limit the search, but it still needs validation against representative images.&lt;/p&gt;

&lt;p&gt;Automated similarity metrics can help flag changes in a controlled pipeline. They do not know that a tiny serial number matters more than a large blank background unless your evaluation makes that distinction.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Change dimensions deliberately when quality alone is insufficient
&lt;/h2&gt;

&lt;p&gt;If no acceptable candidate fits, a second pass can consider smaller dimensions. Do not reduce dimensions silently and then show only a successful byte count.&lt;/p&gt;

&lt;p&gt;For example, a proposed workflow might offer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;No tested quality setting met the limit at the current dimensions. Try a smaller export, choose another accepted format, or keep the original for manual review.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Reducing both width and height to 75% produces 56.25% as many pixels. That arithmetic does not predict the encoded file size: compression still depends on image content and representation.&lt;/p&gt;

&lt;p&gt;Resize from the original working source for each candidate. Repeatedly decoding and recompressing an already lossy candidate makes it harder to compare alternatives fairly.&lt;/p&gt;

&lt;p&gt;Keep any minimum dimensions required by the recipient. A 40 KB thumbnail is not a successful replacement for a full-size image someone needs to zoom into.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Make the report explain the result
&lt;/h2&gt;

&lt;p&gt;A useful export report would show the selected format, dimensions, measured bytes, tested setting, and whether the target was met.&lt;/p&gt;

&lt;p&gt;Use literal statuses such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Within byte limit — visual review required.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Above byte limit — smallest tested candidate retained.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Encoding failed — no output produced.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These distinguish a measurement from a judgment and a failed operation from an oversized but valid file.&lt;/p&gt;

&lt;p&gt;For batch statistics, add input bytes and output bytes first, then compute the total change. Averaging each file's percentage saved gives every tiny icon the same weight as a large photograph and can tell a different story.&lt;/p&gt;

&lt;p&gt;Also show increases honestly. If a particular file grew, do not clamp its “saved” percentage to zero or quietly drop it from the report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with measured compression
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://batchset.com/tools/image-compressor?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=engineering-workflows-3&amp;amp;utm_content=compression-byte-budget" rel="noopener noreferrer"&gt;BatchSet's Bulk Image Compressor&lt;/a&gt; offers browser-based batch compression, quality controls, before-and-after sizes, and ZIP export. The free workflow uses a quality slider; target file-size mode is a Pro option.&lt;/p&gt;

&lt;p&gt;Use the exported bytes and a visual check to judge the result. A quality setting is a starting point. A usable file inside the receiving system's actual limit is the outcome to verify.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webperf</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>A Favicon Is a Tiny Deployment: Asset Design, HTML, Manifests, and Cache Debugging</title>
      <dc:creator>Muhaymin Bin Mehmood</dc:creator>
      <pubDate>Tue, 06 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/muhayminbinmehmood/a-favicon-is-a-tiny-deployment-asset-design-html-manifests-and-cache-debugging-50g6</link>
      <guid>https://dev.to/muhayminbinmehmood/a-favicon-is-a-tiny-deployment-asset-design-html-manifests-and-cache-debugging-50g6</guid>
      <description>&lt;p&gt;You replace the logo, deploy the site, and still see the old favicon in a browser tab.&lt;/p&gt;

&lt;p&gt;You regenerate the file. The old icon remains. Then someone installs the site on their phone and gets a different icon again.&lt;/p&gt;

&lt;p&gt;The problem may have nothing to do with the logo itself. A site can expose multiple icon files, multiple declarations, cached responses, and a manifest used by installation surfaces.&lt;/p&gt;

&lt;p&gt;A favicon is a small asset with a surprisingly complete delivery path: design, encoding, HTML discovery, fetching, caching, and display.&lt;/p&gt;

&lt;p&gt;Here is a framework-neutral workflow for treating it like a real deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Design for the smallest use first
&lt;/h2&gt;

&lt;p&gt;A wordmark that looks excellent at 800 pixels wide may become unreadable in a 16-pixel tab icon.&lt;/p&gt;

&lt;p&gt;Do not start by generating every output size from the full logo and assume the design problem is solved.&lt;/p&gt;

&lt;p&gt;Choose a recognizable mark: a symbol, a simplified initial, or another compact part of the identity. Look at it at actual small sizes. Thin strokes, tiny gaps, and decorative details can disappear.&lt;/p&gt;

&lt;p&gt;My suggested acceptance questions are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the mark recognizable at 16 and 32 pixels?&lt;/li&gt;
&lt;li&gt;Does it have enough internal space after resizing?&lt;/li&gt;
&lt;li&gt;Is it distinguishable on light and dark surrounding UI?&lt;/li&gt;
&lt;li&gt;Is it clearly related to the product's identity?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A larger source improves the available input quality. It does not guarantee that every small derivative has a useful silhouette.&lt;/p&gt;

&lt;p&gt;For a new product, review the tiny icon before committing to a final export package. It is easier to simplify the source once than to debug ten blurry outputs later.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Separate icon purposes
&lt;/h2&gt;

&lt;p&gt;Different browser and platform surfaces can use different declarations.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;th&gt;Intended role in this reference package&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;favicon.ico&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Conventional browser icon fallback with multiple sizes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Small PNG icons&lt;/td&gt;
&lt;td&gt;Explicit raster tab-icon options&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;apple-touch-icon.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Apple touch-icon declaration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Larger manifest icons&lt;/td&gt;
&lt;td&gt;App-installation surfaces that use the manifest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;site.webmanifest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Metadata referencing the manifest icon files&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An SVG tab icon can be another option when it fits the target browser support and your artwork. It does not eliminate the need to verify other surfaces.&lt;/p&gt;

&lt;p&gt;The HTML &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/link" rel="noopener noreferrer"&gt;&lt;code&gt;link&lt;/code&gt; element&lt;/a&gt; supplies resource relationships. Its &lt;code&gt;sizes&lt;/code&gt; and &lt;code&gt;type&lt;/code&gt; metadata help describe icon candidates; listing a size does not resize an incorrectly generated file.&lt;/p&gt;

&lt;p&gt;For app icons, the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/icons" rel="noopener noreferrer"&gt;manifest &lt;code&gt;icons&lt;/code&gt; reference&lt;/a&gt; describes the distinct metadata used there.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Use explicit paths and real dimensions
&lt;/h2&gt;

&lt;p&gt;Here is a reference HTML declaration set. Adjust the paths to the files you actually ship:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"icon"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/icons/favicon.ico"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"image/x-icon"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"icon"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/icons/favicon-32x32.png"&lt;/span&gt;
      &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"image/png"&lt;/span&gt; &lt;span class="na"&gt;sizes=&lt;/span&gt;&lt;span class="s"&gt;"32x32"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"icon"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/icons/favicon-16x16.png"&lt;/span&gt;
      &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"image/png"&lt;/span&gt; &lt;span class="na"&gt;sizes=&lt;/span&gt;&lt;span class="s"&gt;"16x16"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"apple-touch-icon"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/icons/apple-touch-icon.png"&lt;/span&gt;
      &lt;span class="na"&gt;sizes=&lt;/span&gt;&lt;span class="s"&gt;"180x180"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"manifest"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/site.webmanifest"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a template, not a universal platform checklist. Frameworks may generate their own declarations from filesystem conventions or metadata APIs.&lt;/p&gt;

&lt;p&gt;Inspect the resulting HTML rather than adding duplicate tags blindly. An inherited layout, a CMS theme, and a plugin can each contribute icon declarations.&lt;/p&gt;

&lt;p&gt;If the application is served below a path such as &lt;code&gt;/portal/&lt;/code&gt;, a root-relative &lt;code&gt;/icons/&lt;/code&gt; URL points to the origin root. Decide whether that is where your assets live. Do not assume your application base path is automatically prefixed.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel" rel="noopener noreferrer"&gt;MDN &lt;code&gt;rel&lt;/code&gt; reference&lt;/a&gt; is useful when checking the relationships you declare.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Treat the manifest as another linked document
&lt;/h2&gt;

&lt;p&gt;A minimal example might contain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example Workspace"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"short_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Workspace"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"start_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"display"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"standalone"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"icons"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/icons/app-192.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sizes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"192x192"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image/png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"purpose"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"any"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/icons/app-512.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sizes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"512x512"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image/png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"purpose"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"any"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those filenames are illustrative. Verify they exist and have the declared dimensions.&lt;/p&gt;

&lt;p&gt;A manifest is not an installability guarantee. Requirements and behavior depend on the platform and the rest of the application.&lt;/p&gt;

&lt;p&gt;Also check where relative paths resolve. A relative icon path is resolved from the manifest URL, not simply from whichever application screen you happened to be viewing.&lt;/p&gt;

&lt;p&gt;For a subpath deployment, inspect &lt;code&gt;start_url&lt;/code&gt;, scope, and icon locations together. A valid JSON file can still point users or fetches to the wrong place.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Do not label an ordinary icon maskable without checking it
&lt;/h2&gt;

&lt;p&gt;Some app surfaces apply shape masks. An icon designed to reach the corners of its square may lose important content when cropped.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;purpose&lt;/code&gt; value of &lt;code&gt;maskable&lt;/code&gt; is a promise about how the artwork should survive those treatments. It is not a request for the browser to repair the design.&lt;/p&gt;

&lt;p&gt;If you need maskable support, create and test artwork for it, and declare that specific file appropriately. Keep critical content within the required safe region and check previews using the platform's tooling.&lt;/p&gt;

&lt;p&gt;Do not claim a package provides maskable artwork merely because it includes a 512-pixel PNG. Size and maskability are different properties.&lt;/p&gt;

&lt;p&gt;This is particularly relevant for logos whose essential letter or symbol sits near an edge. Test the silhouette after the mask, not only before it.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Debug the fetched asset before blaming the cache
&lt;/h2&gt;

&lt;p&gt;A filename ending in &lt;code&gt;.png&lt;/code&gt; can still return an HTML error page. A single-page application may answer an unknown asset route with its application shell.&lt;/p&gt;

&lt;p&gt;For each declared icon, verify:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The requested URL is correct.&lt;/li&gt;
&lt;li&gt;The response succeeds.&lt;/li&gt;
&lt;li&gt;The response content type matches an actual image.&lt;/li&gt;
&lt;li&gt;The decoded dimensions match the declaration.&lt;/li&gt;
&lt;li&gt;The response body is the new artwork.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A 200 status alone is insufficient.&lt;/p&gt;

&lt;p&gt;In a browser console, this helper checks a raster asset's actual decoded dimensions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;inspectRasterIcon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no-store&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Icon request failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contentType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Expected image response, got &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bitmap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createImageBitmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bitmap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bitmap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;contentType&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bitmap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use it for a same-origin PNG or another supported raster input. It is not an ICO-frame inspector, an SVG renderer, or a guarantee that browser tab selection uses that particular candidate. Cross-origin requests also require appropriate CORS access.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Make updates observable
&lt;/h2&gt;

&lt;p&gt;If the server delivers the new bytes but the tab still shows the old icon, separate the possible layers: HTTP cache, service-worker cache, browser icon storage, an already-installed app icon, and stale HTML declarations.&lt;/p&gt;

&lt;p&gt;A versioned asset name such as &lt;code&gt;favicon-32x32-v2.png&lt;/code&gt; can make an updated candidate easier to identify. Update the declaration and ship the new file together.&lt;/p&gt;

&lt;p&gt;Versioning is not permission to delete every previous asset immediately. Cached HTML or an older app installation may still refer to it.&lt;/p&gt;

&lt;p&gt;For a simple release, I would:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deploy new versioned icon files.&lt;/li&gt;
&lt;li&gt;Update HTML and manifest references.&lt;/li&gt;
&lt;li&gt;Check the served page and manifest.&lt;/li&gt;
&lt;li&gt;Inspect service-worker behavior if present.&lt;/li&gt;
&lt;li&gt;Test a fresh browser context and a new installation separately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid treating one hard reload as proof that every installation surface has updated. An icon attached to a saved shortcut can have its own lifecycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Keep a small release checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Evidence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Small-size readability&lt;/td&gt;
&lt;td&gt;Actual-size 16 and 32 pixel previews&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTML declarations&lt;/td&gt;
&lt;td&gt;Rendered document points to intended files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Asset delivery&lt;/td&gt;
&lt;td&gt;Correct status, type, bytes, and dimensions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manifest&lt;/td&gt;
&lt;td&gt;Valid JSON with working icon URLs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment paths&lt;/td&gt;
&lt;td&gt;Correct under the production base path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Light and dark UI&lt;/td&gt;
&lt;td&gt;Recognizable mark on target surfaces&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Installation&lt;/td&gt;
&lt;td&gt;Real device or supported platform test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Update&lt;/td&gt;
&lt;td&gt;New references visible after deployment&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is a narrow icon-delivery checklist. It does not replace testing the rest of a PWA or auditing the security of SVG input in a generator.&lt;/p&gt;

&lt;p&gt;The aim is to leave no mystery about which file was generated, which file was fetched, and which surface was inspected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generate the package, then verify the deployment
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://batchset.com/tools/favicon-generator?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=engineering-workflows-2&amp;amp;utm_content=favicon-deployment" rel="noopener noreferrer"&gt;BatchSet's Favicon Generator&lt;/a&gt; generates an ICO, PNG sizes, an Apple touch icon, larger app icons, and a web manifest locally in your browser.&lt;/p&gt;

&lt;p&gt;Use a simple square brand mark, download the package, and adapt the provided declarations to your actual deployment paths. Treat generated files as a starting package, not proof that every browser or installation surface is already configured.&lt;/p&gt;

&lt;p&gt;A tiny icon deserves a small, deliberate deployment check. That is usually faster than repeatedly regenerating the logo while the wrong file keeps being served.&lt;/p&gt;

</description>
      <category>html</category>
      <category>webdev</category>
      <category>frontend</category>
      <category>design</category>
    </item>
    <item>
      <title>Combining Images into a PDF Does Not Make Them Searchable: A Practical Browser Export Workflow</title>
      <dc:creator>Muhaymin Bin Mehmood</dc:creator>
      <pubDate>Mon, 05 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/muhayminbinmehmood/combining-images-into-a-pdf-does-not-make-them-searchable-a-practical-browser-export-workflow-3b1n</link>
      <guid>https://dev.to/muhayminbinmehmood/combining-images-into-a-pdf-does-not-make-them-searchable-a-practical-browser-export-workflow-3b1n</guid>
      <description>&lt;p&gt;You photograph ten receipts, combine them into a PDF, and send the result to a colleague.&lt;/p&gt;

&lt;p&gt;The file opens. Every receipt is visible. Then your colleague searches for a supplier name and finds nothing.&lt;/p&gt;

&lt;p&gt;Nothing necessarily went wrong with the conversion. The PDF may simply contain images of text rather than actual text.&lt;/p&gt;

&lt;p&gt;An image-to-PDF tool packages visual pages. Searchability, semantic structure, and accessibility are separate capabilities.&lt;/p&gt;

&lt;p&gt;That distinction matters when you build exports for receipts, screenshots, catalogs, portfolios, and administrative documents. A successful download should not create expectations the file cannot meet.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Define the output before implementing the converter
&lt;/h2&gt;

&lt;p&gt;Start by deciding what the recipient needs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Image-only PDF can provide it?&lt;/th&gt;
&lt;th&gt;Additional work to consider&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;View several images in one file&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Page order and readable layout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Print a visual packet&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Page size, margins, and effective resolution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search text photographed on the pages&lt;/td&gt;
&lt;td&gt;Not by embedding images alone&lt;/td&gt;
&lt;td&gt;OCR and verified text output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Copy text from a receipt photo&lt;/td&gt;
&lt;td&gt;Not by embedding images alone&lt;/td&gt;
&lt;td&gt;A usable text layer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Navigate a structured document&lt;/td&gt;
&lt;td&gt;Not automatically&lt;/td&gt;
&lt;td&gt;Structure, tags, reading order, and testing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;W3C's &lt;a href="https://www.w3.org/WAI/WCAG22/Techniques/pdf/PDF7" rel="noopener noreferrer"&gt;PDF OCR technique&lt;/a&gt; addresses adding actual text to scanned documents. OCR can support search and reading, but recognized text still needs review, and OCR alone does not establish complete accessibility.&lt;/p&gt;

&lt;p&gt;For an expense packet, images may be sufficient. For a document someone must read with assistive technology, a different authoring workflow may be necessary.&lt;/p&gt;

&lt;p&gt;Make that distinction visible before export, not after the user has uploaded a finished document elsewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Page order is part of the data
&lt;/h2&gt;

&lt;p&gt;File-selection order, filename order, and the intended document order are not always the same.&lt;/p&gt;

&lt;p&gt;Consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;receipt-1.jpg
receipt-10.jpg
receipt-2.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plain lexical sorting places &lt;code&gt;receipt-10&lt;/code&gt; before &lt;code&gt;receipt-2&lt;/code&gt;. Numeric-aware filename sorting can help, but it still cannot know the intended chronology when names are inconsistent.&lt;/p&gt;

&lt;p&gt;For a proposed interface, I would show numbered previews before export and allow explicit reordering. Keep that order in a stable array instead of reconstructing it from whichever asynchronous conversion finishes first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;documentPages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;receipt-a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;firstFile&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;receipt-b&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;secondFile&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// Export this array in order; do not sort by completion time.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If one input fails, tell the user which page is missing. Do not quietly produce a nine-page document that looks like a complete ten-page packet.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Fit the image without changing its proportions
&lt;/h2&gt;

&lt;p&gt;A landscape screenshot should not be stretched to fill a portrait page. A tall receipt should not be cropped just to avoid whitespace.&lt;/p&gt;

&lt;p&gt;For a reference layout, fit the full image inside a page box while preserving its aspect ratio. PDF coordinates are commonly expressed in points; one point is 1/72 inch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fitImageToPage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;imageWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;imageHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;pageWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;pageHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;margin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;imageWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;imageHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageHeight&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RangeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Dimensions must be positive finite numbers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;margin&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RangeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid margin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;availableWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pageWidth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;availableHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pageHeight&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;availableWidth&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;availableHeight&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RangeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Margins leave no drawing area&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;availableWidth&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;imageWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;availableHeight&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;imageHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;imageWidth&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;imageHeight&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pageWidth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pageHeight&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This computes placement. It does not resample or improve the source image.&lt;/p&gt;

&lt;p&gt;For unusually tall receipts, you might offer a custom-height page or a deliberate split across multiple pages. Explain the option. Cropping off a total is not a formatting improvement.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Embedding and re-encoding are different choices
&lt;/h2&gt;

&lt;p&gt;Some PDF libraries can embed JPEG or PNG bytes directly. Others use a canvas-based path that decodes the source and creates a new encoded image first.&lt;/p&gt;

&lt;p&gt;Re-encoding can change quality and file size. Direct embedding avoids that particular conversion step, but does not solve layout, orientation, or every compatibility issue.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://pdf-lib.js.org/docs/api/classes/pdfdocument" rel="noopener noreferrer"&gt;PDF-LIB API reference&lt;/a&gt; documents image embedding and document serialization. Here is a deliberately narrow reference example using that library as an injected dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;makeImagePDF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PDFDocument&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;PDFDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/jpeg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;embedJpg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;embedPng&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Unsupported input in this example: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addPage&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;595.28&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;841.89&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;placement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fitImageToPage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;imageWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;imageHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;pageWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getWidth&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;pageHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getHeight&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;placement&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The calling application supplies &lt;code&gt;PDFDocument&lt;/code&gt; from its installed &lt;code&gt;pdf-lib&lt;/code&gt; dependency and uses the layout helper above. This is not BatchSet's implementation.&lt;/p&gt;

&lt;p&gt;The example supports only JPEG and PNG, trusts file MIME labels as routing hints, and aborts on the first error. It does not normalize orientation, accept WebP, run OCR, add document tags, or apply production resource limits. Real decoding must validate bytes rather than treating a MIME label as proof of the file's format.&lt;/p&gt;

&lt;p&gt;WebP input requires an appropriate conversion path before using these embedding methods. Do not assume every image format accepted by the browser is directly embeddable by your PDF library.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Judge resolution at the final placed size
&lt;/h2&gt;

&lt;p&gt;A 1200-pixel-wide image printed six inches wide has an effective resolution of 200 pixels per inch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1200 pixels / 6 inches = 200 pixels per inch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is an illustrative calculation, not a guarantee of readable receipt text. Source sharpness, camera focus, contrast, compression, and the actual printer still matter.&lt;/p&gt;

&lt;p&gt;Changing a DPI metadata field does not manufacture missing detail. Neither does placing a blurry photo into a higher-resolution canvas.&lt;/p&gt;

&lt;p&gt;Inspect the smallest relevant text at the intended print size. For a screenshot packet, that may be an error message. For a receipt, it may be a tax identifier or total. If those details fail, a visually pleasant thumbnail is not enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. A PDF wrapper is not a compression guarantee
&lt;/h2&gt;

&lt;p&gt;The PDF must store image content and document structures. It can be larger than the source images, depending on representation and overhead.&lt;/p&gt;

&lt;p&gt;My suggested export report includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Input image count and actual output page count.&lt;/li&gt;
&lt;li&gt;Total input encoded bytes.&lt;/li&gt;
&lt;li&gt;Final PDF bytes.&lt;/li&gt;
&lt;li&gt;Any images resized or re-encoded.&lt;/li&gt;
&lt;li&gt;A list of failures or omissions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If size reduction is required, make it a separate option and compare the actual output. Some screenshots and small images become larger when re-encoded.&lt;/p&gt;

&lt;p&gt;Avoid blanket “compress every page to JPEG” behavior. Text screenshots, line art, transparent graphics, and photographs can have different needs. Review representative pages before applying one quality setting to the whole packet.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Browser-based does not mean unlimited
&lt;/h2&gt;

&lt;p&gt;Even processing one page at a time, the PDF document object may retain embedded resources until serialization. Generating the final byte array can introduce another memory peak.&lt;/p&gt;

&lt;p&gt;For large packets, expose a realistic batch limit, provide progress, and support cancellation where the chosen library permits it. If an operation has no abort interface, explain that cancellation may stop the next page rather than instantly interrupt the current one.&lt;/p&gt;

&lt;p&gt;Keep originals available until the user has inspected the final file. Do not call a packet “complete” before export and verification finish.&lt;/p&gt;

&lt;p&gt;Local processing can keep source files away from a conversion server, but it does not prevent the user from later uploading the result to an email or document service. Privacy language should describe the actual conversion stage.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Verify what the recipient will receive
&lt;/h2&gt;

&lt;p&gt;For a release or an important packet, my checklist is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the exported PDF in more than one viewer.&lt;/li&gt;
&lt;li&gt;Confirm page count and intended order.&lt;/li&gt;
&lt;li&gt;Check orientation, margins, and uncropped edges.&lt;/li&gt;
&lt;li&gt;Zoom into small text and review a print proof if printing matters.&lt;/li&gt;
&lt;li&gt;Search a known word to determine whether a text layer exists.&lt;/li&gt;
&lt;li&gt;Compare final bytes with any receiving system's size limit.&lt;/li&gt;
&lt;li&gt;Use an appropriate accessibility workflow if that is a requirement.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Searchability testing should include checking OCR accuracy where OCR was applied. Finding one word is not evidence that every page's text is correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combine the pages with clear expectations
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://batchset.com/tools/image-to-pdf?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=engineering-workflows-2&amp;amp;utm_content=images-to-pdf" rel="noopener noreferrer"&gt;BatchSet's Image to PDF tool&lt;/a&gt; combines JPG, PNG, and WebP images into a PDF in the browser, with page-size and ordering options.&lt;/p&gt;

&lt;p&gt;Use it when you need a visual packet in one file. Do not assume image conversion adds OCR, semantic tags, or an accessibility guarantee. Verify the exported pages, and choose a text-aware document workflow when the recipient needs more than pictures of words.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>pdf</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>An Alt Attribute Is Not an Accessibility Strategy: Write Image Alternatives That Match the Context</title>
      <dc:creator>Muhaymin Bin Mehmood</dc:creator>
      <pubDate>Sun, 04 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/muhayminbinmehmood/an-alt-attribute-is-not-an-accessibility-strategy-write-image-alternatives-that-match-the-context-46d9</link>
      <guid>https://dev.to/muhayminbinmehmood/an-alt-attribute-is-not-an-accessibility-strategy-write-image-alternatives-that-match-the-context-46d9</guid>
      <description>&lt;p&gt;An automated check finds an image without an &lt;code&gt;alt&lt;/code&gt; attribute. You add &lt;code&gt;alt="image"&lt;/code&gt;, run the check again, and the warning disappears.&lt;/p&gt;

&lt;p&gt;The HTML changed. The experience barely improved.&lt;/p&gt;

&lt;p&gt;Presence is easy to check. Whether an alternative communicates the right information requires understanding why the image exists on that page.&lt;/p&gt;

&lt;p&gt;The same photo can be informative in a product gallery, redundant inside a product link with a clear label, and decorative in a background composition. A useful alternative belongs to the image's &lt;strong&gt;use&lt;/strong&gt;, not just to the image file.&lt;/p&gt;

&lt;p&gt;This article is a practical review workflow for developers and content teams. It does not claim that an image scanner can determine accessibility compliance from an attribute count.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Missing, empty, and descriptive are different states
&lt;/h2&gt;

&lt;p&gt;Consider three examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"bag.jpg"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"bag.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"bag.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Forest-green handbag with a gold clasp"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They should not be treated as interchangeable.&lt;/p&gt;

&lt;p&gt;An absent attribute leaves the text alternative unspecified. An empty value can intentionally omit a decorative image from the reading experience. A descriptive value can communicate information that matters in context.&lt;/p&gt;

&lt;p&gt;For a decorative image, adding descriptive prose can create noise rather than remove a barrier. W3C's &lt;a href="https://www.w3.org/WAI/tutorials/images/decorative/" rel="noopener noreferrer"&gt;decorative-image guidance&lt;/a&gt; explains why an empty alternative is appropriate and why leaving the attribute out is a different decision.&lt;/p&gt;

&lt;p&gt;An audit that tests only &lt;code&gt;hasAttribute("alt")&lt;/code&gt; can identify missing attributes. It cannot infer that every empty value is correct or that every nonempty value is useful.&lt;/p&gt;

&lt;p&gt;That limitation should shape the report language: “missing alternative” and “requires contextual review” are more defensible than “all images accessible.”&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Ask what the reader would lose
&lt;/h2&gt;

&lt;p&gt;Before writing anything, ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If this image were unavailable, what information or action would the reader lose?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Suppose a catalog page already says “Forest-green leather handbag” beside a photo. On that page, repeating those exact words may add little. In a detail gallery, another view might reveal the clasp, strap, or internal compartments.&lt;/p&gt;

&lt;p&gt;The alternative for that second view could communicate its distinct purpose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt;
  &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"bag-interior.jpg"&lt;/span&gt;
  &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Open handbag showing two compartments and a zipped inner pocket"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The wording should match what the image actually shows. Do not infer materials, dimensions, authenticity, or hidden features from a thumbnail.&lt;/p&gt;

&lt;p&gt;W3C's &lt;a href="https://www.w3.org/WAI/tutorials/images/informative/" rel="noopener noreferrer"&gt;informative-image tutorial&lt;/a&gt; focuses on conveying the relevant meaning, rather than mechanically describing every visible detail.&lt;/p&gt;

&lt;p&gt;My editorial rule is to identify the one reason this image deserves to be on this page. Start there. Add detail only when it helps someone make the same decision that the visual is supporting.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Functional images need a destination or action
&lt;/h2&gt;

&lt;p&gt;An image can supply the label for a link or control.&lt;/p&gt;

&lt;p&gt;This link has an alternative, but the label is not very helpful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/downloads/catalog.pdf"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"download-icon.png"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Downward arrow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important information is what activating it does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/downloads/catalog.pdf"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"download-icon.png"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Download the product catalog PDF"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the link already contains visible text, the icon can instead have an empty alternative:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/downloads/catalog.pdf"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"download-icon.png"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Download the product catalog PDF
&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are simple cases, not a complete treatment of accessible-name computation. Inspect the rendered control when labels are assembled through nested elements or ARIA attributes.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.w3.org/WAI/tutorials/images/functional/" rel="noopener noreferrer"&gt;W3C functional-image guidance&lt;/a&gt; is a good reference for deciding when the alternative should describe the action instead of the appearance.&lt;/p&gt;

&lt;p&gt;For image-only controls, include an explicit label requirement in component review. Do not let an unlabeled control pass merely because its icon is attractive.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Do not force an entire chart into one sentence
&lt;/h2&gt;

&lt;p&gt;A chart can contain a conclusion and the evidence behind it. A brief alternative may communicate the chart's identity or key message, but it often cannot carry every data point.&lt;/p&gt;

&lt;p&gt;For example, a chart comparing monthly image bytes should have a nearby textual explanation and, where useful, a data table. The visual should not be the only place the numbers exist.&lt;/p&gt;

&lt;p&gt;An illustrative structure is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;figure&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt;
    &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"monthly-image-bytes.png"&lt;/span&gt;
    &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Monthly image transfer totals; explanation and data table follow"&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;figcaption&amp;gt;&lt;/span&gt;
    Image-transfer totals for the sampled pages, January to March.
  &lt;span class="nt"&gt;&amp;lt;/figcaption&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/figure&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;The totals declined each month for this sample.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- Include the actual accessible data table here. --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The table must contain the real data and clear headings. The comment above is only a placement marker, not a substitute for it.&lt;/p&gt;

&lt;p&gt;W3C's &lt;a href="https://www.w3.org/WAI/tutorials/images/complex/" rel="noopener noreferrer"&gt;complex-image guidance&lt;/a&gt; covers alternatives that provide both a short identification and more complete information.&lt;/p&gt;

&lt;p&gt;For engineering teams, this becomes a publishing contract: whoever exports the chart also delivers its underlying data or a meaningful narrative. A designer should not have to reconstruct the numbers from pixels.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Build context into the asset workflow
&lt;/h2&gt;

&lt;p&gt;A filename such as &lt;code&gt;IMG_1042.jpg&lt;/code&gt; is a poor starting label. So is automatically turning a slug into a sentence.&lt;/p&gt;

&lt;p&gt;My suggested content record contains separate fields:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Asset identifier&lt;/td&gt;
&lt;td&gt;Locate the actual file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Page or placement&lt;/td&gt;
&lt;td&gt;Explain where it is used&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Role&lt;/td&gt;
&lt;td&gt;Informative, decorative, functional, or complex&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text alternative&lt;/td&gt;
&lt;td&gt;Communicate the necessary meaning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extended explanation&lt;/td&gt;
&lt;td&gt;Support charts or detailed instructional images&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review owner&lt;/td&gt;
&lt;td&gt;Identify who can verify the content&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A single reusable “default alt” can be a helpful draft. It should not prevent a different value at a different placement.&lt;/p&gt;

&lt;p&gt;For product galleries, use placement records to distinguish a front view, back view, detail, and interior view. Do not invent different wording merely to make strings unique. The distinction should reflect information the images actually contribute.&lt;/p&gt;

&lt;p&gt;This is a proposed content model, not a claim about BatchSet's current storage schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Treat generated descriptions as drafts
&lt;/h2&gt;

&lt;p&gt;An image-description model may identify a bag and a clasp. It does not necessarily know that the image is functioning as a download button, that adjacent text already explains it, or that the page is teaching users how to identify a manufacturing defect.&lt;/p&gt;

&lt;p&gt;The strongest review prompt starts with the intended use, not just the pixels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Placement: Product detail gallery
Purpose: Show how the inner compartments are arranged
Already stated nearby: Product name and exterior color
Required review: Confirm visible details; do not infer measurements
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That record helps a human reviewer as much as it helps a model.&lt;/p&gt;

&lt;p&gt;For a small team, I would prioritize consequential images first: controls, diagrams that explain a process, product details needed to compare choices, and screenshots that contain essential instructions.&lt;/p&gt;

&lt;p&gt;Then review repetitive decoration and redundant card images. The goal is understandable content, not the longest possible description for every asset.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Give automated checks a narrow, honest job
&lt;/h2&gt;

&lt;p&gt;In a browser console, a small inventory can collect candidates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;imageInventory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;img&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentSrc&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;hasAlt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;alt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;alt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;inLinkOrButton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;closest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a, button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageInventory&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code does not compute accessible names, examine CSS backgrounds, inspect every shadow root, or understand the image's purpose. It creates a review list from the current document.&lt;/p&gt;

&lt;p&gt;I would use three queues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Missing attributes to investigate.&lt;/li&gt;
&lt;li&gt;Empty attributes to confirm against the intended role.&lt;/li&gt;
&lt;li&gt;Existing descriptions to check for usefulness and accuracy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not automatically classify the second and third queues as failures. They contain context-dependent decisions.&lt;/p&gt;

&lt;p&gt;When a scan cannot inspect the required markup, mark the check unavailable. That is a measurement limitation, not evidence that the page passed.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Review the experience, not only the markup
&lt;/h2&gt;

&lt;p&gt;A practical acceptance pass should include:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Inspecting labels on image links and buttons.&lt;/li&gt;
&lt;li&gt;Reading product-card content in its actual order.&lt;/li&gt;
&lt;li&gt;Checking whether charts still make sense without seeing them.&lt;/li&gt;
&lt;li&gt;Verifying that decorative elements do not create repetitive announcements.&lt;/li&gt;
&lt;li&gt;Testing important flows with a screen reader and keyboard.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Images are only one part of accessibility. Focus order, interaction behavior, form labels, structure, and many other details still matter.&lt;/p&gt;

&lt;p&gt;A successful image review does not certify the whole site. It improves one specific part of the experience with evidence you can explain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with discovery, then add judgment
&lt;/h2&gt;

&lt;p&gt;If you need a starting inventory, &lt;a href="https://batchset.com/tools/website-image-auditor?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=engineering-workflows-2&amp;amp;utm_content=alt-text-context" rel="noopener noreferrer"&gt;BatchSet's Website Image Auditor&lt;/a&gt; can flag missing alt attributes among the images it analyzes when the necessary markup is available.&lt;/p&gt;

&lt;p&gt;It is an image-focused audit, not an accessibility certification. Unmeasurable images and fallback scans have coverage limitations, so review the report's analyzed set and availability indicators.&lt;/p&gt;

&lt;p&gt;Use the findings to locate work. Use the page's purpose to decide what the alternative should say.&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>html</category>
      <category>webdev</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Your QR Code Works on Screen. Will It Still Scan After Printing?</title>
      <dc:creator>Muhaymin Bin Mehmood</dc:creator>
      <pubDate>Sat, 03 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/muhayminbinmehmood/your-qr-code-works-on-screen-will-it-still-scan-after-printing-23ee</link>
      <guid>https://dev.to/muhayminbinmehmood/your-qr-code-works-on-screen-will-it-still-scan-after-printing-23ee</guid>
      <description>&lt;p&gt;A QR code scans from your laptop screen. You put it into a brochure, reduce it to fit the layout, export a PDF, and send it to a printer.&lt;/p&gt;

&lt;p&gt;That first successful scan only verified the first artifact.&lt;/p&gt;

&lt;p&gt;The printed code has different conditions: different physical dimensions, contrast, surface finish, surrounding artwork, camera distance, and possibly different pixel edges after export.&lt;/p&gt;

&lt;p&gt;For developers who generate QR codes, “the encoder returned an image” is not the same acceptance condition as “the final customer can open the correct destination.”&lt;/p&gt;

&lt;p&gt;Here is a practical preflight workflow for ordinary QR Code symbols. It is not guidance for Micro QR, which has different layout requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Understand the three sizes involved
&lt;/h2&gt;

&lt;p&gt;A QR code has at least three separate size concepts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Symbol size:&lt;/strong&gt; the number of modules in the encoded square.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export size:&lt;/strong&gt; the number of pixels or vector dimensions in the downloaded file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Placement size:&lt;/strong&gt; the physical size of the code on the final material.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A module is one cell in the symbol's grid.&lt;/p&gt;

&lt;p&gt;DENSO WAVE documents versions from 1 to 40, starting at 21 × 21 modules and increasing by four modules per side with each version. See its &lt;a href="https://www.qrcode.com/en/about/version.html" rel="noopener noreferrer"&gt;version and capacity explanation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For standard QR Code, that gives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;modulesForVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RangeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;QR version must be an integer from 1 to 40&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Saving a larger PNG changes export resolution. It does not make a physically tiny code larger when the designer places it at the same width on a business card.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Reserve the quiet zone before decorating the layout
&lt;/h2&gt;

&lt;p&gt;The quiet zone is clear space around the symbol. It helps separate the code from nearby text, borders, and artwork.&lt;/p&gt;

&lt;p&gt;For ordinary QR Code, DENSO WAVE specifies a &lt;strong&gt;four-module margin on all four sides&lt;/strong&gt;. See its &lt;a href="https://www.qrcode.com/en/howto/code.html" rel="noopener noreferrer"&gt;code-area guidance&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That means a 29-module symbol needs a 37-module-wide area when the minimum margin is included:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;29 symbol modules + 4 left + 4 right = 37 modules
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same calculation applies vertically.&lt;/p&gt;

&lt;p&gt;Do not crop the white border away because it looks like wasted space. A decorative frame inside that clear area is also using the margin.&lt;/p&gt;

&lt;p&gt;If a generator lets you choose a margin smaller than four modules, use four or more for this standard QR print workflow. A code occasionally scanning with less space is not evidence that the smaller margin is a reliable production setting.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Calculate module size at final placement
&lt;/h2&gt;

&lt;p&gt;Here is a planning helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;qrPrintPlan&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outerWidthMm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dpi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;margin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RangeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid standard QR version&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outerWidthMm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;outerWidthMm&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dpi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;dpi&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;margin&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RangeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid dimensions, DPI, or quiet zone&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;symbolModules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;totalModules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;symbolModules&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;moduleMm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;outerWidthMm&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;totalModules&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;symbolModules&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;totalModules&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;moduleMm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;nominalPrinterDotsPerModule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;moduleMm&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dpi&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;25.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;nominalOuterPixels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;outerWidthMm&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dpi&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;25.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;qrPrintPlan&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;outerWidthMm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this illustrative version-3 code placed in a 25 mm outer square, including the four-module margin:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Total width is 37 modules.&lt;/li&gt;
&lt;li&gt;Each module is approximately 0.676 mm wide.&lt;/li&gt;
&lt;li&gt;At 300 DPI, that corresponds to approximately 7.98 nominal printer dots per module.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These numbers describe geometry, not a certified scan threshold. Real output also depends on the printer, substrate, ink spread, scaling, and camera conditions.&lt;/p&gt;

&lt;p&gt;There is no universal “make every QR code 2 cm wide” rule that solves every payload and viewing distance. At the same physical width, a denser symbol has smaller modules.&lt;/p&gt;

&lt;p&gt;If you deliberately use an integer raster scale, a 37-module-wide artifact at eight pixels per module is 296 pixels wide. Preserve that grid when exporting. Do not assume a later arbitrary resize will preserve it.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Payload length is a design input
&lt;/h2&gt;

&lt;p&gt;A long URL full of parameters can require a denser symbol than a short URL. The exact version depends on encoding mode, content, and error-correction level; character count alone is not a reliable version calculator.&lt;/p&gt;

&lt;p&gt;For print campaigns, ask whether all that data needs to be encoded directly.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;What the symbol stores&lt;/th&gt;
&lt;th&gt;Main dependency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Direct static URL&lt;/td&gt;
&lt;td&gt;The final address&lt;/td&gt;
&lt;td&gt;That address remaining usable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Static QR containing a short link&lt;/td&gt;
&lt;td&gt;A short redirect address&lt;/td&gt;
&lt;td&gt;The redirect service and mapping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Editable dynamic QR&lt;/td&gt;
&lt;td&gt;Usually a stable redirect address&lt;/td&gt;
&lt;td&gt;The service resolving it to the current destination&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A dynamic QR image does not rewrite its printed modules when the destination changes. The server-side mapping changes.&lt;/p&gt;

&lt;p&gt;A shorter redirect address can reduce the payload, but introduces a dependency on that service. The benefits and the operational responsibility belong in the same decision.&lt;/p&gt;

&lt;p&gt;If a printed item must remain useful for years, plan who owns the destination, who can update it, and what happens when the service or subscription changes. A technically valid symbol can still point to an unavailable page.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Error correction is not permission to cover anything
&lt;/h2&gt;

&lt;p&gt;QR error correction helps recover data from damage. DENSO WAVE explains the trade-off: greater correction capability adds redundancy and can increase the required symbol size. Its percentages concern restoration of codewords, not an arbitrary percentage of the picture's area. See the &lt;a href="https://www.qrcode.com/en/about/error_correction.html" rel="noopener noreferrer"&gt;error-correction explanation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That distinction matters for logos.&lt;/p&gt;

&lt;p&gt;“Maximum correction” does not mean you may cover any large central region, erase finder patterns, or ignore the quiet zone. The location and kind of damage matter.&lt;/p&gt;

&lt;p&gt;My recommended baseline is a plain, high-contrast code with no overlay. Confirm that baseline first. Add branding only when it survives the same final-material tests.&lt;/p&gt;

&lt;p&gt;If branding causes failures, remove it before reducing the margin or making the modules smaller. The symbol's primary job is to communicate its payload.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Export for the actual production path
&lt;/h2&gt;

&lt;p&gt;Choose a format with the next step in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SVG:&lt;/strong&gt; useful when the layout tool and printer preserve the vector artwork correctly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PNG:&lt;/strong&gt; useful for a controlled raster workflow with sufficient resolution at final placement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JPEG:&lt;/strong&gt; a poor default for a sharp module grid because lossy artifacts can alter edges.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Vector export does not guarantee print success. A later tool may rasterize it, crop the margin, apply transparency, or scale it incorrectly.&lt;/p&gt;

&lt;p&gt;For raster artwork, avoid repeated resampling and do not turn the code into a low-quality compressed photo asset. Keep dark modules distinct from a light, clear background.&lt;/p&gt;

&lt;p&gt;A transparent background deserves particular attention: placing it over a photograph can destroy the clear quiet zone even when the original export looked fine on white.&lt;/p&gt;

&lt;p&gt;Verify the delivered PDF or final design export, not only the generator's preview.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Test the landing experience as well as the symbol
&lt;/h2&gt;

&lt;p&gt;There are several independent success conditions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The camera recognizes the symbol.&lt;/li&gt;
&lt;li&gt;The decoded payload is correct.&lt;/li&gt;
&lt;li&gt;The address resolves and any redirects complete.&lt;/li&gt;
&lt;li&gt;The landing page works on the customer's phone.&lt;/li&gt;
&lt;li&gt;The page presents the action promised beside the code.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If a restaurant card says “View menu,” opening a desktop-only download page is not a successful customer experience even though decoding worked.&lt;/p&gt;

&lt;p&gt;Use an ordinary camera app and, where relevant, a second scanner implementation. Test the final printed proof on more than one device. Check a fresh navigation rather than relying only on an already-open page.&lt;/p&gt;

&lt;p&gt;Do not publish private test credentials inside a QR payload. For WiFi or contact-card codes, inspect the actual decoded content and make sure you intended to distribute it.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Build a small preflight matrix
&lt;/h2&gt;

&lt;p&gt;Before committing to a print run, I would test:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Cases to include&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Material&lt;/td&gt;
&lt;td&gt;Actual paper, label, packaging, or sign surface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Placement&lt;/td&gt;
&lt;td&gt;Final size and final surrounding artwork&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lighting&lt;/td&gt;
&lt;td&gt;Normal use, dimmer light, and realistic glare&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Angle&lt;/td&gt;
&lt;td&gt;Straight-on and a plausible tilted view&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Device&lt;/td&gt;
&lt;td&gt;More than one phone, including an older one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distance&lt;/td&gt;
&lt;td&gt;The distance a customer can comfortably use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Destination&lt;/td&gt;
&lt;td&gt;Correct content, working redirects, mobile usability&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Write down the result. “It scanned once” is weaker evidence than “these devices opened the correct page from the approved print proof under these conditions.”&lt;/p&gt;

&lt;p&gt;When a failure appears, change one variable at a time: increase placement size, restore the margin, simplify the payload, remove an overlay, or improve contrast. That makes the cause easier to identify.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generate the code, then approve the proof
&lt;/h2&gt;

&lt;p&gt;You can create static QR codes and download PNG or SVG using &lt;a href="https://batchset.com/tools/qr-code-generator?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=engineering-workflows&amp;amp;utm_content=qr-print-preflight" rel="noopener noreferrer"&gt;BatchSet's QR Code Generator&lt;/a&gt;. Dynamic QR codes and scan tracking are available with a free account.&lt;/p&gt;

&lt;p&gt;For standard QR printing, explicitly select a quiet zone of at least four modules, choose appropriate dimensions, and test the final proof. A generator prepares the artifact; the real-world scan test approves it.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>qrcode</category>
      <category>design</category>
      <category>testing</category>
    </item>
    <item>
      <title>A Short Link Is an HTTP Contract: Redirects, Caching, and Click Counts That Mean What They Say</title>
      <dc:creator>Muhaymin Bin Mehmood</dc:creator>
      <pubDate>Fri, 02 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/muhayminbinmehmood/a-short-link-is-an-http-contract-redirects-caching-and-click-counts-that-mean-what-they-say-3735</link>
      <guid>https://dev.to/muhayminbinmehmood/a-short-link-is-an-http-contract-redirects-caching-and-click-counts-that-mean-what-they-say-3735</guid>
      <description>&lt;p&gt;A URL shortener looks like a small feature: save a long URL, generate a code, and redirect anyone who visits it.&lt;/p&gt;

&lt;p&gt;But a link can outlive the interface that created it. It may sit in an email, a saved message, or a printed brochure for months. The redirect becomes a public contract between that artifact and the destination.&lt;/p&gt;

&lt;p&gt;Several decisions hide inside that contract: whether the destination can change, whether caches can bypass the redirect service, which query parameters reach the landing page, and what a “click” actually means.&lt;/p&gt;

&lt;p&gt;This is a reference architecture for developers. It does not describe BatchSet's internal status codes, cache configuration, event pipeline, or bot filtering. Those require implementation evidence, not assumptions from a product page.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Choose the behavior before choosing the status code
&lt;/h2&gt;

&lt;p&gt;Suppose a business prints this illustrative address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://go.example.com/summer-menu
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The destination is a seasonal menu. The business expects to replace it later without reprinting the card.&lt;/p&gt;

&lt;p&gt;That is different from permanently moving a documentation page to a new canonical location.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Meaning relevant to this design&lt;/th&gt;
&lt;th&gt;Method behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;301&lt;/td&gt;
&lt;td&gt;Permanent redirect&lt;/td&gt;
&lt;td&gt;Clients may change POST to GET&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;302&lt;/td&gt;
&lt;td&gt;Temporary redirect&lt;/td&gt;
&lt;td&gt;Clients may change POST to GET&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;307&lt;/td&gt;
&lt;td&gt;Temporary redirect&lt;/td&gt;
&lt;td&gt;Preserves the request method and body&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;308&lt;/td&gt;
&lt;td&gt;Permanent redirect&lt;/td&gt;
&lt;td&gt;Preserves the request method and body&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For an ordinary navigation-only short-link endpoint, I would explicitly support GET and HEAD and reject other methods. A temporary redirect can fit an editable destination. That is a design choice, not a universal requirement.&lt;/p&gt;

&lt;p&gt;If an application really needs to forward non-GET requests, method preservation becomes a separate, deliberate decision. Sending a submitted body to a different destination can have consequences that do not exist for a simple link click.&lt;/p&gt;

&lt;p&gt;See the HTTP semantics in &lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html#name-redirection-3xx" rel="noopener noreferrer"&gt;RFC 9110, redirection&lt;/a&gt;, along with MDN's &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/302" rel="noopener noreferrer"&gt;302&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/307" rel="noopener noreferrer"&gt;307&lt;/a&gt; references.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Cache policy is part of editability
&lt;/h2&gt;

&lt;p&gt;A temporary status does not substitute for a cache strategy.&lt;/p&gt;

&lt;p&gt;If a client or intermediary reuses a stored redirect, an updated destination may not be consulted on that visit. The same reuse can keep the request from reaching the application analytics handler.&lt;/p&gt;

&lt;p&gt;For a small editable-link service, a straightforward reference policy is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;302&lt;/span&gt; &lt;span class="ne"&gt;Found&lt;/span&gt;
&lt;span class="na"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://shop.example.com/menu&lt;/span&gt;
&lt;span class="na"&gt;Cache-Control&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;no-store&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;no-store&lt;/code&gt; tells compliant caches not to store that response. &lt;code&gt;no-cache&lt;/code&gt; has a different meaning: storage is allowed, but reuse requires validation. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control" rel="noopener noreferrer"&gt;MDN explains the distinction&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This does not erase older cached redirects or make browser history behavior identical across clients. Configure and test the CDN as well as the application. Some deployments have explicit platform rules that must be reconciled with response headers.&lt;/p&gt;

&lt;p&gt;At higher volume, you may cache the &lt;strong&gt;destination lookup&lt;/strong&gt; internally while keeping each public redirect observable. That creates a new obligation: invalidate the mapping promptly when someone edits or disables the link.&lt;/p&gt;

&lt;p&gt;Caching a database lookup and caching an HTTP redirect are different operations. Name them separately in the design.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. A redirect request is not proof of a human click
&lt;/h2&gt;

&lt;p&gt;Imagine sharing a link in a messaging app. The app may retrieve it to build a preview. A security scanner may inspect it before the recipient opens the message. A user may also visit twice.&lt;/p&gt;

&lt;p&gt;A request counter can be useful without claiming to count people.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;What it can mean&lt;/th&gt;
&lt;th&gt;What it does not prove&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Redirect requests&lt;/td&gt;
&lt;td&gt;Requests observed by the redirect service&lt;/td&gt;
&lt;td&gt;Human intent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filtered requests&lt;/td&gt;
&lt;td&gt;Requests remaining after a stated filter&lt;/td&gt;
&lt;td&gt;Perfect bot exclusion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Estimated unique visitors&lt;/td&gt;
&lt;td&gt;Deduplicated observations under a stated rule&lt;/td&gt;
&lt;td&gt;Exact distinct people&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Landing-page sessions&lt;/td&gt;
&lt;td&gt;Sessions recorded at the destination&lt;/td&gt;
&lt;td&gt;Every short-link request arrived&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conversions&lt;/td&gt;
&lt;td&gt;A defined destination-side event&lt;/td&gt;
&lt;td&gt;Every visit caused a sale&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;User agents, request methods, timing, and known crawler signatures can inform filtering. None of them is a flawless human detector.&lt;/p&gt;

&lt;p&gt;The product decision I recommend is to document the metric and retain the distinction between raw observations and derived estimates. Avoid replacing one ambiguous label with a more impressive ambiguous label.&lt;/p&gt;

&lt;p&gt;If a dashboard says “clicks,” its help text should explain what is counted and what may be missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Make analytics failure a deliberate trade-off
&lt;/h2&gt;

&lt;p&gt;There are two tempting extremes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wait for every analytics write before redirecting, so logging failures can delay navigation.&lt;/li&gt;
&lt;li&gt;Start an unawaited promise and return immediately, assuming the hosting runtime will finish it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither is automatically correct.&lt;/p&gt;

&lt;p&gt;For a reference design, a bounded durable enqueue can keep the event path short. The service can choose to continue redirecting when event ingestion fails, record the failure, and accept that the analytics stream may have a gap.&lt;/p&gt;

&lt;p&gt;An alternative is logging at an edge or request layer with a suitable durability contract. Platform-specific background execution should use the platform's supported mechanism, not a detached promise with no lifecycle guarantee.&lt;/p&gt;

&lt;p&gt;Define the desired behavior during an outage:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does navigation continue?&lt;/li&gt;
&lt;li&gt;Can the failed event be recovered?&lt;/li&gt;
&lt;li&gt;Can operators see the missing-data condition?&lt;/li&gt;
&lt;li&gt;Does retrying an event create duplicates?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A unique event ID helps a consumer deduplicate retries. It does not make an event pipeline “exactly once” by itself.&lt;/p&gt;

&lt;p&gt;These are engineering choices for a new implementation. They are not claims about how BatchSet currently records clicks.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Validate destinations without claiming they are trustworthy
&lt;/h2&gt;

&lt;p&gt;For a link that should open a website, require an absolute HTTP or HTTPS URL and reject embedded credentials.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parseDestination&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;destination&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Use an absolute HTTP or HTTPS URL&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Embedded credentials are not supported&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is syntax and scheme validation. It does not establish that a website is honest, safe, reachable, or owned by the creator.&lt;/p&gt;

&lt;p&gt;Production abuse handling needs its own decisions: creation limits, reserved codes, reporting, review, disabling malicious links, and appropriate checks for private-network destinations if those should not be supported.&lt;/p&gt;

&lt;p&gt;If you later add server-side destination previews, the threat model changes again. A browser redirect does not require your server to fetch the destination; generating a preview does. That new fetcher needs its own SSRF protections.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Do not accidentally create a public open-redirect parameter
&lt;/h2&gt;

&lt;p&gt;This pattern makes the saved mapping irrelevant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/r/summer-menu?destination=https://unexpected.example
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If any visitor can override the target, the short-code owner no longer controls what the public artifact means.&lt;/p&gt;

&lt;p&gt;Prefer a stored, authorized mapping. Editing that mapping should require permission. Anonymous visitors should only resolve it.&lt;/p&gt;

&lt;p&gt;Also decide what happens to query parameters. If the saved destination already has campaign tags, blindly appending every incoming parameter can overwrite attribution or introduce parameters the landing application never expected.&lt;/p&gt;

&lt;p&gt;My default for a reference service is &lt;strong&gt;do not forward incoming parameters&lt;/strong&gt;. If forwarding is needed, use a small allowlist and define precedence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;addAllowedCampaignParameters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;savedURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;incomingURL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;savedURL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;incoming&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;incomingURL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utm_source&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utm_medium&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utm_campaign&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;incoming&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The saved value wins here. Other applications may choose differently, but the rule should be intentional and testable.&lt;/p&gt;

&lt;p&gt;Campaign parameters should not carry passwords, personal email addresses, or other secrets. URLs can be retained in histories, logs, screenshots, and downstream systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Test the public contract, including negative cases
&lt;/h2&gt;

&lt;p&gt;A useful acceptance matrix for a short-link implementation includes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Expected decision to verify&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Existing code, GET&lt;/td&gt;
&lt;td&gt;Correct destination and cache policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Existing code, HEAD&lt;/td&gt;
&lt;td&gt;Deliberate response and counting policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST to navigation endpoint&lt;/td&gt;
&lt;td&gt;Rejected if unsupported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unknown code&lt;/td&gt;
&lt;td&gt;Clear not-found response&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disabled or expired code&lt;/td&gt;
&lt;td&gt;No silent redirect to an unrelated page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Destination edited&lt;/td&gt;
&lt;td&gt;Subsequent fresh requests use the new target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analytics unavailable&lt;/td&gt;
&lt;td&gt;Navigation follows the documented outage policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replayed event&lt;/td&gt;
&lt;td&gt;Consumer deduplicates under its stated rule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unexpected query parameter&lt;/td&gt;
&lt;td&gt;Cannot override the destination&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-web scheme or credentials&lt;/td&gt;
&lt;td&gt;Rejected at creation and edit time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use example domains when testing destination behavior. Do not test a forwarding policy by sending real form data to arbitrary external sites.&lt;/p&gt;

&lt;p&gt;Also compare redirect counts with destination-side analytics, expecting disagreement. A discrepancy is a debugging starting point, not proof that either system measures the same event incorrectly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the link workflow to use
&lt;/h2&gt;

&lt;p&gt;BatchSet offers a &lt;a href="https://batchset.com/tools/url-shortener?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=engineering-workflows&amp;amp;utm_content=short-link-contract" rel="noopener noreferrer"&gt;URL Shortener with click analytics and optional custom codes&lt;/a&gt;, available with a free account.&lt;/p&gt;

&lt;p&gt;If you want to organize links for a campaign, start there. Verify the final destination before sharing, keep campaign labels consistent, and interpret click reporting alongside landing-page outcomes.&lt;/p&gt;

&lt;p&gt;A short link should be easy to share. The meaning of its destination and its metrics should be just as easy to explain.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>backend</category>
      <category>analytics</category>
      <category>security</category>
    </item>
  </channel>
</rss>
