<?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: Rachid</title>
    <description>The latest articles on DEV Community by Rachid (@rachido_sama).</description>
    <link>https://dev.to/rachido_sama</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%2F3978239%2Fa8e109b1-7865-4cb7-97f0-7ecac664e6b3.jpg</url>
      <title>DEV Community: Rachid</title>
      <link>https://dev.to/rachido_sama</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rachido_sama"/>
    <language>en</language>
    <item>
      <title>title: "How We Built a Scalable Multi-Service Marketplace: Architecture &amp; Lessons Learned"</title>
      <dc:creator>Rachid</dc:creator>
      <pubDate>Tue, 04 Aug 2026 09:48:10 +0000</pubDate>
      <link>https://dev.to/rachido_sama/title-how-we-built-a-scalable-multi-service-marketplace-architecture-lessons-learned-3ia8</link>
      <guid>https://dev.to/rachido_sama/title-how-we-built-a-scalable-multi-service-marketplace-architecture-lessons-learned-3ia8</guid>
      <description>&lt;p&gt;Connecting service providers with homeowners in real time comes with tough technical challenges: asynchronous job requests, local geofencing, real-time status updates, and scaling database indexing for dozens of categoris.&lt;/p&gt;

&lt;p&gt;When we built &lt;strong&gt;Bricole&lt;/strong&gt;, our goal was to create a fast, seamless service marketplace where users can connect directly with verified local professionals—from plumbing and electrical work to home healthcare.&lt;/p&gt;

&lt;p&gt;In this article, Ill break down the architectural choices, tech stack, and key technical lessons we learned along the way.&lt;/p&gt;





&lt;div class="ltag-features"&gt;
  &lt;div class="ltag-feature"&gt;
    &lt;div class="ltag-feature__icon"&gt;
          
    &lt;/div&gt;
  &lt;div class="ltag-feature__content"&gt;
    &lt;div class="ltag-feature__title"&gt;Real-Time Matching&lt;/div&gt;
    &lt;div class="ltag-feature__body"&gt;

      &lt;p&gt;Instant connection between users and local service providers.&lt;/p&gt;


    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;div class="ltag-feature"&gt;
    &lt;div class="ltag-feature__icon"&gt;
          
    &lt;/div&gt;
  &lt;div class="ltag-feature__content"&gt;
    &lt;div class="ltag-feature__title"&gt;Scalable Backend&lt;/div&gt;
    &lt;div class="ltag-feature__body"&gt;

      &lt;p&gt;Cloud infrastructure structured for high concurrency and regional queries.&lt;/p&gt;


    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



&lt;/div&gt;


&lt;h2&gt;
  
  
  🛠️ The Tech Stack Overview
&lt;/h2&gt;

&lt;p&gt;To balance mobile performance,rapid iterations, and developer velocity, we settled on a modern stack:&lt;/p&gt;


&lt;div class="ltag-row"&gt;
  &lt;div class="ltag-col ltag-col-span-2 ltag-col-background-subtle"&gt;
  &lt;p&gt;&lt;strong&gt;Mobile Frontend&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Native Android built with &lt;strong&gt;Jetpack Compose&lt;/strong&gt; and &lt;strong&gt;Material Design 3&lt;/strong&gt;, giving us declarative UI rendering and smooth 60fps layout animations .&lt;/p&gt;

&lt;/div&gt;


&lt;div class="ltag-col ltag-col-span-2 ltag-col-background-default"&gt;
  &lt;p&gt;&lt;strong&gt;Backend &amp;amp; Infra&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Powered by &lt;strong&gt;Firebase&lt;/strong&gt; (Firestore, Cloud Functions, Auth) for real-time data sync and rapid API scalability across service hubs.&lt;/p&gt;

&lt;/div&gt;



&lt;/div&gt;





&lt;p&gt;&lt;strong&gt;Key Architectural Challenges&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. High-Performance Mobile UI State
&lt;/h3&gt;

&lt;p&gt;Managing asynchronous UI updates across job creation, provider tracking, and active status feeds required strict state boundaries. By leveraging Jetpack Compose state hoist strategies, we minimized unnecessary re-renders when polling position or processing updates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Category &amp;amp; Location Indexing&lt;/strong&gt;&lt;br&gt;
A marketplace with categories ranging from plumbing to home care needs to serve fast search results without triggering heavy database reads. We restructured our database schemas to decouple static provider data from dynamic availability indexes.&lt;/p&gt;



