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.
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.
1. Images: alt text, dimensions, file size
The most common one is a missing alt. People either write alt="image" or stuff keywords in there. Both read badly through a screen reader. Alt text describes the picture to someone who can't see it: alt="Dark grey steel garage door after installation, seen from the street". Purely decorative images get alt="" — an empty string, not an omitted attribute.
If editors upload content through a CMS, the template has to cover for them: alt={image.alt || contextFallback}. Don't assume everyone remembers. On one client site I counted 32 images without alt; after the fix, zero.
Docs: https://web.dev/learn/accessibility/images
Second issue is dimensions. Without width and height, 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.
<img src="/hero.webp" width="1200" height="630" alt="…" />
Docs: https://web.dev/articles/optimize-cls
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.
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 ?auto=format,compress&q=75&w=800. Also don't lazy-load the hero — give it fetchpriority="high" and lazy-load the rest.
Static assets on one site went from 996 KB to 79 KB.
Docs: https://developer.chrome.com/docs/lighthouse/performance/uses-webp-images
2. Don't load the whole font family
Text goes blank for a second on mobile, and LCP suffers for it.
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.
What I do: keep only the weights in use, say 400 and 600; ship woff2 split by unicode-range, so an English page never fetches the CJK file; and set font-display: swap so text appears in the system font first.
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.
Docs: https://developer.chrome.com/docs/lighthouse/performance/font-display
3. Third-party scripts: not whether, but when
Scrolling feels sticky, clicking a button does nothing for a moment. That's usually total blocking time.
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.
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:
['scroll','click','keydown','mousemove','touchstart']
.forEach(e => addEventListener(e, loadTags, { once: true, passive: true }));
requestIdleCallback?.(loadTags);
setTimeout(loadTags, 3500);
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.
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.
Docs: https://developer.chrome.com/docs/lighthouse/performance/third-party-summary and https://web.dev/articles/tbt
4. Navigation built from buttons is invisible to crawlers
The audit says Links are not crawlable, which means search engines can't follow your links.
Crawlers read <a href="...">. If your nav is a button with an on:click 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.
<a href="/#whatwedo">What we do</a>
You keep the styling and the smooth scroll. Let CSS handle it with html { scroll-behavior: smooth } rather than intercepting clicks in JavaScript.
On one site I counted four real <a href> elements on the whole page; the rest of the navigation was JavaScript. Real links took SEO from 83 to 100.
Docs: https://developers.google.com/search/docs/crawling-indexing/links-crawlable
5. canonical tags and structured data
canonical 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.
<link rel="canonical" href="https://example.com/page" />
<meta property="og:url" content="https://example.com/page" />
One trap here is worth its own paragraph. In Svelte:
<script type="application/ld+json">{JSON.stringify(data)}</script>
That renders the literal string on the page. Users can see {JSON.stringify(data)} sitting in the markup, because Svelte doesn't evaluate what's inside a <script> element. You need {@html ...}. I found it only because the Facebook pixel logged "Malformed JSON".
So after adding structured data, open the page in a browser and look at what actually renders. Don't trust the source file.
Docs: https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls and https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data
6. Contrast and touch targets
When accessibility stalls at 97 or 98, the culprits are almost always color-contrast and target-size.
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.
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.
On my own site: #a4a4a4 on #f7f7f7 measured 2.85:1, so it became #767676 at 4.54:1. Footer links were 85×20 px and got padding to clear 24 px.
Docs: https://web.dev/learn/accessibility/color-contrast and https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html
Two honest notes
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.
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.
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.
Tool: https://prismicaudit.com
Top comments (0)