Prismic Page Audit Free online audit tool
HomeNotes › Why your site never hits a perfect Lighthouse score: six issues I keep fixing

Why your site never hits a perfect Lighthouse score: six issues I keep fixing

By Cody Wang — front-end engineer building and optimising Prismic / SvelteKit / Next.js sites

No magic here. These are the six issues I actually fix, over and over. For each one: why it happenswhat Google says (links) → the real numbers from my own projectshow to fix it.


#1. Images: the three things everyone misses (alt / dimensions / weight)

alt is for humans — and machines

When Lighthouse says *"Image elements do not have [alt] attributes"*, the tempting fix is alt="image". That is the same as leaving it empty. Screen readers use it; when an image fails to load, users read it.

  • alt="garage door garage doors auckland garage door install" (keyword soup — it reads terribly)
  • alt="Dark grey steel garage door installed, looking down the driveway from the street"
  • Decorative images: alt="" (empty string, not omitted)
  • CMS sites: content editors upload the images, so the template must fall back: alt={image.alt || contextFallback}

📎 web.dev · Accessible images 📊 My fix: a site with 32 images missing alt → 0.

② No width / height means the page "jumps"

The browser doesn't know how much space an image needs, lays everything else out, then reflows when the image arrives. That's CLS — and users click on the wrong thing.

<img src="/hero.webp" width="1200" height="630" alt="…" />
<!-- or in CSS: aspect-ratio: 16 / 10; -->

📎 web.dev · Optimize CLS

③ Format, dimensions and weight are three separate problems

A PNG straight out of the design tool can be a 527 KB hero; an icon SVG can weigh 123 KB (it should be ~2 KB).

  • Convert to WebP / AVIF;
  • Serve at display size (never a 2000px image in a 400px card);
  • Let the CDN do it: ?auto=format,compress&q=75&w=800auto=format serves WebP to browsers that support it;
  • The hero image should not be loading="lazy"; give it fetchpriority="high" instead. Everything below the fold gets loading="lazy".

📎 Lighthouse · Serve images in modern formats 📊 My fix: static assets 996 KB → 79 KB.


#2. Fonts: don't load the whole family

Symptom: on mobile, text stays blank for a second, dragging LCP down.

Why: many sites ship the entire family at once — four to six weights, full character sets included. Until the font arrives the browser may render nothing (FOIT).

Fix:

  • Load only the weights you actually use (say 400 and 600);
  • Ship only the character sets you need (woff2 + unicode-range — this matters enormously for CJK: a Latin-only page should never download a Chinese font);
  • Add font-display: swap.

📎 Lighthouse · Ensure text remains visible during webfont load 📊 My fix: Roboto 468 KB + Raleway 309 KB57 KB + 43.7 KB (419 KB → 100.7 KB).


#3. Third-party scripts: it's not *whether*, it's *when*

Symptom: Total Blocking Time of several hundred milliseconds, janky scrolling, taps that don't register.

Why: GTM, Facebook Pixel, chat widgets are usually synchronous JS competing with your page for the main thread. While it's busy, the page doesn't respond to the user.

Fix — push loading to idle or first interaction:

['scroll','click','keydown','mousemove','touchstart']
  .forEach(e => addEventListener(e, loadTags, { once: true, passive: true }));
requestIdleCallback?.(loadTags);          // or
setTimeout(loadTags, 3500);               // a safety net

⚠️ A real constraint: on most client sites the tracking scripts are a business requirement and cannot be removed. So change the timing, not the existence — that's negotiable, and it works.

📎 Lighthouse · Third-party summary | web.dev · TBT 📊 My fix: TBT 284 ms → 179 ms by changing only when the tags load.


Symptom: Lighthouse flags Links are not crawlable; new pages take forever to be indexed.

Why: crawlers only follow <a href="...">. A <button on:click={() => scrollTo('#x')}> is not a link to them — your most important entry points are invisible to search engines.

Fix:

<a href="/#whatwedo">What we do</a>
<!-- Want smooth scrolling? Use CSS: html { scroll-behavior: smooth } — don't intercept clicks in JS -->

📎 Google Search Central · Links are crawlable 📊 My fix: a site where only 4 real <a href> existed in the whole page (everything else was JS scrolling) → real links, same look → SEO 83 → 100.


#5. canonical and structured data

Why:

  • canonical tells search engines which URL is the authoritative one — sites with query parameters, multiple languages or a staging copy run into duplicates constantly;
  • Structured data (JSON-LD) is what enables rich results: ratings, service area, organisation details.
<link rel="canonical" href="https://example.com/page" />
<meta property="og:url" content="https://example.com/page" />
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"ProfessionalService","name":"…"}
</script>

⚠️ A mistake I made: in Svelte, <script type="application/ld+json">{JSON.stringify(data)}</script> outputs that expression as literal text — users could literally read {JSON.stringify(data)} on the page. You need {@html ...}. I only noticed because Facebook Pixel reported "Malformed JSON" — so always look at the rendered page after shipping.

📎 Canonical | Structured data


#6. Contrast and touch targets: the last points of accessibility

Symptom: accessibility sits at 97/98 and won't move; color-contrast and target-size are flagged.

Why:

  • Pale grey text on white "looks premium" but fails WCAG AA, which requires 4.5:1 (3:1 for large text);
  • Tappable targets need to be at least 24×24 px on mobile, and links packed too closely fail too.

Fix: stop eyeballing it — run Lighthouse and let it tell you the exact colour values. Readability beats "designer grey".

📎 web.dev · Color contrast | W3C · Target Size 📊 My fix: #a4a4a4 on #f7f7f7 measured 2.85:1 (fail) → #767676 (4.54:1, pass); footer links at 85×20 px → padded to ≥24 px tall.


#Closing

A perfect score isn't the goal — a user seeing content within three seconds, being able to tap things, and being able to find you, is the goal. Lighthouse is useful because it translates "how it feels" into verifiable numbers you can fix and re-verify one by one.

On my hardest project I needed eight rounds to take mobile from 61 to 86 — with third-party tracking scripts that had to stay. Most sites don't need eight rounds: the first three items alone usually halve the page weight.

What's your site stuck on? Send me the URL and I'll run a free audit with the tool I built: why the score is capped, which items you can fix, and which ones are the mathematical ceiling of your third-party scripts.

(The tool: prismicaudit.com — 59 tech-stack detectors plus performance / image / SEO / accessibility checks. The desktop Lighthouse scores are 100/100/100/100.)