<?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: Amazong110</title>
    <description>The latest articles on DEV Community by Amazong110 (@amazong110).</description>
    <link>https://dev.to/amazong110</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%2F4139050%2F9d60ef5c-4ccd-4154-876e-9f056d978f9a.jpg</url>
      <title>DEV Community: Amazong110</title>
      <link>https://dev.to/amazong110</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/amazong110"/>
    <language>en</language>
    <item>
      <title>Six things that keep a site off a perfect Lighthouse score</title>
      <dc:creator>Amazong110</dc:creator>
      <pubDate>Wed, 23 Sep 2026 08:45:20 +0000</pubDate>
      <link>https://dev.to/amazong110/six-things-that-keep-a-site-off-a-perfect-lighthouse-score-3ecg</link>
      <guid>https://dev.to/amazong110/six-things-that-keep-a-site-off-a-perfect-lighthouse-score-3ecg</guid>
      <description>&lt;p&gt;I'm Cody Wang. I build and optimise Prismic, SvelteKit and Next.js sites, mostly for clients in New Zealand and for Chinese brands going international.&lt;/p&gt;

&lt;p&gt;These six things are ones I've fixed myself, more than once. No theory dump. Just what it looked like, what I assumed at first, what I actually changed, and where the official docs are.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Images: alt text, dimensions, file size
&lt;/h2&gt;

&lt;p&gt;The most common one is a missing &lt;code&gt;alt&lt;/code&gt;. People either write &lt;code&gt;alt="image"&lt;/code&gt; or stuff keywords in there. Both read badly through a screen reader. Alt text describes the picture to someone who can't see it: &lt;code&gt;alt="Dark grey steel garage door after installation, seen from the street"&lt;/code&gt;. Purely decorative images get &lt;code&gt;alt=""&lt;/code&gt; — an empty string, not an omitted attribute.&lt;/p&gt;

&lt;p&gt;If editors upload content through a CMS, the template has to cover for them: &lt;code&gt;alt={image.alt || contextFallback}&lt;/code&gt;. Don't assume everyone remembers. On one client site I counted 32 images without alt; after the fix, zero.&lt;/p&gt;

&lt;p&gt;Docs: &lt;a href="https://web.dev/learn/accessibility/images" rel="noopener noreferrer"&gt;https://web.dev/learn/accessibility/images&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Second issue is dimensions. Without &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt;, the browser doesn't know how much room an image needs. It lays out everything else first, then reflows once the image arrives, and the user watches the content jump. That's where CLS comes from.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/hero.webp"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"1200"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"630"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"…"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docs: &lt;a href="https://web.dev/articles/optimize-cls" rel="noopener noreferrer"&gt;https://web.dev/articles/optimize-cls&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Third is file size, and it's the most visible one. A hero exported straight from the design file can land at 527 KB. I once found a star rating icon, an SVG, weighing 123 KB when it should have been about 2 KB.&lt;/p&gt;

