Rafe Blandford

Optimising an old Codebase for Modern Web Standards

Rafe Blandford
6 min read
Lighthouse scores: Performance 99, Accessibility 100, Best Practices 100, SEO 100

After migrating three legacy mobile tech sites to static archives, I had a working but underperforming set of pages. The HTML was from roughly 2020, built on a codebase that had its roots in the mid-2000s. It worked, but a Lighthouse audit told the real story: Performance 71, Accessibility 66, Best Practices 73, SEO 83.

Not terrible for a decade-old codebase. But not good enough to leave alone, especially when the pages would now be frozen forever. Whatever state they were in was the state they'd remain in. Worth spending an afternoon to get them right.

How

This was a great test case for agentic engineering. Well defined problems, lots of drudge work that could be easily automated, and a clear set of outcomes to aime for.

All of the work described below was done through Claude Code, with a series of prompts and getting Claude to write the automation scripts and directly interact with the Google Lighthouse/PageSpeed and related services programmatically and through APIs.

The systematic approach

Rather than chasing individual issues, we ran Lighthouse against a representative page and worked through the categories methodically. The principle was straightforward: fix what's safe, leave what's inherent to the design. These are archived pages -- we're not redesigning them, just removing unnecessary penalties.

Quick wins: lazy loading and script deferral

The single biggest performance improvement came from adding loading="lazy" to images. With 387,339 image tags across the three sites (skipping the first two per page to avoid penalising the Largest Contentful Paint), this alone roughly halved the LCP time. Combined with deferring non-critical scripts and adding preconnect hints for cross-domain resources, the First Contentful Paint dropped from 2.4 seconds to 0.9 seconds.

Removing dead weight

The post-processing stage had already stripped the obviously defunct services -- Google Analytics, Chartbeat, Skimlinks, AddThis. But Lighthouse flagged more. A Google Custom Search Engine brand script was still loading despite the search having been removed years ago. jQuery was loading from Google's CDN over HTTP (not HTTPS), adding a redirect penalty on every page load.

Then there was Google Translate. The widget had been embedded across all three sites and was still injecting scripts into 30,764 pages. It served no purpose on an archive and was contributing to page weight.

The polyfill purge

This was the most satisfying discovery. The sites were loading two JavaScript libraries on every page: Modernizr and respond.js (also known as mediaqueries.js). Both are polyfills -- Modernizr detects HTML5/CSS3 feature support, and respond.js adds media query support to browsers that lack it. Their target audience: Internet Explorer 6 through 8.

In 2026, these are doing precisely nothing useful. But they weren't harmless either. Modernizr was actively causing a Cumulative Layout Shift problem. On page load, it swaps the class on the <html> element from no-js to js, which can trigger a layout recalculation. In our case, this was responsible for 2.0 of the total 2.131 CLS score. Removing it eliminated the issue at its source.

Respond.js was a smaller win -- 16KB and one network request -- but every request matters when you're optimising.

Patching jQuery 1.6.2

The sites shipped with jQuery 1.6.2 from 2011. Upgrading to a modern version would have been risky -- the flexslider carousel, Disqus integration, and various inline scripts all depended on jQuery's older API. Instead, we made a surgical patch: the only thing triggering Lighthouse's "deprecated APIs" flag was three vendor-prefixed requestAnimationFrame calls (webkitRequestAnimationFrame, mozRequestAnimationFrame, oRequestAnimationFrame). These were replaced with the standard requestAnimationFrame. One find-and-replace, flag cleared, no behavioural change.

We also switched from Google's CDN to a locally-hosted copy, which meant the patched version was what got served, and we eliminated one cross-domain DNS lookup.

Security headers

For a static archive that will never accept user input, the security header story is straightforward. We added HSTS (one-year max-age with subdomains), a Content Security Policy locking script sources to specific known domains, X-Content-Type-Options, X-Frame-Options, a strict referrer policy, and a permissions policy disabling camera, microphone, geolocation, and payment APIs. All configured in Caddy, all appropriate for a read-only archive.

Accessibility fixes

