Frontend Performance Optimization in Practice: A Complete Guide to Core Web Vitals

Introduction
One day, my manager approached me saying our website’s Lighthouse score was only 60, and I had one week to get it to 90. To be honest, I panicked. Opening Chrome DevTools and staring at that colorful Lighthouse report, metrics like LCP, FID, and CLS completely confused me. Performance optimization—for many frontend developers, it’s just unexpected extra work.
I’ll be honest, I’ve stepped on plenty of landmines. The first time I optimized, I set lazy loading on all images, and my LCP score actually dropped. Later I discovered that above-the-fold hero images shouldn’t be lazy-loaded at all. Another time, I spent two full days researching Service Worker caching strategies, only to improve the score by 2 points. The ROI was abysmal.
This article isn’t theoretical—it’s a practical guide that will show real results in 2 weeks. I’ll tell you which optimizations have the highest ROI, which pitfalls to avoid, and how to systematically improve your Lighthouse score from 60 to 90+ step by step. These are battle-tested insights with data and real cases.
Chapter 1: Understanding the Three Core Web Vitals
Don’t be intimidated by these acronyms. Simply put, it’s about: loading speed, interaction responsiveness, and visual stability.
What Are Core Web Vitals
Google introduced three core user experience metrics in 2020 that not only affect user experience but directly impact SEO rankings. Even if your content is excellent, poor performance will hurt your rankings. There was a major update in March 2024: INP replaced FID as a core metric. If you’re still optimizing FID, you’re behind.
LCP - Largest Contentful Paint
LCP measures how fast your main content loads, typically the hero image, headline, or video above the fold. The standards are:
- <2.5s: Good (green)
- 2.5-4s: Needs improvement (yellow)
- >4s: Poor (red)
I like to use a restaurant analogy: LCP is like how fast your main course arrives. If you wait 10 minutes and still no food, you’re not happy, right?
Real data: when page load time increases from 3 to 5 seconds, bounce rate jumps 38%. On mobile, if loading exceeds 3 seconds, 53% of users will leave. Optimize LCP well, and conversion rates can improve 7-15%.
INP - Interaction to Next Paint
This is the new metric from March 2024, officially replacing FID. INP measures how fast your page responds to user interactions, including the complete response cycle for clicks, keyboard input, and touches. The standards are:
- <200ms: Good (green)
- 200-500ms: Needs improvement (yellow)
- >500ms: Poor (red)
Why did Google replace FID? Because FID only measured “First Input Delay,” while INP covers the entire interaction process. Like ordering at a restaurant, FID only checks if the waiter heard you, while INP measures the entire process from ordering to food delivery.
The first time I saw our project’s INP was 650ms, clicking a button took over half a second—no wonder users complained about lag. Later I discovered it was YouTube auto-embed causing the issue. After removal, INP dropped to 220ms, and users clearly felt the improvement.
CLS - Cumulative Layout Shift
CLS measures visual stability. Ever experienced this: you’re about to click a button, suddenly the page jumps, and you hit an ad instead? That’s CLS at work. The standards are:
- <0.1: Good (green)
- 0.1-0.25: Needs improvement (yellow)
- >0.25: Poor (red)
When I first saw a CLS of 0.5, I thought 0.5 was pretty small. Later I learned that was already a disaster zone. Common CLS issues: images without width/height, ads suddenly inserted, font flashing (FOIT).
Chapter 2: LCP Optimization - Highest ROI Performance Win
Why do I prioritize LCP? Because:
- Most direct impact: Users feel it immediately
- Biggest optimization potential: Common LCP can improve from 5s to under 2s
- Mature technical solutions: Image optimization, CDN acceleration are proven approaches
- Highest ROI: Low investment, fast results, the king of cost-effectiveness
1. Image Optimization (ROI: ⭐⭐⭐⭐⭐)
This is the most important optimization, accounting for 70%+ of LCP issues. I always start with images for every performance optimization.
a) Use Modern Image Formats
Don’t underestimate image formats—switching from JPEG to AVIF can save 300KB per image.
<!-- Option 1: Use <picture> tag, browser auto-selects best format -->
<picture>
<source srcset="hero.avif" type="image/avif">
<source srcset="hero.webp" type="image/webp">
<img src="hero.jpg" alt="Hero image"> <!-- fallback for older browsers -->
</picture>// Option 2: Next.js auto-optimization (recommended)
import Image from 'next/image'
<Image
src="/hero.jpg"
width={1200}
height={600}
priority // Important: LCP images must have priority, no lazy loading!
/>Real numbers:
- AVIF has 41% better compression than JPEG
- WebP has 30% better compression than JPEG
- Real case: I optimized an e-commerce homepage, hero image from 500KB to 120KB (AVIF format), LCP from 4.2s to 2.1s—cut in half!
b) Image Lazy Loading (Except LCP Images)
Here’s a pitfall I hit: I lazy-loaded the LCP image too, and the score actually dropped.
<!-- ❌ Wrong: Don't lazy-load LCP images! -->
<img src="hero.jpg" loading="lazy">
<!-- ✅ Correct: Above-the-fold images use eager, lazy-load others -->
<img src="hero.jpg" loading="eager"> <!-- LCP image -->
<img src="product1.jpg" loading="lazy"> <!-- Below-fold images lazy-load -->
<img src="product2.jpg" loading="lazy">Remember: don’t lazy-load any images visible above the fold. Lazy loading is for images below the fold.
c) Responsive Images
Mobile users don’t need desktop-sized images. srcset can save 60% of traffic.
<img
src="hero-800w.jpg"
srcset="hero-400w.jpg 400w,
hero-800w.jpg 800w,
hero-1200w.jpg 1200w"
sizes="(max-width: 600px) 400px,
(max-width: 1000px) 800px,
1200px"
alt="Hero"
/>Real impact: mobile users loading 400w images instead of 1200w can improve LCP by 1 second.
d) Image CDN and Preload
CDN isn’t a luxury—it’s essential. Aliyun OSS costs just a few dozen dollars per year.
<!-- Preload critical images, browser loads them first -->
<link rel="preload" as="image" href="hero.jpg">
<!-- Use image CDN (auto format conversion + compression) -->
<!-- Tencent Cloud COS, Aliyun OSS, Cloudflare Images all support this -->
<img src="https://cdn.example.com/hero.jpg?x-oss-process=image/format,webp/quality,80">e) Image Size and Compression
Tool recommendations:
- TinyPNG: Online compression, simple and effective
- ImageOptim: Mac tool, batch compression
- Squoosh: Google’s tool, supports AVIF
Compression rules:
- Hero images 80% quality (visually minimal difference)
- Other images 70% (good enough)
- Size at 2x design width (for high-DPI screens)
f) Avoid Base64 Inline for Large Images
// ❌ Don't inline large images (>10KB)
// This increases HTML size and delays rendering
const heroImage = 'data:image/jpeg;base64,/9j/4AAQSkZJRg...' // 500KB
// ✅ Base64 only for small icons (<10KB)
// Like loading icons, simple SVG icons
const icon = 'data:image/svg+xml;base64,PHN2ZyB3aWR...' // 2KB2. Server Response Time Optimization (ROI: ⭐⭐⭐⭐)
a) Use CDN
Put all static resources on CDN, HTML can also be CDN-cached (combined with SSG/ISR). I’ve seen cases where TTFB dropped from 200ms to 50ms—users clearly felt the difference.
b) Server-Side Rendering (SSR) or Static Generation (SSG)
// Next.js - Static generation (preferred, best performance)
export async function getStaticProps() {
const data = await fetchData()
return {
props: { data },
revalidate: 60 // ISR: regenerate after 60 seconds
}
}
// Or server-side rendering (for real-time data requirements)
export async function getServerSideProps() {
const data = await fetchData()
return { props: { data } }
}c) Database Query Optimization
- Add indexes (don’t forget to analyze with explain)
- Use Redis to cache hot data
- Avoid N+1 queries (use join or dataloader)
3. Resource Loading Optimization (ROI: ⭐⭐⭐)
a) Inline Critical CSS
<!-- Inline critical CSS in <head> to avoid render-blocking -->
<style>
.hero {
width: 100%;
height: 600px;
background: #f0f0f0;
}
.nav {
position: fixed;
top: 0;
width: 100%;
}
</style>
<!-- Defer non-critical CSS -->
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>b) Font Optimization
/* Use font-display to avoid FOIT (Flash of Invisible Text) */
@font-face {
font-family: 'CustomFont';
src: url('font.woff2') format('woff2');
font-display: swap; /* Show fallback font immediately, avoid blank screen */
}<!-- Preload critical fonts -->
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>Chapter 3: INP Optimization - Making Interactions Smoother
Third-party scripts are the #1 killer of INP! I’ve seen the most extreme case: one page loaded 12 third-party scripts—Google Analytics, Facebook Pixel, chat widget, ad scripts… INP went straight to 800ms+.
1. JavaScript Execution Optimization (ROI: ⭐⭐⭐⭐)
a) Code Splitting
Code splitting sounds fancy, but it’s really just “load on demand”—users only load code for the page they visit.
// React - Route-level code splitting
import { lazy, Suspense } from 'react'
const Dashboard = lazy(() => import('./Dashboard'))
const Profile = lazy(() => import('./Profile'))
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</Suspense>
)
}
// Vue 3 - Supports async components too
const Dashboard = defineAsyncComponent(() => import('./Dashboard.vue'))Real impact: A admin dashboard went from 1.2MB to 200KB above-the-fold, INP from 450ms to 180ms. Users experienced pages loading twice as fast.
b) Break Up Long Tasks
Tasks blocking the main thread for over 50ms are long tasks, causing INP to spike.
// ❌ Long task blocking main thread (freezes UI)
function processLargeData(data) {
for (let i = 0; i < 10000; i++) {
// Complex calculation, processing 10K items at once
heavyCalculation(data[i])
}
}
// ✅ Use requestIdleCallback to break into smaller tasks
function processLargeData(data) {
let index = 0
function processChunk() {
const chunkSize = 100 // Process 100 items at a time
let count = 0
while (index < data.length && count < chunkSize) {
heavyCalculation(data[index])
index++
count++
}
if (index < data.length) {
// Not finished, continue when idle
requestIdleCallback(processChunk)
}
}
requestIdleCallback(processChunk)
}c) Use Web Workers
Move complex calculations to Workers, don’t block the main thread.
// worker.js
self.onmessage = (e) => {
const result = complexCalculation(e.data) // Complex calculation runs in Worker
self.postMessage(result)
}
// main.js
const worker = new Worker('worker.js')
worker.postMessage(data)
worker.onmessage = (e) => {
console.log('Result:', e.data)
// Got result, update UI
}2. Third-Party Script Optimization (ROI: ⭐⭐⭐⭐⭐)
Third-party scripts are like hosting a party—the more people, the more chaos. Every script wants to grab resources.
a) Defer Non-Critical Scripts
<!-- ❌ Blocking load (freezes page rendering) -->
<script src="analytics.js"></script>
<!-- ✅ Defer until page loads -->
<script defer src="analytics.js"></script>
<!-- ✅ Or manually delay 3 seconds (more aggressive) -->
<script>
window.addEventListener('load', () => {
setTimeout(() => {
const script = document.createElement('script')
script.src = 'analytics.js'
document.body.appendChild(script)
}, 3000) // Load analytics after user browses for 3 seconds
})
</script>b) Use Facade Pattern for Heavy Components
YouTube video embeds are 1MB+, loading directly will severely drag down INP.
// Lightweight YouTube facade, only loads real video on click
function VideoFacade({ videoId }) {
const [showVideo, setShowVideo] = useState(false)
if (!showVideo) {
return (
<div
className="video-facade"
style={{
backgroundImage: `url(https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg)`,
cursor: 'pointer'
}}
onClick={() => setShowVideo(true)}
>
<button className="play-button">▶ Play Video</button>
</div>
)
}
return <iframe src={`https://www.youtube.com/embed/${videoId}`} />
}Real impact: A blog page removed YouTube auto-embed, INP from 650ms to 220ms.
3. Event Handler Optimization (ROI: ⭐⭐⭐)
a) Debounce and Throttle
import { debounce, throttle } from 'lodash'
// Search box debounce (trigger only after user stops typing for 300ms)
const handleSearch = debounce((value) => {
fetchSearchResults(value)
}, 300)
// Scroll event throttle (max once per 100ms)
const handleScroll = throttle(() => {
updateScrollPosition()
}, 100)b) Use Passive Event Listeners
// Improve scroll performance, tell browser we won't call preventDefault
window.addEventListener('scroll', handleScroll, { passive: true })
window.addEventListener('touchmove', handleTouch, { passive: true })Chapter 4: CLS Optimization - Preventing Layout Shifts
CLS is like reading a book when someone suddenly pushes it up—you have to find your place again. Super annoying. The good news: CLS is the easiest to fix.
1. Set Dimensions for Images and Videos (ROI: ⭐⭐⭐⭐⭐)
Images without dimensions are the #1 cause of CLS, but also the easiest to fix.
<!-- ❌ No width/height, shifts when loading -->
<img src="photo.jpg" alt="Photo">
<!-- ✅ Set width/height, browser reserves space -->
<img src="photo.jpg" width="800" height="600" alt="Photo">
<!-- ✅ Or use CSS aspect-ratio (modern browsers) -->
<style>
img {
width: 100%;
aspect-ratio: 16 / 9;
}
</style>Real case: A news site added width/height to all images, CLS from 0.35 to 0.05. That simple.
2. Font Loading Optimization (ROI: ⭐⭐⭐⭐)
a) Use font-display: swap
@font-face {
font-family: 'CustomFont';
src: url('font.woff2') format('woff2');
font-display: swap; /* Avoid FOIT (blank screen during font load) */
}b) Preload Critical Fonts
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>c) Use System Fonts or Variable Fonts
/* System fonts, no loading needed, zero delay */
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
/* Variable Font, one file contains all weights (100-900) */
@font-face {
font-family: 'Inter';
src: url('Inter-Variable.woff2') format('woff2-variations');
font-weight: 100 900;
}3. Reserve Space for Dynamic Content (ROI: ⭐⭐⭐⭐)
a) Reserve Ad Space
.ad-container {
min-height: 250px; /* Reserve ad height, won't shift before ad loads */
background: #f0f0f0; /* Placeholder background */
}b) Skeleton Screens
Skeleton screens aren’t just pretty—they stabilize layout.
// Show skeleton before loading, prevent sudden content appearance causing shifts
function ProductCard({ loading, data }) {
if (loading) {
return (
<div className="skeleton">
<div className="skeleton-image" style={{ width: '100%', height: '200px', background: '#e0e0e0' }} />
<div className="skeleton-title" style={{ width: '80%', height: '20px', background: '#e0e0e0', margin: '10px 0' }} />
<div className="skeleton-price" style={{ width: '40%', height: '20px', background: '#e0e0e0' }} />
</div>
)
}
return (
<div className="product">
<img src={data.image} alt={data.title} />
<h3>{data.title}</h3>
<p>{data.price}</p>
</div>
)
}4. Avoid Inserting Content Above Existing Content (ROI: ⭐⭐⭐⭐⭐)
// ❌ Insert banner at top, causes content to shift down (CLS explosion)
<div>
{showBanner && <Banner />}
<Content />
</div>
// ✅ Use fixed positioning, doesn't affect layout
<div>
{showBanner && <Banner style={{ position: 'fixed', top: 0, zIndex: 1000 }} />}
<Content style={{ marginTop: showBanner ? '60px' : '0' }} />
</div>5. Use transform Instead of top/left for Animations (ROI: ⭐⭐⭐)
I’ve seen people use top for animations to show off, result: CLS explodes.
/* ❌ Triggers layout, causes CLS */
.element {
position: relative;
animation: slideIn 0.3s;
}
@keyframes slideIn {
from { top: -100px; } /* Changing top triggers reflow */
to { top: 0; }
}
/* ✅ Use transform, only triggers composite (GPU accelerated) */
.element {
animation: slideIn 0.3s;
}
@keyframes slideIn {
from { transform: translateY(-100px); }
to { transform: translateY(0); }
}Chapter 5: Practical Integration - Complete Optimization Workflow
Priority matters. Don’t jump straight to SSR—get image optimization done first. The first time I optimized, just adding width/height to images improved the score by 10 points. Really free wins.
Optimization Priority Matrix
| Optimization | ROI | Difficulty | Priority |
|---|---|---|---|
| Add image width/height | Very High | Very Low | P0 |
| Image format conversion | Very High | Low | P0 |
| LCP image optimization | Very High | Low | P0 |
| Defer third-party scripts | Very High | Low | P0 |
| Font optimization | High | Low | P1 |
| Code splitting | High | Medium | P1 |
| CDN | High | Low | P1 |
| SSR/SSG | Medium | High | P2 |
| Web Workers | Low | High | P3 |
Testing Tools and Commands
# 1. Lighthouse (most authoritative)
# Chrome DevTools > Lighthouse > Generate report
# Use incognito mode, otherwise Chrome extensions will interfere
# 2. WebPageTest (real device testing)
# https://webpagetest.org
# Can choose different regions, different network speeds
# 3. Chrome DevTools Performance panel
# Record loading process, analyze bottlenecks
# See time for each task
# 4. npm bundle analysis
npx webpack-bundle-analyzer
# Visualize which packages are largest
# 5. Image analysis
npx sharp-cli info image.jpg
# Check image dimensions, format, sizePitfall Guide
- ❌ Don’t test Lighthouse in production (use incognito mode or disable extensions)
- ❌ Don’t test only once (test at least 3 times and average, network fluctuates)
- ❌ Don’t only test desktop (mobile more important, 75% of traffic from mobile)
- ❌ Don’t ignore Network throttling (simulate slow network, Fast 3G)
- ❌ Never lazy-load LCP images (I stepped on this landmine)
Conclusion
Performance optimization isn’t a one-time task—it’s an ongoing process. Like fitness, it requires consistency and discipline. But when you see your Lighthouse score go from 60 to 90, it’s truly rewarding.
Performance optimization isn’t just technical work—it’s respect for user experience. Every second you optimize keeps more users engaged. Mobile users have only 3 seconds of patience, and you now have the ability to reduce that to 1 second.
Let’s work together: give every user a smooth browsing experience.
Published on: Nov 24, 2025 · Modified on: Dec 4, 2025
Related Posts

Complete Guide to Deploying Astro on Cloudflare: SSR Configuration + 3x Speed Boost for China

Building an Astro Blog from Scratch: Complete Guide from Homepage to Deployment in 1 Hour
