<?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: West</title>
    <description>The latest articles on DEV Community by West (@west_dcaaa3a98181e80af749).</description>
    <link>https://dev.to/west_dcaaa3a98181e80af749</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4071463%2F80265a6a-d2d2-471e-a4c7-d62bdf66afcd.png</url>
      <title>DEV Community: West</title>
      <link>https://dev.to/west_dcaaa3a98181e80af749</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/west_dcaaa3a98181e80af749"/>
    <language>en</language>
    <item>
      <title>The 51 KB Image Wasn't the Problem: How I Cut Mobile LCP from 4.0s to 1.47s</title>
      <dc:creator>West</dc:creator>
      <pubDate>Mon, 07 Sep 2026 09:09:45 +0000</pubDate>
      <link>https://dev.to/west_dcaaa3a98181e80af749/the-51-kb-image-wasnt-the-problem-how-i-cut-mobile-lcp-from-40s-to-147s-25cf</link>
      <guid>https://dev.to/west_dcaaa3a98181e80af749/the-51-kb-image-wasnt-the-problem-how-i-cut-mobile-lcp-from-40s-to-147s-25cf</guid>
      <description>&lt;p&gt;The homepage looked like an obvious image-performance problem.&lt;/p&gt;

&lt;p&gt;It was a storefront full of digital planner covers and sticker previews. On mobile, Lighthouse reported a median Largest Contentful Paint (LCP) of &lt;strong&gt;4.04 seconds&lt;/strong&gt;. The hero image was the LCP element, so the first instinct was predictable: compress the image again.&lt;/p&gt;

&lt;p&gt;But the hero was already a 51 KB WebP.&lt;/p&gt;

&lt;p&gt;The real problem was everything surrounding it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;nearly 1 MB of initial HTML and React Server Component data;&lt;/li&gt;
&lt;li&gt;134 KB of render-blocking CSS;&lt;/li&gt;
&lt;li&gt;authentication and saved-product providers loaded on a public page;&lt;/li&gt;
&lt;li&gt;full product objects serialized for cards that needed only a few fields; and&lt;/li&gt;
&lt;li&gt;an LCP image that was responsive, but not yet expressed to the browser as the page's critical image.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After fixing those issues, the production mobile Lighthouse median dropped from &lt;strong&gt;4.04 seconds to 1.47 seconds&lt;/strong&gt;. CLS stayed at 0, and median Total Blocking Time was 9 ms.&lt;/p&gt;

&lt;p&gt;This article explains the changes that mattered, the changes that were insufficient on their own, and the tests I added to keep the page fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the LCP timeline, not the image file
&lt;/h2&gt;

&lt;p&gt;LCP is not just image download time. It can be split into four parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Time to First Byte&lt;/li&gt;
&lt;li&gt;Resource load delay&lt;/li&gt;
&lt;li&gt;Resource load duration&lt;/li&gt;
&lt;li&gt;Element render delay&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That distinction matters because shrinking an image only improves the third part. It does nothing when the browser discovers the image late, rendering is blocked by CSS, or the main thread is busy processing unrelated JavaScript.&lt;/p&gt;

&lt;p&gt;In my baseline run, Time to First Byte was about &lt;strong&gt;98 ms&lt;/strong&gt;. The server was not the bottleneck. The 51 KB hero was also not large enough to explain a four-second LCP by itself.&lt;/p&gt;

&lt;p&gt;The waterfall pointed elsewhere: the browser was receiving and processing a large document, stylesheet, RSC payload, and client-side code before the hero could become visible.&lt;/p&gt;

&lt;p&gt;That changed the optimization question from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How can I compress this image further?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What is preventing this already-small image from painting sooner?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. Stop sending detail-page data to catalog cards
&lt;/h2&gt;

&lt;p&gt;The homepage fetched complete product records. A sticker product could contain every sheet in the pack, aliases, high-resolution image URLs, and download URLs.&lt;/p&gt;

&lt;p&gt;The card needed only a title, category, thumbnail, price, and a small amount of metadata.&lt;/p&gt;

&lt;p&gt;Passing the full objects from a Server Component to a Client Component meant all of that unused data was serialized into the RSC payload.&lt;/p&gt;