&lt;p&gt;&lt;a href="link=" class="crayons-btn crayons-btn--primary"&gt;Explore the platform on Bricole App&lt;/a&gt;
&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Lessons Learned&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Keep Web &amp;amp; App Roles Clear&lt;/strong&gt;: Use your web footprint as a high-converting gateway for discovery and SEO, while keeping deep provider interactions inside a responsive mobile application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimize for Core Web Vitals Early&lt;/strong&gt;: Web landing pages and gateway routes must load rapidly to maximize app install conversion rates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decouple Dynamic Data&lt;/strong&gt;: Static metadata (service descriptions, standard pricing) should be aggressively cached, keeping real-time listeners strictly for active requests.&lt;/li&gt;
&lt;/ol&gt;





&lt;div class="ltag-offer"&gt;
  &lt;div class="ltag-offer__body"&gt;
&lt;br&gt;
Looking for verified home services or interested in our engineering approach? Check out Bricole!&lt;br&gt;
&lt;/div&gt;
    &lt;a href="https://bricoleapp.com" class="ltag-offer__button crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;Visit Bricole&lt;/a&gt;
&lt;/div&gt;


</description>
      <category>webdev</category>
      <category>android</category>
      <category>architecture</category>
      <category>showdev</category>
    </item>
    <item>
      <title>The 8 Performance Fixes That Finally Got My Astro Site to 100 on Mobile PageSpeed</title>
      <dc:creator>Rachid</dc:creator>
      <pubDate>Sat, 01 Aug 2026 19:49:33 +0000</pubDate>
      <link>https://dev.to/rachido_sama/the-8-performance-fixes-that-finally-got-my-astro-site-to-100-on-mobile-pagespeed-36fj</link>
      <guid>https://dev.to/rachido_sama/the-8-performance-fixes-that-finally-got-my-astro-site-to-100-on-mobile-pagespeed-36fj</guid>
      <description>&lt;p&gt;Still trying to hit that elusive &lt;strong&gt;100 on PageSpeed Insights&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;I was too.&lt;/p&gt;

&lt;p&gt;I spent far longer than I'd like to admit stuck in the high 80s and low 90s on a real client project. I tried everything the internet recommends—compressing images, minifying JavaScript, lazy loading, removing unused CSS—and yet the score barely moved.&lt;/p&gt;

&lt;p&gt;Mobile was the real challenge. That's where Lighthouse seems determined to humble you.&lt;/p&gt;

