All articles

A Practical Guide to Improving Your Website's Performance Score

Shivam Singh
Shivam Singh

AI Engineer Intern

August 5, 202613 min read
A Practical Guide to Improving Your Website's Performance Score

A low performance score isn’t just a number on a report. It changes how many people actually experience your site. Page speed factors into search rankings, so a slower site can lose ground to a faster competitor with otherwise similar content. Visitors feel it too: the longer a page takes to load, the more likely someone leaves before it finishes, particularly on a slow mobile connection or an older phone. That’s before you even get to the accessibility problem of shutting out anyone who isn’t on fast wifi with a new device.

In practice, it’s rarely one dramatic problem. It’s usually a handful of small, unglamorous issues piled on top of each other: a config flag left over from an old fix, an animation library doing more work than it needs to, a caching setting nobody’s looked at in a year. Individually, most of these shave off a few hundred milliseconds. Stacked together, they’re the difference between a page that feels instant and one that feels like it’s thinking about it.

None of the seven fixes below touch the design. They just change how fast the page becomes visible and usable, which is also why they’re easy to skip: nothing looks broken in a design review, so nobody notices until a Lighthouse report (or a drop in search ranking) forces the issue.

If any of this sounds familiar (a Lighthouse score you’ve been avoiding, a “temporary” config flag from a year ago that’s somehow still there), you’re not alone. These are the same seven things, roughly in this order, on site after site.

At a glance

01

Make sure image optimisation is actually switched on

Most modern frameworks ship a built-in image optimiser: resize, recompress, convert to a modern format, all on request. Teams disable it early on more often than you’d think, usually to unblock a broken build or a missing dependency, and then never come back to turn it on. Once that happens, every image on every page gets served at full resolution to every visitor, no matter how small it’s actually shown on screen.

Check your framework’s image config for a flag that turns optimisation off, and check whether the dependency it needs (usually an image-processing library) is actually installed. It’s normally a one-line fix, and it re-optimises every image already in the codebase without you touching a single page.

next.config.js

text
module.exports = {
  images: {
    // this one line turns off resizing, format conversion,
    // and responsive variants for every image on the site
    unoptimized: true,
  },
};

One disabled config flag can undo every other image optimisation effort on the site, including work you’ve already done.

02

Resize source images before they ever reach the optimiser

Even with optimisation switched on, the optimiser still needs a reasonable file to start from. Full-resolution design exports, sized for print rather than a phone screen, sometimes end up shipped straight into a project alongside already correctly-sized versions of the same image. A huge source file is slow to process on every cache miss, no matter how good the optimiser is.

Recompress and resize these (same photo, same crop, just re-encoded to a size the browser actually needs) and hero images routinely drop from tens of megabytes to well under one, with no visible difference at the size they’re actually shown.

Format matters here too, not just dimensions. A photo saved as PNG instead of a modern format like WebP or AVIF can be several times larger for identical visual quality, simply because PNG wasn’t designed for photographic detail. If your image optimiser is already switched on (fix one), it usually handles this conversion automatically. If it isn’t, the source format matters just as much as the source resolution.

This is the highest-leverage thing you can check on your own site. Open the network tab, sort by size, and compare what’s being downloaded against what’s actually on screen.

03

Cache pages instead of rendering them fresh on every request

Look for pages set to render fresh, from zero, on every single request. Refetching content and rebuilding the whole page for every visitor, even when two people load it a second apart. That setting has a real use case, a page showing a logged-in user their own data, for instance. But it’s often left on for pages that don’t need it at all: plain CMS content that’s identical for every visitor and only changes when someone edits it.

Swap that flag for a revalidation window instead. The page builds once, serves instantly from cache to every visitor, and rebuilds in the background on a timer, usually enough to keep the content feeling current. Everyone in between skips a server round-trip and a CMS fetch that would’ve returned the exact same page anyway.

page.tsx

text
// before: rebuilds the whole page from scratch, for every visitor
export const dynamic = "force-dynamic";

// after: builds once, refreshes in the background at most every 5 min
export const revalidate = 300;

How long that window should be depends on how often the content actually changes, not on habit. Five minutes is a reasonable default for most CMS-driven pages. A page that’s edited once a quarter could just as easily sit at an hour. If a specific edit needs to go live immediately, most frameworks also support triggering an on-demand revalidation from the CMS side, so you don’t have to choose between “instant” and “efficient” site-wide.

