
Performance case study
In one afternoon, seven changes took our homepage's mobile PageSpeed score from 60 to 100 and its accessibility score from 85 to 100. Here is exactly what we changed, what we left alone, and what it means for your own site.
We tell clients that speed affects rankings, sales and, more and more, whether AI assistants can read a site at all. So when Google PageSpeed Insights gave our own homepage a mobile performance score of 60, we treated it like any client audit.
Four test runs later, on the same afternoon, the homepage scored 100 for performance, accessibility, best practices and SEO on both mobile and desktop. The last mobile run came in at 99, which is within the normal variation between runs.
Where we started
The first run used PageSpeed Insights' standard mobile test: a mid-range phone on a throttled "Slow 4G" connection.
| Measure | Mobile | Desktop |
|---|---|---|
| Performance score | 60 | 96 |
| Accessibility score | 85 | 91 |
| First Contentful Paint (seconds) | 4.4 | 0.9 |
| Largest Contentful Paint (seconds) | 8.6 | 1.0 |
| Total Blocking Time (ms) | 250 | 120 |
Desktop looked healthy. Mobile did not. A Largest Contentful Paint (LCP) of 8.6 seconds means a visitor on a typical phone connection waits more than eight seconds to see the main banner. Google's threshold for "good" is 2.5 seconds.
The server was fine. Our pages come from a Laravel CMS with Redis caching and are ready in around 50 milliseconds. Everything that went wrong happened in the browser, after the page had arrived.
Step 1: move the tracking scripts out of the way
The biggest single problem was Google's own tags. Google Analytics 4 and the Google Ads tag together downloaded about 350 KB of JavaScript and kept the phone's processor busy for around half a second while the page loaded.
We now load the tag library later:
- The tracking queue is still set up straight away, so page views, events and conversions are recorded from the first moment and sent once the library arrives.
- The library itself loads on the visitor's first tap, scroll, key press or mouse movement, or six seconds after the page loads for visitors who never touch it.
- A form submission loads it immediately, so no enquiry goes untracked.
Our first version used a 2.5-second timer. It worked on mobile but sometimes landed inside the desktop test, and desktop dropped to 82 in one run. Adding mouse movement as a trigger and lengthening the timer fixed it. Real desktop visitors move the mouse almost at once; the test never does.
The trade-off is that someone who leaves within a few seconds without touching the page may not show up in Analytics. For us that small gap in bounce data is worth the speed. Each business will weigh it differently.
Step 2: help the browser find the main image early
On mobile, the largest element on screen is the banner image. The browser only found it after downloading and reading our stylesheet, and it then competed for bandwidth with fonts, icons and scripts.
We made three changes:
- A preload hint for each screen size. Phones fetch the mobile crop and larger screens fetch the desktop image, and the request starts before the stylesheet is read.
- Images sized for the screen. Several images were up to twice the size they are displayed at. We made smaller WebP copies and let the browser choose the right one. The phone mock-up in our "Welcome" section dropped from 167 KB to 64 KB.
- A width and height on every image, so the browser reserves the space before the image arrives and the layout does not jump.
Our CMS now does this automatically. When an editor places an image, the template finds the resized copies and offers them to the browser.
Step 3: host the fonts ourselves, with fallbacks that match
Our fonts, DM Sans and Lato, came from Google Fonts. That meant two extra connections to two other servers before the font files could even start downloading, which is slow on a weak mobile signal.
We now serve the fonts from our own domain. While they load, text appears in Arial, and when the real font arrives the text can shift if the two fonts are different sizes. Google measures that movement as Cumulative Layout Shift (CLS).
The fix is a fallback font adjusted to match. We measured DM Sans and Lato and told the browser to scale and space Arial so it takes up almost exactly the same room, so the swap is barely visible.
One detail took some digging. DM Sans draws narrower letters at large sizes, so a fallback matched to body text was slightly too wide for our headings and the main headline still moved. Matching it again at heading sizes fixed mobile. On desktop, where bandwidth is not the constraint, we also preload the main font so headings never swap. Desktop CLS is now zero.
Step 4: cut the icon font from 314 KB to 28 KB
Our icons came from Font Awesome, loaded in full from a third-party server: a stylesheet and three font files totalling about 314 KB, for roughly a hundred icons the site actually uses.
We built a small tool that finds every icon used in our templates, scripts and CMS content, trims the font files down to just those icons, and writes a smaller stylesheet served from our own domain. The result is 28 KB of icon fonts and no third-party connection.
Editors can pick a new icon at any time, so there is a safety net. If a page uses an icon outside the trimmed set, the server adds the full Font Awesome stylesheet to that page only. Nothing breaks, and we rebuild the trimmed set when it suits us.
Step 5: stop making the browser redo the layout
PageSpeed also flagged "forced reflows". This happens when a script changes the page and then immediately measures it, which makes the browser recalculate the whole layout on the spot.
The main cause was our "Featured Projects" carousel. Its script measured the card width on load to centre the active card, but that width was already set in the stylesheet. We moved the positioning into CSS, and the script now only says which card is active. It never measures anything, and resizing the window needs no JavaScript at all. We did the same for the mobile article carousel.
Step 6: accessibility from 85 to 100
Speed gets the headlines, but these fixes probably matter more to real visitors.
- Colour contrast. White text on our brand blue measured 3.46:1, and on our Trustpilot green 2.62:1. The WCAG AA minimum for normal text is 4.5:1. We kept the brand colours and added slightly deeper shades used only where white text sits on them, and for small blue text on white.
- Colours chosen in the CMS. The template now checks the contrast of colours editors pick for review avatars and chooses white or navy text to suit.
- The contact panel. Our "Get in Touch" slide-out was hidden on screen, but keyboard users could still tab into its form fields. It is now fully switched off while closed.
- Tap targets. The review carousel dots were 9 × 9 pixels. They now have a 24 × 24 pixel tap area and look exactly the same.
- Heading order. A few sections skipped heading levels. We corrected the levels for screen readers without changing how the page looks.
Accessibility now scores 100 on mobile and desktop. Lighthouse's newer checks on how easily AI agents can use the page went from 1 of 2 to 3 of 3.
Step 7: security headers
These do not affect the score, but PageSpeed's trust and safety checks pointed out missing headers. We added:
- Strict-Transport-Security, which tells browsers to use HTTPS only. It starts with a short lifetime that we will lengthen once it has run cleanly.
- X-Frame-Options, so other sites cannot show our pages inside a frame.
- Cross-Origin-Opener-Policy, which keeps our pages separate from windows opened by other sites.
Making the site easy for AI assistants to read
While we were at it, we added two files that are becoming a common way to help AI tools understand a site:
- llms.txt, a plain guide to the site with a one-line description of each service, case study, free tool and recent article. It follows the llmstxt.org proposal.
- llms-full.txt, the full text of those pages in clean Markdown, so an assistant can read our content without menus, scripts and layout in the way.
Both are generated from the CMS like a sitemap, so they stay up to date as we publish.
The results
| Measure | First run | Final run |
|---|---|---|
| Mobile performance score | 60 | 99 to 100 |
| Desktop performance score | 96 | 100 |
| Mobile accessibility score | 85 | 100 |
| Desktop accessibility score | 91 | 100 |
| Mobile First Contentful Paint (seconds) | 4.4 | 1.4 to 1.7 |
| Mobile Largest Contentful Paint (seconds) | 8.6 | 1.7 to 2.0 |
| Mobile Total Blocking Time (ms) | 250 | 10 to 60 |
| Desktop Total Blocking Time (ms) | 120 | 40 |
In plain terms, the main content of our homepage now appears on a mid-range phone on a slow connection about four times faster than before.
What we chose not to do
Some of PageSpeed's remaining suggestions were not worth it for us.
- Inlining "critical CSS". PageSpeed still lists our main stylesheet as slowing the first paint. Splitting it into an inline part and a deferred part could save a little more time, but it risks unstyled flashes and goes stale whenever a page is edited. With mobile already at 99 to 100, the gain did not justify the risk.
- Merging the Google Ads script. Google's tag loads a second script for Ads by itself. Merging them means moving to Google Tag Manager, a bigger job than it is worth now that the tags no longer slow the page.
- A full Content Security Policy. It is worth doing, but it touches every inline script on the site, so it deserves its own carefully tested project.
What this means for your website
Every change above is something we do for clients, on our own Sayu platform, on Shopify or on a custom build. If your site scores in the red on mobile, get in touch, or try our free SEO tools to check your own pages first.
