Performance in the Age of AI, Part 2

DiveJourney was getting more traffic from Google, and I was starting to notice how slow it felt on my phone. Great combination.

I'd spent months using coding agents to build out the site: destination guides, more languages, maps, nearby spots, shop information, analytics, and all the little interactions that make those features useful. I was happy with how much I could ship. But when I opened the product as a user, the accumulated weight was becoming pretty obvious.

Search Console agreed. Both mobile and desktop Core Web Vitals had gone red.

On July 13, I measured seven representative production pages. Their mobile PageSpeed scores ranged from 38 to 59. Lighthouse’s average Largest Contentful Paint, or LCP, was 8.86 seconds, with the worst page at 14.2 seconds. Those were synthetic test results, not a stopwatch on every visitor, but they gave me plenty to investigate.

In Part 1, I wrote about making DiveJourney’s tests and development tools fast enough to keep up with the agents. Meanwhile, I’d let the actual website get heavy. This was the other half of that work.

I used to live in Chrome DevTools

Performance optimization has always been one of my favorite parts of frontend engineering. I like having something concrete to investigate: a late image request, too much JavaScript running on load, a page that jumps around while you’re trying to use it. Change something, reload, and find out whether it helped.

So discovering Chrome DevTools MCP was particularly exciting for me. It lets a coding agent control and inspect a Chrome browser, including performance traces, network requests, and console output.

I used to live in Chrome DevTools for this stuff. Now I could hand Chrome DevTools to an agent.

That meant I didn't have to inspect every waterfall myself and translate the findings into instructions. The agent could open a slow page, investigate what was happening, connect it to the implementation, make a change, and run the page again. I still directed the work and checked the product, but much more of the investigation could happen without me sitting between the browser and the code.

It brought back a part of engineering I really enjoy. I was still working through the performance problem, just with agents handling the implementation and much of the repetitive checking.

And this was a particularly good task for them. There was a specific page to inspect and a measurement to compare after each change. An answer like “this should reduce the bundle size” wasn’t the end of the task. The agent could go find out whether it actually did.

Giving it something useful to measure

I didn't want a fast homepage and a slow rest of the site. A country guide, a destination planning page, and a dive-spot detail page load different data and have different things competing to appear first.

The original seven-page test set included the homepage, a Spanish destination guide, country and destination-section pages, and dive spots. Later comparisons also covered wildlife pages. I added extra pages from the same categories to check beyond the URLs being used for day-to-day tuning.

Google PageSpeed Insights supplied the external baseline. When the public API hit its quota, I had the agents build an importer for the raw Lighthouse results embedded in the generated report pages. That preserved the actual measurements rather than leaving me with screenshots and numbers copied into a chat.

For the repeated runs, I used Lighthouse CI against production builds running locally, preview deployments, and the deployed site. The main test runs used three cold samples per page, with the browser, device profile, and test settings recorded. Chrome DevTools MCP supplied the traces for diagnosis; Supabase evidence helped explain the data work; Vercel identified the deployed build and exposed runtime errors.

There was some very unglamorous checking involved. One set of valid results came from an older production deployment than the release I thought I was testing. Another run measured a Vercel protection page instead of DiveJourney.

Neither mistake would necessarily produce an obviously broken report. The numbers can look perfectly reasonable while describing the wrong thing. So checking the final URL, real page content, and deployment identity became part of the process, not something to remember afterward.

A local production build let the agents iterate without needing a new deployment for every small change. Deployed checks were a separate step: confirm what had actually reached production.

Two rounds of getting less in the browser's way

There were some obvious fixes. Innerkip Quarry had an approximately 1.1 MiB Mapbox image in its initial loading path. Devana Kandu was loading charts and other interaction before anyone needed them.

But similar problems showed up across unrelated pages. Public visitors were downloading shared client code for sessions, analytics, consent, and features they hadn't touched. Some pages carried more translation data than they needed. Announcements, headers, footers, and content arriving from the server could shift the layout as the page loaded.

The first round was mostly about sorting out what really needed to happen before the main content appeared.

The agents reduced shared client code, kept nonessential analytics and interaction out of the initial load, fixed image sizing and priority, and made the layout more stable. Independent server reads could start together rather than waiting on each other. Content farther down the page could arrive without holding up the heading or hero.

These were familiar frontend optimizations. What changed was how much of the investigation, implementation, and retesting I could hand over. I could ask the agent to investigate a shared cause, have it make the change, and then look at what happened across several different page types.

I came back for a second round in late August. The product had continued growing, and there was more to remove from the initial load.

One useful change was keeping a lightweight server-rendered header visible while delaying the heavier interactive version. The footer, notifications, feedback controls, and other secondary UI could wait until the page settled or someone needed them. The agents also removed an extra display-font request and moved more optional destination, dive-spot, and wildlife work away from the main content.