If a page looks the same to every visitor, it almost never needs to render from scratch on every request. A short revalidation window gets you the same freshness for a fraction of the work.

04

Defer third-party scripts until the browser is idle

Analytics, ad-tracking pixels, and tag managers are often set to load “as soon as the page is interactive.” That sounds fine until you realise “interactive” is also exactly when the page’s own JavaScript is trying to hydrate and take over from the server-rendered HTML. Third-party scripts loading at the same moment are fighting your own code for the same sliver of main-thread time, and that fight is what makes a page stay unresponsive to a tap or click for longer than it should.

Push scripts that don’t affect what a visitor sees (most analytics and tracking tags fall into this bucket) to load only once the browser goes idle after the page has fully loaded. Not every third-party script belongs in this bucket, though: a consent banner or a chat widget a visitor might actually click on early deserves to load sooner. The distinction is whether a visitor could plausibly need it before the page has settled, not whether it’s “third-party” as a category.

Add connection warm-up hints (a preconnect link tag) for domains you’re deferring to, so they’re not paying for a cold DNS and TLS handshake on top of everything else once they do run. Worth checking which hints are actually earning their keep, too: a warm-up hint for a domain that goes idle before the script ever fires just wastes one of the browser’s limited early connection slots. Tracking still fires, just a beat later, and nobody notices because none of it was ever visible to begin with.

layout.tsx

text
// before: races the page's own JS for the main thread
<Script src="/tag-manager.js" strategy="afterInteractive" />

// after: waits until the browser is idle, after the page has loaded
<Script src="/tag-manager.js" strategy="lazyOnload" />

05

Never gate your biggest visible element behind JavaScript

This is one of the most common mistakes on sites built with JavaScript animation libraries. A hero image fades in on page load, a nice enough touch, but the fade-in is written so the image sits at zero opacity until JavaScript has fully loaded, parsed, and run. On a fast laptop you’d never notice. On a throttled connection or a busy phone, that gap can stretch to several seconds. The image has already finished downloading. It’s sitting there fully loaded, just invisible, waiting for JavaScript to give it permission to show up.

Downloaded

sooner

Visible

later

This matters a lot for Largest Contentful Paint, one of the core scores search engines and performance tools use to judge a page, because on almost every page the hero image or headline is the largest thing on screen. Every millisecond it stays invisible counts against the score.

The fix is simple: stop making the element’s visibility depend on JavaScript, and animate it with plain CSS instead.

Before: gated on JavaScript

text
<motion.div
  initial={{ opacity: 0, y: 150 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 1.2, delay: 0.1 }}
>
  <img src={hero} />
</motion.div>

After: runs at stylesheet parse

text
<div class="hero-fade-in">
  <img src={hero} />
</div>

/* stylesheet */
@keyframes hero-fade-in {
  from { opacity: 0; transform: translateY(150px); }
  to   { opacity: 1; transform: translateY(0); }
}
.hero-fade-in {
  animation: hero-fade-in 1.2s
    cubic-bezier(.22,1,.36,1) both;
}

A CSS animation like this starts the instant the browser has parsed the stylesheet. It doesn’t wait for JavaScript to load, run, and hydrate the page first. Same fade, same timing, same feel, nothing about it looks different to a visitor. The only thing that changes is which engine gets to gate it, and that alone routinely takes worst-case entrance delays from several seconds down to well under half a second.

06

Keep animations on one consistent clock

Fixing the previous issue can introduce a subtler one. If a hero photo animates on the browser’s CSS clock (starts the instant styles are parsed) while the headline text next to it still animates on a JavaScript clock (starts only once the page is interactive), the two can visibly fall out of sync. This shows up worst on slower phones, where the gap between “styles parsed” and “JavaScript interactive” can stretch past a second. A photo can arrive before its own headline is even ready, or a flourish can appear to trigger twice: once when the CSS animation plays on load, and again when a JavaScript observer fires on mount and replays the same transition it thinks hasn’t happened yet.

Pick one clock per section and put everything on it, so it all starts and finishes from the same trigger. If you’re mixing animation systems in one component, check it directly: throttle the CPU in your browser’s dev tools, reload, and watch whether things that are supposed to move together actually do.

07

Only mark what’s actually visible as high priority

Responsive sites often build several layouts for the same section (mobile, tablet, desktop) and switch between them purely with CSS instead of only building the one that’s needed. Nothing wrong with that on its own. The problem shows up when each layout independently marks its own hero image as high priority: on a desktop screen, that tells the browser to urgently preload the mobile and tablet versions too, files the visitor will never see, competing for bandwidth against the one image they’re actually about to look at.