&lt;p&gt;I added an explicit projection at the server boundary:&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;toHomepageCatalogItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&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;card&lt;/span&gt; &lt;span class="o"&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="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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="nx"&gt;item&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="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;blurb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blurb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;thumbnailUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;thumbnailUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;coverUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coverUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&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;item&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;planner&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="p"&gt;{&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;year&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pages&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;card&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sheetCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sheetCount&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;Then the server mapped the API response before rendering the interactive homepage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stickerCatalog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;stickerResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fulfilled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;stickerResult&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="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toHomepageCatalogItem&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;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;HomePage&lt;/span&gt;
    &lt;span class="na"&gt;planners&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;planners&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;toHomepageCatalogItem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;stickers&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;stickerCatalog&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;/&amp;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 easy to miss in image-heavy applications. The images get blamed because they are visible, while oversized JSON and RSC payloads remain invisible.&lt;/p&gt;

&lt;p&gt;After projecting the catalog data, the production homepage document fell from roughly &lt;strong&gt;999 KB to 169 KB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The general rule is simple: &lt;strong&gt;serialize for the component you are rendering, not for every component that might render the object later.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Push client providers down the route tree
&lt;/h2&gt;

&lt;p&gt;The root layout originally wrapped the entire application with authentication and saved-product state. That was convenient, but it made the public homepage pay for account features before a visitor used them.&lt;/p&gt;

&lt;p&gt;The fix was architectural rather than image-specific: keep the root layout server-first and move client providers to the routes that need them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/layout.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RootLayout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt;&lt;span class="p"&gt;&amp;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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/(site)/layout.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SiteLayout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&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;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AuthProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SavedProductsProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;SavedProductsProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AuthProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;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 public homepage sits outside that route group, while account-aware pages remain fully functional.&lt;/p&gt;

&lt;p&gt;This reduced the client work competing with image decode and paint on slower mobile hardware. It also follows a useful Next.js rule: render providers as deep in the tree as their consumers allow.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Make the LCP image discoverable and responsive
&lt;/h2&gt;

&lt;p&gt;Below-the-fold images should usually be lazy-loaded. The LCP image should not.&lt;/p&gt;

&lt;p&gt;For the hero, I used &lt;code&gt;next/image&lt;/code&gt; with intrinsic dimensions, a realistic &lt;code&gt;sizes&lt;/code&gt; value, and the Next.js 16 &lt;code&gt;preload&lt;/code&gt; prop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Image&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/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;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Image&lt;/span&gt;
  &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/home/hero-digital-planner-ipad.webp"&lt;/span&gt;
  &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Digital weekly planner open on an iPad beside a stylus"&lt;/span&gt;
  &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1402&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1122&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;sizes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"(max-width: 900px) 100vw, (max-width: 1440px) 55vw, 792px"&lt;/span&gt;
  &lt;span class="na"&gt;preload&lt;/span&gt;
  &lt;span class="na"&gt;decoding&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"async"&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each prop has a different job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; reserve the correct aspect ratio and prevent layout shift.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sizes&lt;/code&gt; tells the browser how wide the image will actually render, allowing it to choose an appropriate candidate from &lt;code&gt;srcset&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;preload&lt;/code&gt; tells Next.js that this is the page's critical image.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Next.js 16, &lt;code&gt;priority&lt;/code&gt; is deprecated in favor of &lt;code&gt;preload&lt;/code&gt;. Do not combine every urgency signal by habit. Use &lt;code&gt;preload&lt;/code&gt; for the one image that is predictably the LCP element, and let non-critical images keep the default lazy-loading behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Use image tiers instead of one asset everywhere
&lt;/h2&gt;

&lt;p&gt;An image catalog has at least three different viewing contexts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Context&lt;/th&gt;
&lt;th&gt;Asset&lt;/th&gt;
&lt;th&gt;Goal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Catalog grid&lt;/td&gt;
&lt;td&gt;Thumbnail&lt;/td&gt;
&lt;td&gt;Fast scanning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Product page&lt;/td&gt;
&lt;td&gt;Display image&lt;/td&gt;
&lt;td&gt;Clear normal viewing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zoom viewer&lt;/td&gt;
&lt;td&gt;High-resolution image&lt;/td&gt;
&lt;td&gt;Inspecting details&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Using the original asset in all three contexts wastes bandwidth. Using only a thumbnail makes the product viewer look broken.&lt;/p&gt;

&lt;p&gt;I made the media contract explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;CatalogImage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;thumbnailUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;displayUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;zoomUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&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;Catalog cards request &lt;code&gt;thumbnailUrl&lt;/code&gt;. Product pages start with &lt;code&gt;displayUrl&lt;/code&gt;. The viewer loads &lt;code&gt;zoomUrl&lt;/code&gt; only when the user opens or changes the high-resolution preview.&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;previewUrlFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;variant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;thumbnail&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;variant&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zoom&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;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;zoomUrl&lt;/span&gt;
      &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;displayUrl&lt;/span&gt;
      &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;thumbnailUrl&lt;/span&gt;
      &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&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;variant&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;display&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;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;displayUrl&lt;/span&gt;
      &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;thumbnailUrl&lt;/span&gt;
      &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&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;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;thumbnailUrl&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&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 fallback chain keeps old content usable while new records adopt all three tiers.&lt;/p&gt;

&lt;p&gt;For immutable, versioned media objects, the delivery endpoint returns a long-lived cache policy:&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="err"&gt;Cache-Control: public, max-age=31536000, immutable
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Versioned object keys are important here. A year-long cache is safe only when replacing an image creates a new URL instead of mutating the bytes behind an existing URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Remove render blockers only after proving they block rendering
&lt;/h2&gt;

&lt;p&gt;The baseline included a 134 KB stylesheet that blocked first render. For this application, enabling CSS inlining removed an extra render-blocking request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next&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;nextConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;inlineCss&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;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&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 not a universal recommendation.&lt;/p&gt;

&lt;p&gt;Inlining can increase HTML size, reduce the caching benefit of a shared stylesheet, and behave differently as an experimental feature evolves. It was useful here because the waterfall showed CSS delaying the LCP render, and the document had already been reduced substantially.&lt;/p&gt;

&lt;p&gt;Measure this change against your production route. Do not enable it because an audit contains a generic “eliminate render-blocking resources” suggestion.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Keep visual effects away from the critical paint
&lt;/h2&gt;

&lt;p&gt;Image sites often add fade-ins, clip-path reveals, and scale animations to make grids feel polished. These effects can delay when a fully downloaded image is considered painted.&lt;/p&gt;

&lt;p&gt;My rule became:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the hero is visible immediately;&lt;/li&gt;
&lt;li&gt;below-the-fold cards may animate when appropriate;&lt;/li&gt;
&lt;li&gt;image containers reserve space with &lt;code&gt;aspect-ratio&lt;/code&gt; or intrinsic dimensions;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;prefers-reduced-motion&lt;/code&gt; removes non-essential transitions; and&lt;/li&gt;
&lt;li&gt;the high-resolution viewer progressively replaces the display image without resizing its container.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A 600 ms entrance animation on the LCP element is still roughly 600 ms added to LCP. The browser does not award style points for making the most important content wait.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Add regression tests for performance decisions
&lt;/h2&gt;

&lt;p&gt;Performance regressions often look harmless in code review. Someone passes the complete API object again. A provider returns to the root layout. A hero loses its responsive &lt;code&gt;sizes&lt;/code&gt; value.&lt;/p&gt;

&lt;p&gt;I added tests for those architectural contracts:&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="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;homepage catalog projection excludes detail-only payloads&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="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;projected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toHomepageCatalogItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;largeStickerProduct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;projected&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="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;projected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;downloadUrls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;projected&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;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;largeStickerProduct&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="mi"&gt;4&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;account providers stay off the homepage root bundle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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;rootLayout&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;source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../src/app/layout.tsx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;doesNotMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;rootLayout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sr"&gt;/AuthProvider|SavedProductsProvider/&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;I also test that the hero still uses &lt;code&gt;next/image&lt;/code&gt;, &lt;code&gt;sizes&lt;/code&gt;, and &lt;code&gt;preload&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;These are not substitutes for Lighthouse or real-user monitoring. They protect the decisions that made the measured improvement possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;I ran multiple Lighthouse tests against the canonical production URL and used the median rather than selecting the best run.&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;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mobile LCP&lt;/td&gt;
&lt;td&gt;4.04 s&lt;/td&gt;
&lt;td&gt;1.47 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLS&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TBT&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;9 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Initial document&lt;/td&gt;
&lt;td&gt;~999 KB&lt;/td&gt;
&lt;td&gt;~169 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Desktop LCP was already around 0.89 seconds before this work. Reporting only the desktop result would have hidden the real user experience on constrained mobile networks and devices.&lt;/p&gt;

&lt;p&gt;These numbers are &lt;strong&gt;Lighthouse lab results&lt;/strong&gt;, not field data. The site did not have enough Chrome UX Report data to validate INP or real-user Core Web Vitals, so I did not treat a perfect Lighthouse score as proof that every production user had a perfect experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  The optimization order I would use again
&lt;/h2&gt;

&lt;p&gt;If I were diagnosing another image-heavy site tomorrow, I would work in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Measure the production page several times on mobile.&lt;/li&gt;
&lt;li&gt;Identify the actual LCP element.&lt;/li&gt;
&lt;li&gt;Split LCP into TTFB, load delay, load duration, and render delay.&lt;/li&gt;
&lt;li&gt;Inspect HTML, RSC, CSS, and JavaScript before recompressing the image.&lt;/li&gt;
&lt;li&gt;Remove unused data crossing the server-client boundary.&lt;/li&gt;
&lt;li&gt;Scope client providers and third-party scripts to the routes that need them.&lt;/li&gt;
&lt;li&gt;Give the real LCP image intrinsic dimensions, responsive sizing, and early discovery.&lt;/li&gt;
&lt;li&gt;Serve thumbnails, display images, and zoom assets for different contexts.&lt;/li&gt;
&lt;li&gt;Remove measured render blockers.&lt;/li&gt;
&lt;li&gt;Deploy, retest the canonical production URL, and compare medians.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The biggest lesson was not “use WebP” or “lazy-load images.” It was this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An image can be the LCP element without being the LCP bottleneck.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Optimize the path that gets the image onto the screen, not just the image file.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://web.dev/articles/optimize-lcp" rel="noopener noreferrer"&gt;Optimize Largest Contentful Paint&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nextjs.org/docs/app/api-reference/components/image" rel="noopener noreferrer"&gt;Next.js Image component&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nextjs.org/docs/app/getting-started/server-and-client-components" rel="noopener noreferrer"&gt;Next.js Server and Client Components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/p/editor_guide/"&gt;DEV Community Editor Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>frontend</category>
      <category>performance</category>
      <category>react</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Turning Photos into Buildable Pixel-Bead Patterns</title>
      <dc:creator>West</dc:creator>
      <pubDate>Mon, 07 Sep 2026 09:09:00 +0000</pubDate>
      <link>https://dev.to/west_dcaaa3a98181e80af749/turning-photos-into-buildable-pixel-bead-patterns-2h2c</link>
      <guid>https://dev.to/west_dcaaa3a98181e80af749/turning-photos-into-buildable-pixel-bead-patterns-2h2c</guid>
      <description>&lt;p&gt;A 58×58 bead board contains 3,364 positions. In one of our production checks, treating every position as part of the picture produced exactly 3,364 beads. With automatic background analysis enabled, the same source produced 900.&lt;/p&gt;

&lt;p&gt;That difference is not a cosmetic optimization. It changes the material list, the cost of the project, and whether the exported guide is usable at all.&lt;/p&gt;

&lt;p&gt;While building &lt;a href="https://pixelbeadstudio.com/image-to-pixel-bead" rel="noopener noreferrer"&gt;Pixel Bead Studio&lt;/a&gt;, I learned that converting an image into a physical bead pattern is not simply a matter of resizing a photo and choosing the nearest colors. A useful converter must preserve small features, understand empty space, map digital colors to a finite physical palette, and produce the same result everywhere—from the preview to the printable PDF.&lt;/p&gt;

&lt;p&gt;This post explains the deterministic TypeScript pipeline we built to do that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why naive image resizing fails
&lt;/h2&gt;

&lt;p&gt;The simplest implementation looks reasonable:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Resize the source image to the target grid.&lt;/li&gt;
&lt;li&gt;Read one RGB value for each cell.&lt;/li&gt;
&lt;li&gt;Find the nearest bead color.&lt;/li&gt;
&lt;li&gt;Export the grid.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It also fails in predictable ways.&lt;/p&gt;

&lt;p&gt;Thin outlines disappear because their average coverage is smaller than the surrounding fill. Eyes and facial details become muddy. White backgrounds turn into thousands of white beads. Near-neutral colors jump between visually inconsistent palette entries. Dithering can improve gradients, but it may also break a clean outline into isolated specks.&lt;/p&gt;

&lt;p&gt;The central problem is that a bead cell is both a color sample and a structural decision. We therefore split the conversion into explicit stages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;source pixels
  -&amp;gt; source-resolution background analysis
  -&amp;gt; source-type detection
  -&amp;gt; 8×8 shape-aware sampling per bead
  -&amp;gt; perceptual palette matching
  -&amp;gt; optional protected dithering
  -&amp;gt; one canonical pattern matrix
  -&amp;gt; preview, material list, PNG, and PDF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each stage solves one problem and leaves evidence that can be tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Remove backgrounds before resizing
&lt;/h2&gt;

&lt;p&gt;Background removal must happen at source resolution. If the image is resized first, antialiasing mixes the foreground and background together. The converter then has to decide whether a pale edge pixel belongs to the subject or the canvas—with less information than it had at the start.&lt;/p&gt;

&lt;p&gt;Our automatic mode samples the source perimeter, estimates a candidate background color, and checks how consistently that color appears across multiple sides. It then flood-fills only matching regions connected to the exterior.&lt;/p&gt;

&lt;p&gt;The connectivity rule matters. An enclosed white area may be a real part of the design, such as the center of a ring. Removing every pixel close to white would erase it. Removing only exterior-connected pixels preserves enclosed regions.&lt;/p&gt;

&lt;p&gt;The API makes uncertainty visible instead of hiding it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;BackgroundMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto&lt;/span&gt;&lt;span class="dl"&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;manual&lt;/span&gt;&lt;span class="dl"&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;off&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;BackgroundDiagnostics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BackgroundMode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;removed&lt;/span&gt;&lt;span class="dl"&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;preserved&lt;/span&gt;&lt;span class="dl"&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;disabled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;removedPixels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;Automatic removal is deliberately conservative. If the border is inconsistent, the foreground is too similar to the candidate background, or compression artifacts make the decision ambiguous, the pipeline preserves the pixels. Users can then choose a background color manually or disable removal.&lt;/p&gt;

&lt;p&gt;For creative tools, “I am not confident” is often a better result than a confident destructive edit.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Detect discrete pixel art before sampling it like a photo
&lt;/h2&gt;

&lt;p&gt;A screenshot of pixel art and a photograph should not follow the same resampling path.&lt;/p&gt;

&lt;p&gt;Photographs contain continuous tones and benefit from area-based resampling. Pixel art contains a deliberate lattice. Smoothing it creates colors that never existed in the source and blurs the boundaries we want to preserve.&lt;/p&gt;

&lt;p&gt;We inspect horizontal and vertical edge peaks, search for a repeating period, and measure how strongly edges align with that lattice. A source can then be classified as continuous, a detected pixel lattice, or native low-resolution pixel art.&lt;/p&gt;

&lt;p&gt;When the lattice evidence is strong, we recover colors from stable cell cores and use nearest-neighbor resampling. Otherwise, the image stays on the continuous-tone path.&lt;/p&gt;

&lt;p&gt;This prevents an important category error: improving a photo and preserving pixel art are different jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Treat every bead as an 8×8 analysis problem
&lt;/h2&gt;

&lt;p&gt;One center pixel is too fragile, while a plain average erases small details. Our shape-aware sampler analyzes 64 subpixels for every output bead.&lt;/p&gt;

&lt;p&gt;Inside each 8×8 region, the samples are grouped into two color clusters in Lab space. We then measure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how much of the cell each cluster covers;&lt;/li&gt;
&lt;li&gt;the perceptual distance between the clusters;&lt;/li&gt;
&lt;li&gt;whether the smaller cluster forms a connected feature;&lt;/li&gt;
&lt;li&gt;which sides of the cell the feature touches;&lt;/li&gt;
&lt;li&gt;how that feature continues into neighboring cells.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A simplified decision looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;protectFeature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;featureCoverage&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.18&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nx"&gt;featureCoverage&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;0.50&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nf"&gt;ciede2000&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nx"&gt;connectedCoverage&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.60&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;representative&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;protectFeature&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;feature&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dominantColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Coverage alone is not enough. Random noise may occupy 20% of a cell, but it will not form a coherent local shape. Connectivity helps distinguish an eye, border, or narrow stroke from compression noise.&lt;/p&gt;

&lt;p&gt;The sampler also records an &lt;code&gt;outlineProtected&lt;/code&gt; flag. Later stages are allowed to simplify ordinary surface cells, but they cannot casually overwrite a cell selected to preserve structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Match physical colors perceptually
&lt;/h2&gt;

&lt;p&gt;Bead palettes are finite. A photograph may contain thousands of RGB colors, while a selected bead brand provides a fixed catalog of purchasable color IDs.&lt;/p&gt;

&lt;p&gt;Euclidean RGB distance is a poor proxy for human perception. Equal numeric changes in different RGB channels do not look equally large, especially around skin tones and low-chroma colors.&lt;/p&gt;

&lt;p&gt;We convert both the sampled color and every palette entry from sRGB to CIE Lab, then choose the lowest CIEDE2000 distance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;nearestPaletteColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RGB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;palette&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PaletteColor&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;sampleLab&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rgbToLab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sample&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;palette&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;color&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;distance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ciede2000&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sampleLab&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;hexToLab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hex&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;distance&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;distance&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;distance&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;best&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="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;palette&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="na"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;Infinity&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 still cannot make a limited palette reproduce every source color. What it can do is make the error correspond more closely to what a person sees.&lt;/p&gt;

&lt;p&gt;The output stores real palette IDs rather than anonymous display colors. That lets the same matrix power editing, quantity calculation, and export.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Keep dithering away from structure
&lt;/h2&gt;

&lt;p&gt;Floyd–Steinberg dithering distributes quantization error into neighboring cells. It can simulate intermediate shades with a limited palette, but physical beadwork exposes its cost: every extra speck is another bead the maker must place.&lt;/p&gt;

&lt;p&gt;Our implementation uses serpentine traversal—left to right on one row, right to left on the next—to avoid a directional bias. More importantly, protected outline cells neither receive nor diffuse error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &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;y&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;y&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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;y&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;y&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="mi"&gt;0&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="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="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;x&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;scanRow&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;direction&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;cell&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&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;x&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;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;nearestPaletteColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;palette&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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;dither&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;outlineProtected&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;cell&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="nf"&gt;diffuseError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outlineProtected&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 product exposes three choices: Off for clean flat regions, Auto for color-rich images, and On for stronger gradient simulation. The default should reflect the physical task, not just what looks smooth on a screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Make one pattern matrix the source of truth
&lt;/h2&gt;

&lt;p&gt;An image converter is not finished when the preview looks correct.&lt;/p&gt;

&lt;p&gt;The material count, editable workspace, PNG guide, and PDF guide must all represent the same cells. If each output performs its own interpretation of transparency or background removal, they will drift.&lt;/p&gt;

&lt;p&gt;Our pipeline returns one integer matrix. A non-negative value is a palette index; an empty cell uses a sentinel value. Every downstream output consumes that matrix.&lt;/p&gt;

&lt;p&gt;We verify the contract in browser tests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;automatic, manual, and disabled background modes recompute the pattern;&lt;/li&gt;
&lt;li&gt;material quantities add up to the visible bead count;&lt;/li&gt;
&lt;li&gt;enclosed foreground-colored regions remain present;&lt;/li&gt;
&lt;li&gt;exterior empty cells stay empty in exported PNG files;&lt;/li&gt;
&lt;li&gt;the PDF contains the same palette IDs and quantities shown in the UI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the release described here, the validation gate included 625 unit tests and six targeted desktop/mobile browser tests. Two supplemental visual-dataset tests were skipped in the clean release environment because their local image assets were intentionally not stored in the repository.&lt;/p&gt;

&lt;p&gt;That last detail is worth reporting. A skipped visual test is not a passed visual test.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do differently next time
&lt;/h2&gt;

&lt;p&gt;Three lessons changed how I think about image conversion.&lt;/p&gt;

&lt;p&gt;First, background removal is a topology problem before it is a color problem. Exterior connectivity is more useful than a global “remove white” rule.&lt;/p&gt;

&lt;p&gt;Second, downsampling needs to preserve decisions, not pixels. The important question is not “What is the average color?” but “Is there a connected feature here that the final object needs?”&lt;/p&gt;

&lt;p&gt;Third, printable exports are part of the algorithm. A correct preview with an incorrect material list is still an incorrect converter.&lt;/p&gt;

&lt;p&gt;AI-assisted coding helped us explore implementations, generate adversarial cases, and tighten regression tests. It does not generate the final pattern in this pipeline. The shipped result is produced by deterministic image-processing rules and remains editable by the user.&lt;/p&gt;

&lt;p&gt;That boundary has been useful: use AI to accelerate engineering, but keep the artifact reproducible, inspectable, and under the maker’s control.&lt;/p&gt;

&lt;p&gt;You can try the current converter at &lt;a href="https://pixelbeadstudio.com/image-to-pixel-bead" rel="noopener noreferrer"&gt;Pixel Bead Studio&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>software</category>
      <category>tools</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How I Cut a 3D Homepage from 9.1 MB to 370 KB Without Removing the 3D</title>
      <dc:creator>West</dc:creator>
      <pubDate>Mon, 07 Sep 2026 09:08:31 +0000</pubDate>
      <link>https://dev.to/west_dcaaa3a98181e80af749/how-i-cut-a-3d-homepage-from-91-mb-to-370-kb-without-removing-the-3d-2ad3</link>
      <guid>https://dev.to/west_dcaaa3a98181e80af749/how-i-cut-a-3d-homepage-from-91-mb-to-370-kb-without-removing-the-3d-2ad3</guid>
      <description>&lt;p&gt;A single 3D model accounted for 83.7% of my homepage's initial payload.&lt;/p&gt;

&lt;p&gt;It was a 7,833,440-byte GLB displayed in the hero of &lt;a href="https://worldmodelswatch.com/" rel="noopener noreferrer"&gt;World Models Watch&lt;/a&gt;. The model looked good, but every mobile visitor downloaded it before deciding whether to interact with it.&lt;/p&gt;

&lt;p&gt;Three mobile Lighthouse runs produced this baseline:&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;Baseline median&lt;/th&gt;
&lt;th&gt;Final production median&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;53&lt;/td&gt;
&lt;td&gt;96&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LCP&lt;/td&gt;
&lt;td&gt;4.525 s&lt;/td&gt;
&lt;td&gt;2.653 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLS&lt;/td&gt;
&lt;td&gt;0.146&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TBT&lt;/td&gt;
&lt;td&gt;357 ms&lt;/td&gt;
&lt;td&gt;1 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Initial transfer&lt;/td&gt;
&lt;td&gt;about 9.1 MB&lt;/td&gt;
&lt;td&gt;about 370 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The final page still has the interactive 3D model. It simply does not load it until the visitor asks for it.&lt;/p&gt;

&lt;p&gt;This is a lab-tested case study, not field data. Chrome UX Report did not have enough real-user data for the site, so INP was unavailable. The final LCP also missed my stricter target of less than 2 seconds. That distinction matters: this was a large improvement, not a claim that performance work was finished.&lt;/p&gt;

&lt;p&gt;Here is what changed and, more importantly, how I found the work that actually mattered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Find the request that dominates the page
&lt;/h2&gt;

&lt;p&gt;The first audit could have led to a long list of generic fixes: compress images, reduce JavaScript, minify CSS, add caching, and so on.&lt;/p&gt;

&lt;p&gt;The network evidence pointed somewhere much more specific:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Initial page resources:                    ~9.1 MB
/generation-models/anisotropy-barn-lamp.glb: 7.83 MB
Share of measured resources:                83.7%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Optimizing several 30 KB assets while continuing to load a 7.8 MB model would not materially change the experience.&lt;/p&gt;

&lt;p&gt;The useful question was not “How do I make a GLB slightly smaller?” It was “Why is a mobile visitor downloading this GLB before interacting with the 3D viewer?”&lt;/p&gt;

&lt;p&gt;That changed the solution from asset tuning to load-path design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replace eager 3D with a mobile interaction boundary
&lt;/h2&gt;

&lt;p&gt;On mobile, the homepage now starts with a 19 KB AVIF cover. The cover has the same 4:3 geometry as the interactive stage, so switching from one to the other does not move surrounding content.&lt;/p&gt;

&lt;p&gt;The actual stage component is imported after a tap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Image&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;StageComponent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./HomeInteractiveStage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;HomeInteractiveStage&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;stagePromise&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;StageComponent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;requestInteractiveStage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;stagePromise&lt;/span&gt; &lt;span class="o"&gt;??=&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./HomeInteractiveStage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;module&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kr"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HomeInteractiveStage&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;stagePromise&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;HomeInteractiveStageClient&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;Stage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setStage&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;StageComponent&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;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&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;requested&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setRequested&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loadStage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&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="nf"&gt;setRequested&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="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;requestInteractiveStage&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;component&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="nf"&gt;setStage&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;component&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="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;requested&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;Stage&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Stage&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Loading 3D…&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;
      &lt;span class="na"&gt;aria-label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Load interactive 3D preview"&lt;/span&gt;
      &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;loadStage&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;
    &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Image&lt;/span&gt;
        &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;
        &lt;span class="na"&gt;fetchPriority&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"high"&lt;/span&gt;
        &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;576&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"eager"&lt;/span&gt;
        &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/home-lab/model-cover-mobile.avif"&lt;/span&gt;
        &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Tap to load · Drag to rotate&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;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 promise is cached at module level. Repeated clicks or renders reuse the same import instead of starting duplicate work.&lt;/p&gt;

&lt;p&gt;Desktop can still request the interactive stage automatically. The important product decision is that mobile users get a useful, stable first screen before paying for the 3D runtime and asset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lazy-load the runtime as well as the component
&lt;/h2&gt;

&lt;p&gt;Code-splitting the React component is not enough if another import pulls the viewer runtime into the initial route.&lt;/p&gt;

&lt;p&gt;I removed direct &lt;code&gt;@google/model-viewer&lt;/code&gt; imports from homepage and generation-output components. A shared loader now injects the self-hosted runtime only when a 3D component mounts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&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;selector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;script[data-model-viewer-runtime="true"]&lt;/span&gt;&lt;span class="dl"&gt;'&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;viewerPromise&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadModelViewer&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;customElements&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;model-viewer&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="nb"&gt;Promise&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;viewerPromise&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;viewerPromise&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;viewerPromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&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;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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existing&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="nx"&gt;querySelector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLScriptElement&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;selector&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;script&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;existing&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;script&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;load&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nx"&gt;customElements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;whenDefined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;model-viewer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;once&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="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&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="o"&gt;=&amp;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;3D runtime 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="na"&gt;once&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="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;existing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;modelViewerRuntime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/vendor/model-viewer.min.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&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;module&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;script&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;viewerPromise&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 loader solves three problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it keeps the runtime out of the initial mobile path;&lt;/li&gt;
&lt;li&gt;it prevents duplicate script elements;&lt;/li&gt;
&lt;li&gt;it gives every 3D surface one loading contract.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The walkable Three.js world was heavier still. Instead of embedding that entire runtime on the homepage, I moved it to a dedicated &lt;code&gt;/world-experience&lt;/code&gt; route. The homepage shows a preview and a clear entry point.&lt;/p&gt;

&lt;p&gt;The feature was not removed. Its cost was moved to the moment of intent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check route prefetch after adding lazy loading
&lt;/h2&gt;

&lt;p&gt;After deferring the component, I still saw generator code arrive earlier than expected.&lt;/p&gt;

&lt;p&gt;The first assumption was stale build output. The actual cause was Next.js route prefetching. Homepage calls to action and the mobile navigation linked to generator routes, so the browser downloaded route dependencies before the user navigated.&lt;/p&gt;

&lt;p&gt;For heavy destinations, I disabled automatic prefetch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/image-to-3d-model"&lt;/span&gt; &lt;span class="na"&gt;prefetch&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  Create your model
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an easy failure mode to miss. A component can be correctly lazy-loaded on its own route while a nearby &lt;code&gt;&amp;lt;Link&amp;gt;&lt;/code&gt; quietly warms that route and reintroduces the same cost.&lt;/p&gt;

&lt;p&gt;Prefetch is valuable when the prefetched destination is small and likely to be visited. It is not automatically a win when the route owns a large editor, authentication UI, task history, or 3D dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep global layouts boring
&lt;/h2&gt;

&lt;p&gt;The root layout originally had responsibilities that were not needed by anonymous homepage visitors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the global authentication provider;&lt;/li&gt;
&lt;li&gt;a deferred generation task center;&lt;/li&gt;
&lt;li&gt;advertising;&lt;/li&gt;
&lt;li&gt;analytics.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;“Deferred” components can still expand a route's dependency graph. A provider at the root makes every page pay part of its initialization cost.&lt;/p&gt;

&lt;p&gt;I moved account and task-center behavior closer to the routes that require it. Public marketing pages now render without initializing Clerk. Advertising and analytics wait until the browser is idle, with a timeout fallback, instead of joining the critical rendering path.&lt;/p&gt;

&lt;p&gt;The architectural rule is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Root layout: document shell and truly global UI
Public page: immediately useful content
Account routes: authentication runtime
Generator routes: editor and task dependencies
3D interaction: viewer and asset after intent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Route ownership is a performance feature. It stops one convenient global import from taxing every visitor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix layout stability at the boundary
&lt;/h2&gt;

&lt;p&gt;The baseline CLS median was 0.146. The 3D area and mobile sections did not reserve enough stable space before late content arrived.&lt;/p&gt;

&lt;p&gt;The cover and loaded stage now share fixed intrinsic dimensions and a 4:3 container. Loading text also renders inside that same box. The page no longer discovers the height of the hero after JavaScript runs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.interactiveStage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;aspect-ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;inline-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.stageCover&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nc"&gt;.stageLoading&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nc"&gt;.stageLoaded&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;block-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;inline-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&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 why the final CLS reached 0. The image format helped transfer size; intrinsic geometry fixed stability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Protect the loading boundary with tests
&lt;/h2&gt;

&lt;p&gt;Performance regressions often look harmless in code review. Someone restores a static viewer import, puts authentication back in the root layout, or removes &lt;code&gt;prefetch={false}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I added source-level tests for the invariants that must stay true:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mobile 3D stays behind an explicit request&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="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;gate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../src/components/home/HomeInteractiveStageClient.tsx&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;stage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../src/components/home/HomeInteractiveStage.tsx&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;loader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../src/lib/model-viewer-loader.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/onClick=&lt;/span&gt;&lt;span class="se"&gt;\{&lt;/span&gt;&lt;span class="sr"&gt;loadInteractiveStage&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="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;doesNotMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/import&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="sr"&gt;"@google&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;model-viewer"&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="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loader&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;vendor&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;model-viewer&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;min&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;js/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;statSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cover&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;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;the public shell does not initialize account UI&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="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;layout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../src/app/layout.tsx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;doesNotMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/ClerkProvider/&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;These tests do not replace Lighthouse, browser traces, or real-user monitoring. They stop known architectural regressions before another measurement is needed.&lt;/p&gt;

&lt;p&gt;The production release was also checked for behavior, not only scores:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no GLB request before interaction;&lt;/li&gt;
&lt;li&gt;no initial Clerk or Google-script request;&lt;/li&gt;
&lt;li&gt;the viewer runtime and GLB loaded successfully after a click;&lt;/li&gt;
&lt;li&gt;homepage, generator, sign-in, and world-experience routes still worked;&lt;/li&gt;
&lt;li&gt;the production build and complete test suite passed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the numbers actually prove
&lt;/h2&gt;

&lt;p&gt;The final three production Lighthouse runs reported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Performance: 97 / 91 / 96
LCP:         2.43 s / 2.94 s / 2.65 s
CLS:         0 / 0 / 0
TBT:         0 ms / 19 ms / 1 ms
Transfer:    ~370 KB
Requests:    43
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The improvement came from removing work, not making the browser execute the same work more efficiently.&lt;/p&gt;

&lt;p&gt;There are also clear limits to the result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;median LCP was 2.653 seconds, above the aggressive sub-2-second target;&lt;/li&gt;
&lt;li&gt;lab variance remained visible across runs;&lt;/li&gt;
&lt;li&gt;INP could not be evaluated without enough field data;&lt;/li&gt;
&lt;li&gt;the measurements describe this release and test setup, not a permanent guarantee.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the useful lesson. A dramatic payload reduction can coexist with unfinished LCP work. Report transfer, LCP, CLS, TBT, test conditions, and field-data availability separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist I would reuse
&lt;/h2&gt;

&lt;p&gt;For any homepage with interactive 3D, maps, editors, or other heavy media:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run the audit more than once and report the median.&lt;/li&gt;
&lt;li&gt;Sort initial network requests by transferred bytes.&lt;/li&gt;
&lt;li&gt;Ask whether the largest resource is required before interaction.&lt;/li&gt;
&lt;li&gt;Defer the component, runtime, and asset—not only one of the three.&lt;/li&gt;
&lt;li&gt;Inspect route prefetching for heavy destinations.&lt;/li&gt;
&lt;li&gt;Remove route-specific providers from the root layout.&lt;/li&gt;
&lt;li&gt;Reserve final geometry before client code runs.&lt;/li&gt;
&lt;li&gt;Verify the deferred feature still works after a real click.&lt;/li&gt;
&lt;li&gt;Add tests for the dependency boundaries you do not want to regress.&lt;/li&gt;
&lt;li&gt;Keep lab results separate from field data and from your target.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fastest 7.8 MB GLB is the one a visitor never has to download.&lt;/p&gt;

&lt;p&gt;The better product is not a homepage without 3D. It is a homepage that waits until the visitor says, “Show me the 3D.”&lt;/p&gt;




&lt;p&gt;You can explore the live project at &lt;a href="https://worldmodelswatch.com/" rel="noopener noreferrer"&gt;World Models Watch&lt;/a&gt;. Its public repository and technical documentation are available on &lt;a href="https://github.com/w66917759-commits/worldmodelswatch" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Keeping Photo Editor Previews and Exports in Sync with TypeScript and Sharp</title>
      <dc:creator>West</dc:creator>
      <pubDate>Mon, 07 Sep 2026 09:07:12 +0000</pubDate>
      <link>https://dev.to/west_dcaaa3a98181e80af749/keeping-photo-editor-previews-and-exports-in-sync-with-typescript-and-sharp-gil</link>
      <guid>https://dev.to/west_dcaaa3a98181e80af749/keeping-photo-editor-previews-and-exports-in-sync-with-typescript-and-sharp-gil</guid>
      <description>&lt;p&gt;A user positions a portrait inside a frame, checks the preview, and clicks Export. If the downloaded image crops their hair differently, the editor has broken a basic promise: the layout they approved should survive the export.&lt;/p&gt;

&lt;p&gt;I build &lt;a href="https://photocard.ai" rel="noopener noreferrer"&gt;Photocard.ai&lt;/a&gt;, a tool for creating custom photocards. Its Maker pipeline combines uploaded photos with template artwork. That means handling photo placement, background removal, decorative layers, and multiple output sizes.&lt;/p&gt;

&lt;p&gt;The useful architectural decision was to make those composition decisions explicit and save them in a &lt;strong&gt;versioned render plan&lt;/strong&gt;. Preview and high-resolution rendering can then consume the same decisions.&lt;/p&gt;

&lt;p&gt;This article focuses on that template renderer. The TypeScript examples are simplified illustrations of the design, with storage and application plumbing omitted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The goal is consistent composition across resolutions.&lt;/strong&gt; A lightweight browser preview can still differ from a final render in hair detail, filtering, and edge quality. Those are separate things to validate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Save the decisions that produced the image
&lt;/h2&gt;

&lt;p&gt;A template thumbnail shows the intended appearance. It cannot tell the renderer which source belongs in each slot, where to crop it, or which decoration belongs in front of the person.&lt;/p&gt;

&lt;p&gt;In the Maker implementation, a template manifest describes those rules. A render plan resolves them for a particular set of uploads.&lt;/p&gt;

&lt;p&gt;A reduced representation looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Rect&lt;/span&gt; &lt;span class="o"&gt;=&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="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RenderSlot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;sourceIndex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;sourceCrop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Rect&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// Relative to the oriented source image&lt;/span&gt;
  &lt;span class="nl"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Rect&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// Relative to the output canvas&lt;/span&gt;
  &lt;span class="nl"&gt;zIndex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;subjectAlphaKey&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;refinedForegroundKey&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;effectSeed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RenderPlan&lt;/span&gt; &lt;span class="o"&gt;=&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;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;templateVersionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;assetPackageVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;outputVariant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;slots&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RenderSlot&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 application has additional fields for clipping, filters, subject treatments, and edge processing. The principle stays small: save the information needed to explain the composition.&lt;/p&gt;

&lt;p&gt;When an export starts, it should load the saved crop. Running an automatic crop detector again creates another opportunity to choose a different face position.&lt;/p&gt;

&lt;p&gt;The same applies to decorative effects. If a paper-edge treatment uses randomness, save its seed so another render does not choose a different pattern.&lt;/p&gt;

&lt;p&gt;Version numbers also need something concrete behind them. Keep the corresponding artwork and processing behavior available for as long as old cards must remain editable. A version string cannot recover an overwritten asset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let each template slot choose its source
&lt;/h2&gt;

&lt;p&gt;A portrait inside a film frame may need the complete original photo. A sticker composition may need a transparent person.&lt;/p&gt;

&lt;p&gt;The manifest makes that choice explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SourceComposition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;original_photo&lt;/span&gt;&lt;span class="dl"&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;cutout_subject&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TemplateSlot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;sourceIndex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;sourceComposition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SourceComposition&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Rect&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;zIndex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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 prevents a subtle failure: applying background removal to a slot simply because a cached mask happens to exist.&lt;/p&gt;

&lt;p&gt;For the normal full-photo path, the renderer retains the scene. For a cutout slot, a missing required mask is a validation error. Explicit editing features, such as an outline mode, can introduce their own rules, but the presence of an optional asset should never decide the visual policy by accident.&lt;/p&gt;

&lt;p&gt;This matters for group photos, too. A single-person mask and the original group image contain different subjects. Choosing between them changes the content of the card.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate source coordinates from canvas coordinates
&lt;/h2&gt;

&lt;p&gt;There are two rectangles in a crop operation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;source crop&lt;/strong&gt; selects a region of the uploaded image.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;destination frame&lt;/strong&gt; places that region on the output canvas.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both can use normalized coordinates. An &lt;code&gt;x&lt;/code&gt; value of &lt;code&gt;0.1&lt;/code&gt; means ten percent of the relevant image's width. The source rectangle is measured against the oriented upload; the destination is measured against the canvas.&lt;/p&gt;

&lt;p&gt;Consider a destination frame of &lt;code&gt;{ x: 0.1, y: 0.1, width: 0.8, height: 0.8 }&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Canvas&lt;/th&gt;
&lt;th&gt;Left&lt;/th&gt;
&lt;th&gt;Top&lt;/th&gt;
&lt;th&gt;Frame width&lt;/th&gt;
&lt;th&gt;Frame height&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;600 × 900&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;480&lt;/td&gt;
&lt;td&gt;720&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2400 × 3600&lt;/td&gt;
&lt;td&gt;240&lt;/td&gt;
&lt;td&gt;360&lt;/td&gt;
&lt;td&gt;1920&lt;/td&gt;
&lt;td&gt;2880&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are illustrative sizes with the same aspect ratio. The placement scales without a new layout decision.&lt;/p&gt;

&lt;p&gt;Convert to pixels at the rendering boundary. This helper covers rectangles fully inside an image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;toPixels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Rect&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="kr"&gt;number&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="kr"&gt;number&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;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;rect&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;rect&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;rect&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;rect&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="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;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;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;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;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="o"&gt;||&lt;/span&gt;
    &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&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;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&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;rect&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="nx"&gt;rect&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="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;rect&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;gt;&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;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;rect&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;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;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;Invalid image dimensions or rectangle&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;left&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;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;top&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;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;right&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;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;rect&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;width&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;bottom&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;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;rect&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;height&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;right&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;bottom&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;top&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;Rectangle is smaller than a renderable pixel region&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;top&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;right&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;left&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;bottom&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;top&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;Rounding the boundaries keeps the rectangle's edges explicit. Allow for rounding differences when comparing different output resolutions.&lt;/p&gt;

&lt;p&gt;There are three other details to settle before this helper runs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Orientation:&lt;/strong&gt; normalize EXIF orientation before calculating crop coordinates. Otherwise, the browser and server can interpret the same photograph with different axes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Aspect ratio:&lt;/strong&gt; the crop planner must resolve the fit between the selected source region and the destination. Normalized coordinates alone do not prevent stretching. Sharp exposes distinct &lt;code&gt;cover&lt;/code&gt;, &lt;code&gt;contain&lt;/code&gt;, and &lt;code&gt;fill&lt;/code&gt; behaviors; choose deliberately when materializing the crop. &lt;a href="https://sharp.pixelplumbing.com/api-resize/" rel="noopener noreferrer"&gt;Sharp resize documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Padding:&lt;/strong&gt; a cutout editor may let someone shrink a person enough to reveal transparent space beyond the original image. Our crop system supports bounded virtual source rectangles for this. The small helper above deliberately handles only in-bounds rectangles; virtual crops need a padding step before extraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the layer order visible in code
&lt;/h2&gt;

&lt;p&gt;The composition has a readable order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Background artwork
    ↓
Artwork behind the subjects
    ↓
Prepared photo slots, ordered by zIndex
    ↓
Foreground artwork
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A title behind someone's head and a sticker overlapping their shoulder belong at different points in that sequence.&lt;/p&gt;

&lt;p&gt;With every slot already cropped, masked, treated, and placed onto a transparent canvas, the final compositing step can stay small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;sharp&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sharp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;compositeCard&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="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;behindSubject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;subjects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;png&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;zIndex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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="nl"&gt;foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// All inputs are already prepared at the same canvas dimensions.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subjects&lt;/span&gt; &lt;span class="o"&gt;=&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="nx"&gt;subjects&lt;/span&gt;&lt;span class="p"&gt;]&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;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;zIndex&lt;/span&gt; &lt;span class="o"&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;zIndex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sharp&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="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="nf"&gt;composite&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;input&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="nx"&gt;behindSubject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;left&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="na"&gt;top&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="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;subjects&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;png&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="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;png&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;left&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="na"&gt;top&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;input&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="nx"&gt;foreground&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;left&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="na"&gt;top&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="p"&gt;])&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;png&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBuffer&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 array order is part of the rendering contract. Sharp also requires overlay images to be no larger than the processed base image. Preparing each layer at the target canvas dimensions makes that constraint straightforward to check. &lt;a href="https://sharp.pixelplumbing.com/api-composite/" rel="noopener noreferrer"&gt;Sharp composite documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Filters deserve an equally explicit boundary. If a preset should affect the portrait, apply it to the portrait before composition. Applying it to the flattened result also changes the frame, typography, and decorations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep alpha and foreground color separate
&lt;/h2&gt;