Some experiments went backward. One made the homepage hero image get discovered later; that was corrected. An inline-CSS experiment didn’t justify its tradeoffs and came back out.

I wasn't trying to remove functionality. Login still had to work. Privacy choices still had to work. A translated page still needed the right copy. Getting the main content onto the screen sooner was useful; quietly turning the rest of the product off wasn't.

What improved

By the end of this work, the recorded gains were substantial. These are the original baseline and the latest captured deployed-site results for JavaScript, blocking, and layout shift, followed by the later field outcome I saw in Search Console.

Table
MeasurementStarting pointRecorded result
JavaScript transferred 1,190 KiB 493 KiB, roughly 59% less
Total Blocking Time 969ms average 0ms median
Cumulative Layout Shift 0.114 average 0.0058 median
Search Console Core Web Vitals Mobile and desktop red Mobile and desktop green

The payload, blocking, and layout figures come from tests against the deployed site. The baseline and later tests used different Lighthouse versions, and the table labels averages and medians separately. This is the recorded recovery, not a controlled benchmark of one isolated change.

The last controlled local comparison during the second round was also encouraging. Across the homepage, a destination, a dive spot, and a wildlife page, directly throttled Chrome produced LCP medians of 1.72 to 1.96 seconds. All 12 runs were below 2.5 seconds and met the performance-score, blocking, and layout-shift targets.

Those sub-two-second numbers came from a production build running locally with mobile viewport, network, and CPU constraints. They aren't timings from real visitors, and I wouldn't put them opposite the original hosted PageSpeed LCP and call that a percentage improvement. The controlled comparison is below.

Search Console was the separate real-user check. Its Core Web Vitals report uses rolling CrUX data, so it doesn’t instantly reflect a release. Both mobile and desktop later turned green. That’s the field result behind the headline, not a claim that every Lighthouse score became perfect or that the performance work caused a ranking increase.

Why I stopped chasing the Lighthouse score

There was one part of the second round that took more investigation. The browser could paint the main content quickly while Lighthouse’s normal simulation still predicted a much slower LCP.

I had the agent compare two throttling methods on the same local production build. Each method ran three times on each of four pages, using the same configured mobile viewport, network, and CPU settings.

Table
PageLighthouse simulated LCPDirect DevTools-throttled LCP
Homepage 3.53s 1.72s
Aruba destination 4.74s 1.91s
Devana Kandu dive spot 3.95s 1.88s
Tiger shark page 3.75s 1.96s

Lighthouse's default Lantern mode records an initially unthrottled load, then models how it would behave under slower conditions. DevTools throttling applies constraints while the browser loads the page. Both are approximations; the directly throttled result isn't a measurement from an actual phone either. Google's own throttling documentation discusses limitations in both methods.

In this investigation, the traces pointed to how the simulation handled the streamed content and image-loading dependencies. It was attributing seconds of additional delay that didn’t show up in the directly throttled run. The agent could inspect that disagreement instead of treating the score as a complete explanation of the problem.

I didn't take the smaller number and declare everything fixed. Some First Contentful Paint and Speed Index checks still missed the stricter targets. The local fixtures also limited parts of the visual-completeness testing. There was remaining work; there just wasn't evidence that another risky change to consent or session loading would solve the modeled LCP gap.

So I stopped that round there. I wanted the site to be faster, not to spend indefinitely changing working behavior because one test still wanted another number.

I still had to use the site

Moving things out of the initial load creates its own bugs. During this work, review caught missing safety callouts and public dive-log images. The later round caught a deferred header that could replace an open menu, incorrect German fallback copy in delayed controls, and a gallery that could restart a request already in progress.

Those are exactly the sorts of things that can disappear behind a nice performance result. A page with less content can load faster. That doesn't make it the page I meant to ship.

The numbers can say one thing, but the page could just be empty.

I still opened the pages, checked the content, tried the interactions, and used the site on my phone. The automated checks helped with requests, console errors, and regressions, but I was still responsible for deciding whether the change made sense for the product.

What I’d repeat is giving the agent access to the browser early, rather than waiting until it had finished coding to find out what the code did. That was the enjoyable part of this project: a familiar performance problem, the tools I already liked using, and much less manual work between finding something worth changing and seeing the result.

Try it on your own site

Here's a starting prompt for a coding agent in your own repository. It asks the agent to check its access, set up the measurements, and work through a bounded first pass. Replace the bracketed values. It may still need you to approve an MCP configuration change, restart the client, or supply API access.

PortableText [components.type] is missing "codeBlock"

Setup references: Chrome DevTools MCP, PageSpeed Insights API, and Lighthouse CI.