Real-world techniques that helped us improve LCP, INP, and CLS across multiple production websites.
Performance isn't just a Lighthouse score anymore. Core Web Vitals directly affect user experience, search visibility, and conversion rates. Over the past year, we worked on several client projects where performance had become a serious issue.
The interesting part? None of these projects required a complete rewrite.
Instead, we focused on identifying the biggest bottlenecks and fixing them one by one.
Here's what worked.
The Initial Problems
Most projects had similar issues:
- Large hero images delaying page rendering
- Render-blocking CSS and JavaScript
- Third-party scripts loading before important content
- Layout shifts caused by images, fonts, and ads
- Excessive client-side JavaScript
- Slow API responses affecting initial page load
Typical Lighthouse scores ranged from 40–65 on mobile.
Core Web Vitals were often failing:
- Largest Contentful Paint (LCP): 4–7 seconds
- Interaction to Next Paint (INP): 300–600ms
- Cumulative Layout Shift (CLS): 0.2–0.4
1. Optimizing Images First
This delivered the biggest improvements.
We replaced oversized JPEGs with modern formats like WebP and AVIF, resized images appropriately, and lazy-loaded everything below the fold.
Before:
<img src="/hero.jpg">
After:
<img
src="/hero.avif"
width="1200"
height="700"
loading="eager"
decoding="async"
fetchpriority="high"
alt="Hero image"
/>
Key improvements:
- Explicit width and height eliminated layout shifts
- Smaller image formats reduced download size
- High fetch priority improved LCP
Result:
- LCP improved by nearly 1.5 seconds on several projects.
2. Reducing Render-Blocking Resources
Many sites loaded dozens of CSS and JavaScript files before displaying anything.
We:
- Removed unused CSS
- Deferred non-critical JavaScript
- Split bundles using dynamic imports
- Inlined critical CSS
Instead of:
<script src="app.js"></script>
We used:
<script src="app.js" defer></script>
And lazy-loaded components:
const Dashboard = lazy(() => import("./Dashboard"));
This allowed browsers to render meaningful content much sooner.
3. Prioritizing the Hero Section
The largest content element usually determines LCP.
We made sure the browser prioritized it.
Examples:
<link
rel="preload"
as="image"
href="/hero.avif"
>
We also:
- Reduced hero animations
- Simplified above-the-fold layouts
- Avoided unnecessary sliders
A static hero almost always outperformed complex carousels.
4. Fixing Layout Shifts
CLS issues were everywhere.
Common causes:
- Images without dimensions
- Ads injected after load
- Web fonts swapping late
- Dynamic banners
We fixed them by reserving space.
Example:
.hero-image {
aspect-ratio: 16 / 9;
}
Font loading:
font-display: swap;
Result:
CLS often dropped from 0.28 to 0.02.
5. Optimizing Fonts
Several websites loaded 10–15 font files.
Instead:
- Used only required font weights
- Self-hosted fonts
- Preloaded critical fonts
- Used system fonts where possible
Example:
<link
rel="preload"
href="/fonts/inter.woff2"
as="font"
type="font/woff2"
crossorigin
>
Small changes, noticeable impact.
6. Reducing JavaScript Execution
Large JavaScript bundles often hurt INP more than network speed.
We removed:
- Unused libraries
- Duplicate plugins
- Heavy UI frameworks where unnecessary
Instead of loading everything on page load:
button.addEventListener("click", async () => {
const module = await import("./checkout.js");
module.start();
});
Users only downloaded code when needed.
7. Delaying Third-Party Scripts
Analytics, chat widgets, A/B testing tools, and marketing scripts often slowed down pages.
We delayed them until after initial rendering.
Instead of loading immediately:
<script src="analytics.js"></script>
We loaded them later:
window.addEventListener("load", () => {
const script = document.createElement("script");
script.src = "/analytics.js";
document.body.appendChild(script);
});
Users saw content sooner without losing analytics.
8. Improving Server Response Times
Frontend optimization only goes so far.
Backend improvements included:
- Better database indexing
- API response caching
- CDN integration
- HTTP compression
- Long-lived cache headers
Reducing Time to First Byte (TTFB) had a noticeable effect on LCP.
9. Measuring Real User Performance
Lighthouse is useful, but synthetic tests don't tell the full story.
We tracked:
- Real-user Core Web Vitals
- Chrome User Experience Report (CrUX)
- PageSpeed Insights
- Web Vitals library
Monitoring actual user metrics helped prioritize fixes that mattered most.
Results
Across multiple client projects, we commonly observed:
| Metric | Before | After |
|---|---|---|
| LCP | 4.8s | 2.1s |
| INP | 420ms | 145ms |
| CLS | 0.27 | 0.03 |
| Mobile Lighthouse | 58 | 94 |
| JavaScript Bundle | 1.3MB | 470KB |
While every project was different, the pattern remained consistent: addressing the biggest bottlenecks first produced the most meaningful gains.
Lessons Learned
The biggest improvements rarely came from chasing perfect Lighthouse scores. Instead, they came from focusing on the user's experience:
- Optimize the content users see first.
- Keep JavaScript lean and load only what's needed.
- Reserve layout space to avoid visual shifts.
- Treat third-party scripts with caution.
- Measure performance using real user data, not just lab tests.
- Make performance part of your development workflow rather than a one-time optimization task.
Final Thoughts
Improving Core Web Vitals doesn't require rebuilding an application from scratch. In many cases, a series of targeted optimizations—better image delivery, smarter resource loading, leaner JavaScript, and faster server responses—can dramatically improve both user experience and search performance.
If you're tackling a slow website, start by identifying the largest bottlenecks instead of optimizing everything at once. Incremental improvements compound quickly, and your users will notice the difference long before they see your Lighthouse score.
Top comments (0)