&lt;p&gt;A clean-looking mask can still produce a pale fringe around someone's hair.&lt;/p&gt;

&lt;p&gt;The mask describes coverage. The source RGB may still contain color from the old background, especially around partially transparent edges.&lt;/p&gt;

&lt;p&gt;The preparation pipeline therefore keeps separate artifacts:&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;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Oriented source&lt;/td&gt;
&lt;td&gt;Stable coordinates and original image detail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subject alpha&lt;/td&gt;
&lt;td&gt;Continuous coverage around hair and soft edges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refined foreground&lt;/td&gt;
&lt;td&gt;Foreground color with background contamination reduced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effect silhouette&lt;/td&gt;
&lt;td&gt;Shape used for borders, shadows, and cutout treatments&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These assets need consistent coordinate alignment, even when a renderer resamples them to different resolutions.&lt;/p&gt;

&lt;p&gt;A hard sticker border and a soft strand of hair have different requirements. Deriving an effect silhouette lets the renderer make a stronger outline without hard-thresholding the alpha used for the actual portrait.&lt;/p&gt;

&lt;p&gt;The render plan references the prepared artifacts and mask revision. A retry can reuse that preparation instead of silently asking another segmentation pass to interpret the person again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define what each preview promises
&lt;/h2&gt;

&lt;p&gt;The current local editor work separates a fast browser preview from higher-quality preparation during generation.&lt;/p&gt;