&lt;p&gt;I do three things together: convert to WebP or AVIF, serve the size the layout actually uses (a 400px card doesn't need a 2000px image), and let the CDN handle it with something like &lt;code&gt;?auto=format,compress&amp;amp;q=75&amp;amp;w=800&lt;/code&gt;. Also don't lazy-load the hero — give it &lt;code&gt;fetchpriority="high"&lt;/code&gt; and lazy-load the rest.&lt;/p&gt;

&lt;p&gt;Static assets on one site went from 996 KB to 79 KB.&lt;/p&gt;

&lt;p&gt;Docs: &lt;a href="https://developer.chrome.com/docs/lighthouse/performance/uses-webp-images" rel="noopener noreferrer"&gt;https://developer.chrome.com/docs/lighthouse/performance/uses-webp-images&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Don't load the whole font family
&lt;/h2&gt;

&lt;p&gt;Text goes blank for a second on mobile, and LCP suffers for it.&lt;/p&gt;

&lt;p&gt;Usually the entire family is being pulled down at once: four to six weights, plus full character sets. While it downloads, the browser may show nothing at all. Chinese sites hit this hard, since a single CJK font can run into megabytes.&lt;/p&gt;

&lt;p&gt;What I do: keep only the weights in use, say 400 and 600; ship woff2 split by &lt;code&gt;unicode-range&lt;/code&gt;, so an English page never fetches the CJK file; and set &lt;code&gt;font-display: swap&lt;/code&gt; so text appears in the system font first.&lt;/p&gt;

&lt;p&gt;On one site that was Roboto at 468 KB plus Raleway at 309 KB, 777 KB total, and after the split 57 KB plus 43.7 KB — 100.7 KB.&lt;/p&gt;

&lt;p&gt;Docs: &lt;a href="https://developer.chrome.com/docs/lighthouse/performance/font-display" rel="noopener noreferrer"&gt;https://developer.chrome.com/docs/lighthouse/performance/font-display&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Third-party scripts: not whether, but when
&lt;/h2&gt;

&lt;p&gt;Scrolling feels sticky, clicking a button does nothing for a moment. That's usually total blocking time.&lt;/p&gt;

&lt;p&gt;GTM, the Facebook pixel, chat widgets — they all load as synchronous JavaScript and compete with your page for the main thread. While it's busy, taps do nothing.&lt;/p&gt;

&lt;p&gt;Deleting them is cleanest, but on most client work the tracking scripts are a business requirement and I can't touch them. So I change when they load instead:&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="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scroll&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keydown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mousemove&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;touchstart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loadTags&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="na"&gt;passive&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="nx"&gt;requestIdleCallback&lt;/span&gt;&lt;span class="p"&gt;?.(&lt;/span&gt;&lt;span class="nx"&gt;loadTags&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loadTags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inject them once the page is idle, or after the first sign of interaction. Nothing is removed and nothing stops reporting. With that change on one site, TBT went from 284 ms to 179 ms.&lt;/p&gt;

&lt;p&gt;Worth knowing before you promise a number: changing when a script loads is something you can usually get approved. Removing it is not. Where the tracking scripts stay, mobile performance has a ceiling, and it's not a ceiling you can code your way past.&lt;/p&gt;

&lt;p&gt;Docs: &lt;a href="https://developer.chrome.com/docs/lighthouse/performance/third-party-summary" rel="noopener noreferrer"&gt;https://developer.chrome.com/docs/lighthouse/performance/third-party-summary&lt;/a&gt; and &lt;a href="https://web.dev/articles/tbt" rel="noopener noreferrer"&gt;https://web.dev/articles/tbt&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Navigation built from buttons is invisible to crawlers
&lt;/h2&gt;

&lt;p&gt;The audit says &lt;code&gt;Links are not crawlable&lt;/code&gt;, which means search engines can't follow your links.&lt;/p&gt;

&lt;p&gt;Crawlers read &lt;code&gt;&amp;lt;a href="..."&amp;gt;&lt;/code&gt;. If your nav is a &lt;code&gt;button&lt;/code&gt; with an &lt;code&gt;on:click&lt;/code&gt; handler that scrolls the page, it isn't a link as far as a crawler is concerned. Your most important entry points simply don't exist in search.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/#whatwedo"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;What we do&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You keep the styling and the smooth scroll. Let CSS handle it with &lt;code&gt;html { scroll-behavior: smooth }&lt;/code&gt; rather than intercepting clicks in JavaScript.&lt;/p&gt;

&lt;p&gt;On one site I counted four real &lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt; elements on the whole page; the rest of the navigation was JavaScript. Real links took SEO from 83 to 100.&lt;/p&gt;

&lt;p&gt;Docs: &lt;a href="https://developers.google.com/search/docs/crawling-indexing/links-crawlable" rel="noopener noreferrer"&gt;https://developers.google.com/search/docs/crawling-indexing/links-crawlable&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. canonical tags and structured data
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;canonical&lt;/code&gt; tells search engines which URL is the source of truth for a piece of content. Parameterised URLs, language variants and leftover staging environments all make it easy for them to treat the same page as duplicates. Structured data (JSON-LD) is what lets results show details like ratings or service areas.&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;"canonical"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://example.com/page"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:url"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"https://example.com/page"&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 trap here is worth its own paragraph. In Svelte:&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;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/ld+json"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&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;data&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That renders the literal string on the page. Users can see &lt;code&gt;{JSON.stringify(data)}&lt;/code&gt; sitting in the markup, because Svelte doesn't evaluate what's inside a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; element. You need &lt;code&gt;{@html ...}&lt;/code&gt;. I found it only because the Facebook pixel logged "Malformed JSON".&lt;/p&gt;

&lt;p&gt;So after adding structured data, open the page in a browser and look at what actually renders. Don't trust the source file.&lt;/p&gt;

&lt;p&gt;Docs: &lt;a href="https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls" rel="noopener noreferrer"&gt;https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls&lt;/a&gt; and &lt;a href="https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data" rel="noopener noreferrer"&gt;https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Contrast and touch targets
&lt;/h2&gt;

&lt;p&gt;When accessibility stalls at 97 or 98, the culprits are almost always &lt;code&gt;color-contrast&lt;/code&gt; and &lt;code&gt;target-size&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Light grey text on white looks refined, but WCAG AA wants a contrast ratio of 4.5:1, or 3:1 for large text. Tappable things on mobile need to be at least 24×24 px, and putting them too close together also fails.&lt;/p&gt;

&lt;p&gt;Don't judge this by eye. Run Lighthouse once and it tells you the exact values you're missing. Readability beats the elegant-grey look.&lt;/p&gt;

&lt;p&gt;On my own site: &lt;code&gt;#a4a4a4&lt;/code&gt; on &lt;code&gt;#f7f7f7&lt;/code&gt; measured 2.85:1, so it became &lt;code&gt;#767676&lt;/code&gt; at 4.54:1. Footer links were 85×20 px and got padding to clear 24 px.&lt;/p&gt;

&lt;p&gt;Docs: &lt;a href="https://web.dev/learn/accessibility/color-contrast" rel="noopener noreferrer"&gt;https://web.dev/learn/accessibility/color-contrast&lt;/a&gt; and &lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html" rel="noopener noreferrer"&gt;https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Two honest notes
&lt;/h2&gt;

&lt;p&gt;A perfect Lighthouse score isn't the point. The point is that people see your content within three seconds, can tap what they need, and can find you in search. The score is useful only because it turns those feelings into numbers you can work through and verify one at a time.&lt;/p&gt;

&lt;p&gt;The longest run I've done was eight rounds to take a mobile score from 61 to 86, and that was with every third-party tracking script left in place. Most sites don't need eight rounds. The first three items above will usually cut the page weight in half.&lt;/p&gt;

&lt;p&gt;If you're not sure where your site stands, send me the URL. I'll run it through a tool I built and give you a free diagnosis: why the score is stuck, what's fixable, and which parts are a ceiling set by third-party scripts that no amount of refactoring will move.&lt;/p&gt;

&lt;p&gt;Tool: &lt;a href="https://prismicaudit.com" rel="noopener noreferrer"&gt;https://prismicaudit.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>performance</category>
      <category>seo</category>
      <category>a11y</category>
    </item>
  </channel>
</rss>
