Web Performance: The 80/20 Fixes That Take You From Lighthouse 60 to 90+

The Web Stack · August 26, 2026
The Web Stack WEBP

Most performance advice you read online is either obvious ("don't load 10MB of JavaScript") or impossible to action without a team of infrastructure engineers ("implement HTTP/3 with custom prioritization"). Neither helps a solo developer or a small team staring at a slow site and a Lighthouse score in the 60s.



This article is the 80/20 version. The changes that get you from a Lighthouse 60 to a Lighthouse 90+ on a typical content site or small web app, with one or two days of work. No build pipeline rewrites, no Web Vitals PhD required.



Start with measurement, not with fixes



Open Chrome DevTools. Go to the Lighthouse tab. Run a mobile audit. Read the three Core Web Vitals at the top: LCP, CLS, INP. Whatever is red or yellow is what you fix. Whatever is green, leave alone.



LCP is "Largest Contentful Paint" — how long until the biggest thing on the page is visible. Target: under 2.5 seconds. CLS is "Cumulative Layout Shift" — how much the page jumps around as it loads. Target: under 0.1. INP is "Interaction to Next Paint" — how long the page takes to respond when you click something. Target: under 200ms.



You will almost certainly have one bad number and two okay ones. Fix the bad one. That is your project for the day.



If LCP is the problem: the hero image is doing it



LCP is almost always an image. The hero photo, the logo, the first blog cover. The browser is downloading it, decoding it, painting it, and only then does the page feel loaded. The fix is one of three things, in order of impact.



Fix 1: preload the LCP image



Tell the browser to start downloading the image before it even sees the HTML that references it. Add this to the <head>:



<link rel="preload" as="image" href="/images/hero.webp" />


The browser starts the download immediately. By the time it gets to the HTML that needs the image, the bytes are already on the device. The image paints as soon as the rest of the page is ready. LCP drops by 200-500ms on a typical site.



The catch: you need to know which image is the LCP. Run a Lighthouse audit, click into the LCP element, see the URL. Preload that one. Do not preload every image on the page, or you will hurt everything else.



Fix 2: serve WebP or AVIF instead of JPEG/PNG



WebP is 25-35% smaller than JPEG at the same visual quality. AVIF is 20% smaller than WebP. Browsers have supported both for years. The conversion is one line in your build pipeline or one upload to a service like Squoosh.



Keep a JPEG fallback for ancient browsers, but most of your traffic will get the smaller file. Your LCP image goes from 250KB to 80KB. It downloads faster. The page paints faster. Done.



Fix 3: lazy-load everything else, but not this one



You have probably seen loading="lazy" on images. Good. Use it on every image below the fold. But the LCP image must not be lazy. If it is, the browser waits for layout to start the download. Use eager loading and a high fetchpriority:



<img src="/images/hero.webp" alt="..." fetchpriority="high" />


This single attribute tells the browser "this image matters more than the others, prioritize it." Combined with the preload link, your LCP should land in the green.



If CLS is the problem: the page is jumping around



CLS happens when the page changes layout after the user is already looking at it. The most common cause: an image loads without reserved space, and pushes everything down. The fix is to set width and height on every image.



<img src="/photo.jpg" width="800" height="600" alt="..." />


Even with responsive CSS that sets width: 100%, the browser uses the width and height attributes to calculate the aspect ratio. It reserves the right amount of space before the image loads. When the image arrives, it slots in. No jump. CLS goes to zero.



The same fix applies to embeds, ads, and dynamically-injected components. If a thing is going to appear on the page, reserve its space before it loads. Modern CSS has the aspect-ratio property if you prefer that to width/height attributes.



If INP is the problem: the main thread is blocked



INP is the hardest of the three to fix, because it usually means JavaScript is doing too much work. The user clicks a button, the browser has to finish whatever script is running before it can respond, and the response takes 300ms instead of 50.



The first thing to check: are you loading a giant analytics or tag manager script in the head? Every byte of JavaScript in the head is parsed and executed before the page can respond to a click. Move those scripts to load after the page is interactive, or use the defer attribute, or use a tag manager that loads scripts asynchronously.



The second thing: are you doing heavy computation in response to user events? If a click handler runs a synchronous loop over 10,000 items, that is the problem. Move the work to a Web Worker. Yield to the main thread periodically with await new Promise(r => setTimeout(r, 0)). Break the work into smaller chunks.



The third thing: are you using a heavy framework for things that do not need it? If your site is mostly content with one interactive widget, you may not need Vue or React for the whole page. A small vanilla script for the widget, server-rendered HTML for the rest, is dramatically faster than hydrating a SPA on every page load.



The other three quick wins



Beyond the Core Web Vitals, these three changes take an hour each and move the needle.



1. Compress everything with gzip or brotli



If your HTML, CSS, and JavaScript are served uncompressed, your server is sending 3-4x more bytes than it needs to. Most hosting providers enable gzip by default, but check. You want brotli if you can get it; it is 15-20% smaller than gzip on text content.



Test: in DevTools, look at the network tab, see the "Content-Encoding" header. If it says "br" or "gzip", you are good. If it says nothing, fix your server config.



2. Cache aggressively



Static assets (CSS, JavaScript, images, fonts) should be served with a far-future Cache-Control header. Versioned files (with a hash in the filename like main.ab3f.js) can be cached forever. The browser will only refetch when the filename changes, which is when you actually changed the file.



Cache-Control: public, max-age=31536000, immutable


Add this to your .htaccess or nginx config for your /static/ or /assets/ folder. Returning visitors will barely download anything.



3. Defer non-critical CSS and JavaScript



Your page has CSS for things the user does not see on first load (the cookie banner, the dropdown menus, the print styles). You can split that CSS into a separate file and load it after the page renders:



<link rel="preload" href="/css/non-critical.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/css/non-critical.css"></noscript>


The same applies to scripts. Anything that is not needed for the page to be visible and clickable should be deferred or loaded on demand. The page becomes interactive faster, INP drops, the user is happier.



What to skip



You do not need to rewrite your app in a faster framework. You do not need a service worker for a content site. You do not need a custom CDN or edge workers. You do not need to chase the last 5ms of LCP. The 80/20 lives in the few changes above. Ship them, measure, and stop. The remaining optimizations are diminishing returns, and your time is better spent on features your users actually want.



That is the whole game. Preload the LCP image, set width/height on everything, defer the heavy scripts, compress and cache. One focused day. Lighthouse 60 to 90. Ship.


Frequently asked questions


What is a good Lighthouse score to aim for?


90+ across all four categories is the practical target. 100 is possible but rarely worth the engineering effort for a content site. The biggest jumps come from the changes in this article; after that you are in diminishing returns territory.


Do I need a CDN?


For a small site serving one country, not necessarily. The wins from a CDN come from geographic distribution. If your users are mostly in one region and your hosting is in that region, the marginal benefit is small. Focus on the basics first.


Should I use a static site generator?


If your content does not change on every request, a static site is dramatically faster than a dynamic one. Hugo, Astro, and Eleventy are all solid. The trade-off is that interactive features are harder. For most content sites, the trade is worth it.


Related articles



← More in The Web Stack