The accessibility score had the most room for improvement but also the most constraints -- we weren't going to redesign the layout. The wins came from missing basics: alt text on logos and social icons, an aria-label on the search input, and a colour contrast fix on timestamp elements (bumping from #9c9c9c to #707070 to pass WCAG AA). Small changes, but they moved the score from 66 to 91.

The gotchas

It wasn't all smooth sailing. A post-processing regex intended to add alt text to images was too greedy and corrupted the logo markup across all three sites -- introducing byte sequences (\x01\x02) that broke rendering. We caught it, diagnosed it, and fixed it across 29,000 pages.

Deferring jQuery seemed like an obvious optimisation, but the flexslider carousel depended on jQuery being available synchronously at initialisation time. Deferring it broke the homepage slider on all three sites. The fix was simple -- remove defer from just the jQuery script tag -- but it was a reminder that "defer all the things" isn't always the right advice for legacy code.

The first result

Category Before After
Performance 71 93
Accessibility 66 91
Best Practices 73 96
SEO 83 100

First Contentful Paint went from 2.4s to 0.9s. Largest Contentful Paint dropped from 5.2s to 2.0s. Speed Index halved.

Not bad for an afternoon. But there was more to do.

The second pass: what Google actually complained about

The Lighthouse scores were solid, but then the emails started arriving from Google Search Console. Structured data issues. Deprecated breadcrumb markup. Review snippets with invalid fields. The kind of specific, actionable feedback that Lighthouse alone doesn't catch.

The review markup was the biggest issue. Over 2,200 pages across all three sites had schema.org/Review structured data that was never quite valid. The itemReviewed field was a bare text span instead of a typed object. The author field was missing a name property. The rating scale was ambiguous. Google had been tolerating these for years but was now flagging them as errors that prevented rich results.

The fix was straightforward: strip the review markup entirely and reclassify those pages as schema.org/Article. These are historical reviews of Symbian apps from 2011. There is no benefit to having review rich results in Google for them. The scores stay visible on the page, just not marked up as structured data.

Breadcrumbs were a similar story. Every page had breadcrumb markup using data-vocabulary.org, which Google deprecated in 2020. Rather than just stripping it, we converted all 23,000 pages to proper schema.org/BreadcrumbList using JSON-LD, with absolute URLs derived from each page's file path. A Python script parsed the old markup, extracted the breadcrumb chain, generated the JSON-LD, injected it into the <head>, and stripped the old attributes. One pass, all three sites.

The http problem

A sweep of the HTML source revealed that internal links and image references were still using http:// across nearly 29,000 pages. The og:image meta tags. Links between the sister sites. References to the mediafiles subdomains. Even a double-www bug (http://www.www.allaboutsymbian.com) that had been lurking in 1,400 pages since the original CMS generated it years ago.

Canonical URLs were another quiet problem. They were all relative (href="15510_Little_Big_City.php" instead of https://allaboutsymbian.com/reviews/item/15510_Little_Big_City.php). PageSpeed Insights flagged this as invalid. Another Python script, another 23,000 files fixed.

The final numbers

Category Day 1 Day 2 Day 3
Performance 71 93 96-98
Accessibility 66 91 97
Best Practices 73 96 100
SEO 83 100 100

The remaining performance points are inherent to the page design (large camera comparison images, render-blocking CSS that can't be deferred without breaking the layout). The accessibility gap is touch target spacing in the navigation bar, which would require a CSS redesign. Both are acceptable for an archive.

The lesson

The first pass was about subtraction: removing dead scripts, defunct polyfills, and unnecessary weight. The second pass was about correctness: fixing structured data, converting deprecated markup, ensuring URLs were absolute and used HTTPS. The third pass was about the edges: lazy-loading third-party embeds, adding cache headers to mediafiles subdomains, upgrading logos to Retina resolution.

Each pass had diminishing returns in terms of score improvement but increasing returns in terms of doing things properly. A Lighthouse score of 93 is good. But the difference between 93 and 100 is the difference between "works" and "done right." For pages that will sit unchanged for years, done right is worth the effort.

New writing by email

Occasional pieces on product, technology and AI — and how they actually play out in practice.

Thanks — check your inbox to confirm.