&lt;p&gt;That distinction needs to remain visible in both the architecture and the interface:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Render&lt;/th&gt;
&lt;th&gt;Main purpose&lt;/th&gt;
&lt;th&gt;Expected limitation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Interactive browser preview&lt;/td&gt;
&lt;td&gt;Adjust position and scale&lt;/td&gt;
&lt;td&gt;Approximate masks and effects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server-rendered preview&lt;/td&gt;
&lt;td&gt;Inspect the prepared composition&lt;/td&gt;
&lt;td&gt;Lower output resolution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High-resolution export&lt;/td&gt;
&lt;td&gt;Produce the final file&lt;/td&gt;
&lt;td&gt;Detail remains limited by source quality&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The browser draft can use local photo URLs and a lightweight mask. The authoritative render uses prepared assets and the saved composition rules.&lt;/p&gt;

&lt;p&gt;In the server renderer, preview and HD output share the composition function. The target canvas and resolution-specific assets change. An HD export recomposes the sources and template layers at the requested size.&lt;/p&gt;

&lt;p&gt;That is different from enlarging a flattened preview, but it still cannot manufacture missing detail in a small upload. A large output canvas describes file dimensions; it does not prove that every input contains that much detail.&lt;/p&gt;

&lt;p&gt;Shared geometry also does not guarantee pixel-identical results after downscaling. Resampling, edge refinement, and resolution-dependent effects still need visual checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the rendering promises
&lt;/h2&gt;