Only the layout that’s actually visible at the current screen size should be high priority. Everything else loads normally, or not at all if it’s never shown. If your image component supports a responsive sizes attribute, that’s often a cleaner fix than three separate images altogether: one element, one priority hint, and the browser picks the right source itself based on the viewport it’s actually rendering into.

Check your own site: look at the network requests at a single breakpoint and count how many images are downloading that aren’t even on screen.

Workflow

A repeatable process, not a one-off fix

None of the seven fixes above are useful as a single pass. Performance drifts back over time: a new hero photo lands unresized, a new script gets added at the top of the layout, a page quietly loses its revalidation window during a refactor. It helps to run the same loop every time rather than treating a good Lighthouse score as a finish line.

  1. Baseline first. Run the audit against the real production build, not localhost, and save the report. A score only means something once you have something to compare it to.
  2. Sort by leverage, not by list order. Oversized images and JS-gated visibility usually dwarf everything else on the report. Fix those first, whatever order the tool happens to list them in.
  3. Change one thing at a time. Rebuild and remeasure after each fix. Bundling several changes into one deploy makes it impossible to tell which one moved the score, and just as easy to hide a regression behind an improvement.
  4. Re-run under the same conditions. Same device profile, same network throttle, same number of runs. A single Lighthouse run can vary by several points on its own, so take the median of a few.
  5. Check again after it ships. Some regressions only show up under real traffic on real devices, not the throttled profile a lab tool simulates.
  6. Keep watching, not just at launch. A score that's good today can quietly regress as new features land. Running the same audit on a schedule (weekly, or on every deploy) catches drift before it becomes a redesign-sized problem.

AI in the loop

Where an AI assistant fits into this, and where it doesn’t

An AI coding assistant doesn’t replace any of the five steps above, but it can compress the slow parts of a few of them if you give it the right input. Instead of reading through a Lighthouse report yourself and manually ranking findings, feed it the raw exported JSON and ask it to sort the flagged issues by likely impact. Most of these reports repeat the same handful of offenders (an unoptimised image, a page rendering fresh on every request, a hero gated behind JavaScript), and an assistant that’s read enough of them tends to spot the pattern faster than a first manual pass through the raw output.

The same goes for the fix itself, once you’ve pointed it at the specific file. Swapping a motion.div for a CSS keyframe animation, flipping a force-dynamic flag to a revalidate window, moving a script tag from afterInteractive to lazyOnload: these are small, mechanical, well-documented changes, which is exactly the kind of edit an assistant tends to get right on the first pass. It can also draft the before/after comparison as it goes, which saves the separate step of writing one up afterwards.

What it can’t do is confirm any of it worked. A diff that compiles isn’t the same as a page that’s actually faster, and an assistant working from a description of the bug has no way to know the fix landed without you re-running the audit and watching the score move. Use it to skip the boilerplate in triage and drafting; keep the remeasuring and the post-ship check as something a human, or at least a real Lighthouse run, still does.

The takeaway

What to check first

None of these seven fixes change how a page looks. Same photos, same layout, same animations. Visitors see exactly what they’d have seen before, they just don’t wait as long to see it. Roughly in order of how much each tends to matter:

  1. Check that your framework’s built-in image optimisation is actually switched on. One disabled config flag can undo every other image fix on the site.
  2. Check what you’re actually downloading versus what’s actually being shown. Oversized source images are the most common and highest-impact performance bug on the web, full stop.
  3. Check how your pages are actually being rendered. If a page’s content is the same for every visitor, it almost never needs a “render fresh on every request” flag.
  4. Push third-party scripts to load after the page is idle, not the moment it becomes interactive.
  5. Never gate your largest visible element’s visibility behind JavaScript. Animate it with CSS instead.
  6. Watch for animation systems running on different clocks when you mix CSS- and JavaScript-driven motion in the same component.
  7. Audit what’s marked “high priority to load” on responsive layouts. You may be preloading images nobody on that screen size will ever see.

Work with us

Have a project in mind? Let's talk.

Pilots, platforms, or roadmaps — tell us what you're building and we'll get back within one business day.

Newsletter

Get our latest writing in your inbox.

Agentic engineering, AI platforms, and what we learn shipping them — no spam, unsubscribe anytime.