&lt;p&gt;Eventually we got there on &lt;strong&gt;waxedoc.com&lt;/strong&gt;, a waxing studio website I built with Astro.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mobile PageSpeed:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ 100 Performance&lt;/li&gt;
&lt;li&gt;✅ 100 Accessibility&lt;/li&gt;
&lt;li&gt;✅ 100 Best Practices&lt;/li&gt;
&lt;li&gt;✅ 100 SEO&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(All tested in an incognito window so browser extensions weren't affecting the results.)&lt;/p&gt;

&lt;p&gt;The interesting part is that there wasn't one magic optimization.&lt;/p&gt;

&lt;p&gt;It was a collection of small improvements that, together, made a huge difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Start with a framework that ships less JavaScript
&lt;/h2&gt;

&lt;p&gt;This almost feels unfair, but it's true.&lt;/p&gt;

&lt;p&gt;Astro renders static HTML by default and only hydrates the components you explicitly make interactive through Islands Architecture.&lt;/p&gt;

&lt;p&gt;For most pages on this site, the browser downloads almost no JavaScript during the initial load.&lt;/p&gt;

&lt;p&gt;If you're building a content-heavy website, that gives you a head start before you've optimized a single line of code.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Stop loading fonts from Google Fonts
&lt;/h2&gt;

&lt;p&gt;This change had a much bigger impact than I expected.&lt;/p&gt;

&lt;p&gt;Using the standard Google Fonts &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; means the browser has to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;connect to &lt;code&gt;fonts.googleapis.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;download the CSS&lt;/li&gt;
&lt;li&gt;connect again to &lt;code&gt;fonts.gstatic.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;finally download the font files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's multiple network requests before your custom font even starts loading.&lt;/p&gt;

&lt;p&gt;Instead, I downloaded the &lt;code&gt;.woff2&lt;/code&gt; files and hosted them locally.&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="k"&gt;@font-face&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"Playfair Display"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;600&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;font-display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;swap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url("/fonts/playfair-display-600.woff2")&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;"woff2"&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;&lt;code&gt;font-display: swap&lt;/code&gt; is just as important—it allows text to appear immediately using a fallback font instead of staying invisible while the custom font downloads.&lt;/p&gt;


&lt;h2&gt;
  
  
  3. Preload only the fonts you actually use
&lt;/h2&gt;

&lt;p&gt;One thing I learned is that browsers don't immediately download fonts just because they see an &lt;code&gt;@font-face&lt;/code&gt; rule.&lt;/p&gt;

&lt;p&gt;They first need to discover that an element actually uses that font.&lt;/p&gt;

&lt;p&gt;Adding preload hints for fonts used above the fold helped reduce LCP.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt;
  &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"preload"&lt;/span&gt;
  &lt;span class="na"&gt;as=&lt;/span&gt;&lt;span class="s"&gt;"font"&lt;/span&gt;
  &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"font/woff2"&lt;/span&gt;
  &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/fonts/playfair-display-600.woff2"&lt;/span&gt;
  &lt;span class="na"&gt;crossorigin&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;One warning though: only preload fonts that are genuinely needed.&lt;/p&gt;

&lt;p&gt;I had two font files declared in CSS that weren't used anywhere. Preloading them only wasted bandwidth and triggered Lighthouse warnings about unused preloads.&lt;/p&gt;


&lt;h2&gt;
  
  
  4. Inline your critical CSS
&lt;/h2&gt;

&lt;p&gt;External stylesheets block rendering.&lt;/p&gt;

&lt;p&gt;Until the browser downloads and parses them, it can't paint the page.&lt;/p&gt;

&lt;p&gt;Astro makes this surprisingly easy.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// astro.config.mjs&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;inlineStylesheets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;always&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That single setting removes an extra network request before the first paint.&lt;/p&gt;


&lt;h2&gt;
  
  
  5. Give your LCP image real responsive images
&lt;/h2&gt;

&lt;p&gt;This is one of the easiest mistakes to make.&lt;/p&gt;

&lt;p&gt;Adding a &lt;code&gt;sizes&lt;/code&gt; attribute by itself isn't enough.&lt;/p&gt;

&lt;p&gt;Without a proper &lt;code&gt;srcset&lt;/code&gt;, the browser still has only one image to choose from—which usually means your phone downloads the same massive image your desktop uses.&lt;/p&gt;

&lt;p&gt;Astro's &lt;code&gt;&amp;lt;Image&amp;gt;&lt;/code&gt; component makes this straightforward.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;Image
  src={image}
  width={image.width}
  height={image.height}
  widths={[480, 640, 768, 1024, image.width]}
  sizes="(min-width: 1024px) 50vw, 100vw"
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;For your Largest Contentful Paint image (usually the hero), also add:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;fetchpriority="high"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That tells the browser to prioritize downloading it instead of discovering it halfway through the waterfall.&lt;/p&gt;


&lt;h2&gt;
  
  
  6. Configure proper cache headers
&lt;/h2&gt;

&lt;p&gt;This one's easy to overlook.&lt;/p&gt;

&lt;p&gt;Hashed assets generated during your build are immutable, so they can safely be cached for an entire year.&lt;/p&gt;

&lt;p&gt;On Firebase Hosting I added:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"**/_astro/**"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cache-Control"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"public, max-age=31536000, immutable"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It's basically free performance.&lt;/p&gt;


&lt;h2&gt;
  
  
  7. Don't animate &lt;code&gt;box-shadow&lt;/code&gt; or &lt;code&gt;text-shadow&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This one caught me by surprise.&lt;/p&gt;

&lt;p&gt;The animations looked perfectly smooth, but Lighthouse flagged them as non-composited animations.&lt;/p&gt;

&lt;p&gt;Animating shadows forces the browser to repaint every frame.&lt;/p&gt;

&lt;p&gt;A better approach is keeping the shadow static and only animating &lt;code&gt;opacity&lt;/code&gt; or &lt;code&gt;transform&lt;/code&gt;.&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;.neon&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;text-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;6px&lt;/span&gt; &lt;span class="n"&gt;currentColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flicker&lt;/span&gt; &lt;span class="m"&gt;3s&lt;/span&gt; &lt;span class="n"&gt;infinite&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.neon&lt;/span&gt;&lt;span class="nd"&gt;::after&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data-text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;text-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;14px&lt;/span&gt; &lt;span class="n"&gt;currentColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flicker-glow&lt;/span&gt; &lt;span class="m"&gt;3s&lt;/span&gt; &lt;span class="n"&gt;infinite&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;Same visual effect.&lt;/p&gt;

&lt;p&gt;Less work for the browser.&lt;/p&gt;


&lt;h2&gt;
  
  
  8. Avoid forced synchronous layouts
&lt;/h2&gt;

&lt;p&gt;If you're repeatedly calling methods like &lt;code&gt;getBoundingClientRect()&lt;/code&gt; during scrolling or dragging, you're probably forcing the browser to recalculate layout dozens of times every second.&lt;/p&gt;

&lt;p&gt;Instead, read those values once and reuse them.&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;let&lt;/span&gt; &lt;span class="nx"&gt;box&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="nx"&gt;frame&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;pointerdown&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="nx"&gt;box&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;frame&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;pointermove&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="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;ticking&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;requestAnimationFrame&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="c1"&gt;// use the cached box&lt;/span&gt;
      &lt;span class="nx"&gt;ticking&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It doesn't sound exciting, but these little changes add up.&lt;/p&gt;


&lt;h2&gt;
  
  
  What actually got us to 100
&lt;/h2&gt;

&lt;p&gt;There wasn't a single optimization that suddenly added 20 Lighthouse points.&lt;/p&gt;

&lt;p&gt;It was fixing one small issue after another.&lt;/p&gt;

&lt;p&gt;Fonts loaded a little faster.&lt;/p&gt;

&lt;p&gt;Images became a little smaller.&lt;/p&gt;

&lt;p&gt;Rendering started a little earlier.&lt;/p&gt;

&lt;p&gt;JavaScript blocked the main thread a little less.&lt;/p&gt;

&lt;p&gt;After enough of those improvements, the score finally reached 100.&lt;/p&gt;

&lt;p&gt;More importantly, the site genuinely feels faster—not just in Lighthouse, but to real visitors.&lt;/p&gt;

&lt;p&gt;If you'd rather see these optimizations on a real production website than a stripped-down demo:&lt;/p&gt;


&lt;div class="ltag-offer"&gt;
  &lt;div class="ltag-offer__body"&gt;
&lt;br&gt;
See the actual site these optimizations shipped on—a live Astro-powered business website that consistently scores 100 on mobile PageSpeed Insights.&lt;br&gt;
&lt;/div&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;a href="https://waxedoc.com" class="ltag-offer__button crayons-btn crayons-btn--primary"&amp;gt;Visit Waxed OC&amp;lt;/a&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;I'd love to know: which Lighthouse audit has been the hardest for you to fix lately? Fonts? Images? Third-party scripts? Or something completely different?&lt;/p&gt;

</description>
      <category>astro</category>
      <category>performance</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Astro Is Built for Thousands of Static Pages. So Why Does Nobody Talk About It That Way?</title>
      <dc:creator>Rachid</dc:creator>
      <pubDate>Fri, 31 Jul 2026 15:39:29 +0000</pubDate>
      <link>https://dev.to/rachido_sama/astro-is-built-for-thousands-of-static-pages-so-why-does-nobody-talk-about-it-that-way-2pc4</link>
      <guid>https://dev.to/rachido_sama/astro-is-built-for-thousands-of-static-pages-so-why-does-nobody-talk-about-it-that-way-2pc4</guid>
      <description>&lt;p&gt;Most &lt;strong&gt;Astro&lt;/strong&gt; content I come across falls into one of two buckets: "here's a nice  website I built" or "here's how Astro compares to Next.js for a marketing page." Both fair, both true. But the site I work on doesn't look like either of those, and it's the use case I almost never see written about: hundreds of near-identical, dynamically generated pages, all needing to rank in search, all needing to load fast, in two languages and two markets.&lt;/p&gt;

&lt;p&gt;Concretely: a home-services marketplace with city pages, service pages, and city × service combination page — the kind of programmatic SEO structure where the page count isn't in the dozens, it's in the hundreds today and headed toward tens of thouisands as the underlying data grows. That's a genuinely different problem than "make one fast landing page," and it turns out Astro is unusually well suited to it — better suited, in some concrete ways, than the frameworks that get all the programmatic-SEO blog posts.&lt;/p&gt;

&lt;p&gt;The default that actually matters: zero JS unless you ask for it&lt;/p&gt;

&lt;p&gt;Every "why Astro" post mentions the islands architecture. What gets undersold is what that means specifically for a large templated site: a city × service page — heading, worker cards, an FAQ block, some internal links — ships as plain HTML with no JavaScript bundle at all, by default. Not "a smaller bundle." None.&lt;/p&gt;

&lt;p&gt;Compare that to a static export from a React-based meta-framework, where every page is still a component tree meant to hydrate, so you're shipping (and executing) framework runtime JS even for a page that never needs to do anything client-side. At one page, that's not a big deal. At a few hundred templated pages, each with real content and real images, it's the difference between a site that stays fast as it grows and one that needs constant bundle-splitting effort just to stand still.&lt;/p&gt;

&lt;p&gt;In Astro, the handful of things that genuinely need JS — a filter widget, a search box — opt in explicitly, and you choose when:&lt;/p&gt;

&lt;p&gt;astro&lt;br&gt;
&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;Everything else on the page is never shipped as JS at all, because it was never a component that needed to run in the browser — it just needed to render once, at build time.&lt;/p&gt;

&lt;p&gt;Static generation that scales past "a few pages"&lt;/p&gt;

&lt;p&gt;getStaticPaths() is the other underrated piece. It's not fundamentally different from what other SSGs offer, but in practice, generating a page per city, per service, and per city×service combination from plain data (JSON, a CMS, eventually a database) is exactly the shape of problem Astro's routing was designed around — and it stays boring at scale, which is what you want from build tooling. Add a new city to your data source, and the page exists at the next build with no template changes.&lt;/p&gt;

&lt;p&gt;The place this gets interesting is exactly where it gets hard: once your data source stops being "a hundred hand-checked records" and starts being "tens of thousands of records from a real backend," you have to get deliberate about what actually deserves a static page versus what should be batched, tiered, or deferred — completeness of the underlying data becomes a real input into your routing strategy, not just an afterthought. That's less of an Astro-specific lesson and more a reminder that static-site scale problems are data problems first, framework problems second.&lt;/p&gt;

&lt;p&gt;The sharp edge nobody warns you about&lt;/p&gt;

&lt;p&gt;I wrote up one of these in detail recently: Astro's &lt;a href="" class="article-body-image-wrapper"&gt;&lt;img&gt;&lt;/a&gt; component can't gracefully handle a dead remote URL, because the actual network fetch happens inside an internal step that isn't part of the pluggable Image Service API — so one broken image URL in your dataset can take down an entire production build. Not a dealbreaker, but a real gap between "framework magic" and "framework you should validate your inputs before trusting."&lt;/p&gt;

&lt;p&gt;The fix ended up being a small prebuild script that checks every remote image URL's reachability before Astro ever touches it, with a fallback UI for anything that fails the check. Worth knowing about before it takes down a deploy at 6pm on a Friday, not after.&lt;/p&gt;

&lt;p&gt;Where it's genuinely not the right tool&lt;/p&gt;

&lt;p&gt;To be fair to the frameworks that do get all the blog posts: Astro is not the right choice for the actual application layer. Our app itself is a separate native app; the Astro site is purely the public-facing, SEO-critical surface — city pages, service pages, profile pages. If you need authenticated dashboards, real-time state, or heavy client-side interactivity as the core product, reach for something built around that, not a static-first framework. Astro plays well as the SEO/marketing front door for a product that lives somewhere else, which is exactly our setup.&lt;/p&gt;

&lt;p&gt;Why the silence, then?&lt;/p&gt;

&lt;p&gt;My honest guess: programmatic SEO at real scale is a less glamorous problem than "look at this beautiful blog," so it doesn't get the same content. But if you're building a site where the page count is the whole point — city pages, product category pages, location pages, anything templated across a large dataset — the zero-JS-by-default model is doing more work for you than it gets credit for.&lt;/p&gt;

&lt;p&gt;We run this pattern on &lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag-offer"&gt;
  &lt;div class="ltag-offer__body"&gt;...&lt;/div&gt;
    &lt;a href="https://bricoleapp.com" class="ltag-offer__button crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;Visit Bricole&lt;/a&gt;
&lt;/div&gt;
 , &lt;br&gt;
a bilingual marketplace across two North African markets — happy to go deeper on the pSEO/data side if that's useful to anyone hitting the same wall.

</description>
    </item>
    <item>
      <title>The Astro Image Gotcha That Broke Our Production Build and How We Fixed It</title>
      <dc:creator>Rachid</dc:creator>
      <pubDate>Thu, 30 Jul 2026 23:07:05 +0000</pubDate>
      <link>https://dev.to/rachido_sama/the-astro-image-gotcha-that-broke-our-production-build-and-how-we-fixed-it-566d</link>
      <guid>https://dev.to/rachido_sama/the-astro-image-gotcha-that-broke-our-production-build-and-how-we-fixed-it-566d</guid>
      <description>&lt;p&gt;We ship a bilingual (French/Arabic) marketplace site built on Astro — static output, hundreds of location plus service pages, and a few thousand professional profile pages, each with a photo pulled from Firebase Storage. Nothing exotic. Then one day the production build just... died. No warning during local dev, no failing test, no lint error. npm run build exited non-zero, and the stack trace pointed at astro:assets.&lt;/p&gt;

&lt;p&gt;The cause, once I dug in: one profile's photo URL had gone stale. A Firebase Storage download token had rotated, so a request that used to return a JPEG now returned a 403. That's it. One dead URL, out of a few thousand, took the entire build down with it.&lt;/p&gt;

&lt;p&gt;That surprised me, because I assumed Astro's image pipeline would fail soft on a bad remote image — log a warning, skip it, move on. It doesn't. Here's why, and what we did about it.&lt;/p&gt;

&lt;p&gt;Why &lt;a href="" class="article-body-image-wrapper"&gt;&lt;img&gt;&lt;/a&gt; can't catch this&lt;/p&gt;

&lt;p&gt;Astro's &lt;a href="" class="article-body-image-wrapper"&gt;&lt;img&gt;&lt;/a&gt; component and the underlying getImage() function both hand off to an Image Service, whose job is to transform() the image — resize it, convert format, whatever your config says. That's the part you can hook into and customize.&lt;/p&gt;

&lt;p&gt;The problem is that transform() only ever receives an image that's already been fetched, as a raw buffer. The actual network request for a remote src happens earlier inside Astro's own internal loadRemoteImage step — whixh isn't part of the public, pluggable Image Service API. There's no supported hook that sits between "here's a remote URL" and "here's the fetched buffer" where you can intercept a failed request and decide what to do about it.&lt;/p&gt;

&lt;p&gt;So from the framework's point of view, a 403 or a timeout on a remote image isn't a "handle this gracefully" situation — it's a fatal error during static generation. If even one image URL in your dataset is dead, the whole build stops.&lt;/p&gt;

&lt;p&gt;The two-minute fix (and why it's not enough)&lt;/p&gt;

&lt;p&gt;The fastest way out is obvious: swap &lt;a href="" class="article-body-image-wrapper"&gt;&lt;img&gt;&lt;/a&gt; for a plain &lt;a href="" class="article-body-image-wrapper"&gt;&lt;img&gt;&lt;/a&gt; tag. That sidesteps Astro's build-time fetch-and-transform step entirely, because a plain &lt;a href="" class="article-body-image-wrapper"&gt;&lt;img&gt;&lt;/a&gt; just emits the URL as-is and lets the browser deal with it at runtime.&lt;/p&gt;

&lt;p&gt;That got our deploys unblocked immediately, but it's a real trade-off, not a fix — you lose automatic resizing, format conversion (WebP/AVIF), and the responsive srcset handling that's the entire reason to use &lt;a href="" class="article-body-image-wrapper"&gt;&lt;img&gt;&lt;/a&gt; in the first place. Fine as a stopgap. Not something to leave in place.&lt;/p&gt;

&lt;p&gt;The actual fix: check before you build, not during&lt;/p&gt;

&lt;p&gt;The real problem wasn't that Astro handles failures badly — it's that we were handing Astro URLs we hadn't verified. So we moved the verification earlier, into a prebuild step that runs before Astro ever touches an image.&lt;/p&gt;

&lt;p&gt;The script is small: collect every unique remote image URL your pages will reference, HEAD-check each one with a timeout, and write the ones that fail to a JSON manifest.&lt;/p&gt;

&lt;p&gt;js&lt;br&gt;
// scripts/validate-image-urls.mjs&lt;br&gt;
import fs from "node:fs";&lt;/p&gt;

&lt;p&gt;const TIMEOUT_MS = 8000;&lt;br&gt;
const CONCURRENCY = 10;&lt;/p&gt;

&lt;p&gt;async function checkUrl(url) {&lt;br&gt;
  const controller = new AbortController();&lt;br&gt;
  const timer = setTimeout(() =&amp;gt; controller.abort(), TIMEOUT_MS);&lt;br&gt;
  try {&lt;br&gt;
    const res = await fetch(url, { method: "HEAD", signal: controller.signal });&lt;br&gt;
    return res.ok;&lt;br&gt;
  } catch {&lt;br&gt;
    return false; // timeout, DNS failure, connection reset — all treated as "bad"&lt;br&gt;
  } finally {&lt;br&gt;
    clearTimeout(timer);&lt;br&gt;
  }&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Two design decisions mattered more than the code itself:&lt;/p&gt;

&lt;p&gt;Fail closed per URL, fail open in aggregate. If a single URL times out or errors, treat it as bad — that's the safe default for one image. But if, say, 70% of all URLs fail in the same run, that's not "most of our images died overnight," that's almost certainly the checker itself being broken — offline CI runner, DNS hiccup, rate limiting. In that case the script leaves the existing manifest untouched rather than blanking every photo on the site. A partial network failure in your validation step shouldn't be able to nuke your production content.&lt;/p&gt;

&lt;p&gt;Hook it in with prebuild, not an Astro integration. We wired this through npm's own lifecycle ("prebuild": "node scripts/validate-image-urls.mjs" in package.json), which runs automatically before build. We deliberately skipped writing it as an Astro integration hook — it's simpler, it's fully decoupled from Astro's internal build order, and it'd survive a framework migration untouched.&lt;/p&gt;

&lt;p&gt;On the component side, it's a one-line check:&lt;/p&gt;

&lt;h2&gt;
  
  
  astro
&lt;/h2&gt;

&lt;p&gt;import { isBadImageUrl } from "../utils/badImageUrls";&lt;/p&gt;

&lt;h2&gt;
  
  
  const showFallback = isBadImageUrl(profile.thumbnail);
&lt;/h2&gt;

&lt;p&gt;{showFallback&lt;br&gt;
  ? /* render an initials avatar instead &lt;em&gt;/ null&lt;br&gt;
  : /&lt;/em&gt; render the optimized Image component with profile.thumbnail as the source */ null}&lt;/p&gt;

&lt;p&gt;Known-bad URLs get a graceful fallback (initials avatar) instead of ever reaching &lt;a href="" class="article-body-image-wrapper"&gt;&lt;img&gt;&lt;/a&gt;. Everything else gets the full optimization pipeline as normal.&lt;/p&gt;

&lt;p&gt;Does it actually work at scale?&lt;/p&gt;

&lt;p&gt;The first real production run checked several thousand unique image URLs against the live network in a few seconds thanks to the concurrency limit, and it did exactly what it was supposed to: flagged a handful of genuinely dead URLs, left everything else alone, and the build stopped depending on the uptime of every single image host on the internet.&lt;/p&gt;

&lt;p&gt;One small habit that paid off while shipping this: when I reverted the emergency &lt;a href="" class="article-body-image-wrapper"&gt;&lt;img&gt;&lt;/a&gt; fallback back to &lt;a href="" class="article-body-image-wrapper"&gt;&lt;img&gt;&lt;/a&gt; once the validator was in place, I diffed against git history (git show HEAD:path/to/file) instead of trusting my memory of what the original props looked like. Cheap insurance — worth doing any time you're undoing an emergency fix under time pressure.&lt;/p&gt;

&lt;p&gt;The takeaway&lt;/p&gt;

&lt;p&gt;If you're using Astro's &lt;a href="" class="article-body-image-wrapper"&gt;&lt;img&gt;&lt;/a&gt; or getImage() with remote sources you don't fully control — user uploads, a CMS, cloud storage — don't assume a bad URL fails gracefully. It doesn't, by design, because the fetch happens in a part of the pipeline you can't hook into. Validate before the build starts, not during it.&lt;/p&gt;

&lt;p&gt;We run this on bricoleapp.com, a bilingual home-services marketplace for Morocco and Egypt — happy to go deeper on any part of this if it's useful.&lt;/p&gt;

</description>
      <category>astro</category>
      <category>seo</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