&lt;p&gt;Synthetic images make useful tests because their expected pixels are easy to reason about.&lt;/p&gt;

&lt;p&gt;A solid blue background, a red subject, and a green foreground patch can verify layer order without depending on a real portrait. A soft alpha edge can check whether filtering preserves transparency.&lt;/p&gt;

&lt;p&gt;The repository includes tests for shared crop geometry at preview and HD sizes, original-photo behavior when a matte exists, EXIF orientation, alpha preservation, and behind-subject versus foreground ordering.&lt;/p&gt;

&lt;p&gt;For a similar editor, I would start with these checks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Render one saved crop at two resolutions and compare normalized placement within a rounding tolerance.&lt;/li&gt;
&lt;li&gt;Give a full-photo slot an available mask and confirm that its normal rendering path still preserves the complete image.&lt;/li&gt;
&lt;li&gt;Place a subject across behind-subject and foreground artwork, then inspect known overlap pixels.&lt;/li&gt;
&lt;li&gt;Apply a portrait filter and check that the template artwork stays unchanged.&lt;/li&gt;
&lt;li&gt;Remove a required cutout artifact and confirm that rendering fails instead of producing a different composition.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then inspect real photos: loose hair, dark clothing, busy backgrounds, unusual framing, and groups. Geometry tests can establish where the subject lands. They cannot establish whether the hair edge looks convincing.&lt;/p&gt;

&lt;p&gt;If you are building a template editor, start by saving one source crop, one destination frame, and one template version. Render that plan at two sizes before adding more effects. It gives you a concrete contract to preserve as the editor grows.&lt;/p&gt;

&lt;p&gt;These are the rendering ideas behind the Maker in &lt;a href="https://photocard.ai/kpop-photocard-maker" rel="noopener noreferrer"&gt;Photocard.ai&lt;/a&gt;. The templates provide the artwork; the render plan records how each photo belongs inside it.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>software</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
