Frontend Performance Metrics: LCP, FID, CLS, and Core Web Vitals
Core Web Vitals: Google's Performance Bar
In 2020 Google introduced Core Web Vitals as a search ranking signal:
| Metric | Meaning | Good | Needs work | Poor |
|---|---|---|---|---|
| LCP | Largest Contentful Paint | ≤2.5s | ≤4s | >4s |
| INP | Interaction to Next Paint | ≤200ms | ≤500ms | >500ms |
| CLS | Cumulative Layout Shift | ≤0.1 | ≤0.25 | >0.25 |
As of March 2024, INP officially replaced FID as the interactivity metric.
LCP: Largest Contentful Paint
What is LCP?
LCP measures when the largest visible content element renders—typically:
- Images (
<img>) - Background images (CSS
background-image) - Text blocks (
<p>,<h1>) <video>poster frames
Four phases of LCP
TTFB → resource load delay → resource load time → element render delay
↑ ↑ ↑ ↑
Server <link rel="preload"> CDN distance critical CSS/fonts
response priority/preload /compression blocking render
Optimization strategies
1. Optimize TTFB
ToolsKu TTFB: ~50ms (CDN serves static HTML directly)
Typical SSR site: 200–500ms (server render per request)
Static export + CDN naturally yields very low TTFB.
2. Preload critical resources
<link rel="preload" href="/fonts/geist-sans.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="/i18n/toolClients/zh-CN.json" as="fetch" crossorigin />
ToolsKu preloads tool client JSON in layout.tsx.
3. Font optimization
// next.config.ts - npm package fonts instead of Google Fonts CDN
// Avoid: fonts.googleapis.com often ECONNRESET in some regions
import { GeistSans } from 'geist/font/sans';
4. Image optimization
For tool sites, LCP is often hero text—not images. Blog images still matter:
<img
src="photo.webp"
loading="eager" <!-- LCP image: not lazy -->
fetchpriority="high"
decoding="async"
/>
INP: Interaction to Next Paint
From FID to INP
FID only measured first input delay—not total interaction time. INP measures the full cycle of interactions:
User click/keypress
↓
Input event starts
↓
Event handlers (JS)
↓
Style/layout
↓
Paint
↓
INP = time from input start to paint complete
Common INP issues
Long main-thread tasks
// Bad: sync heavy work blocks the main thread
function processData(data) {
const result = heavyComputation(data); // blocks ~500ms
updateUI(result);
}
// Good: chunk or use a Worker
async function processData(data) {
const result = await processInWorker(data);
updateUI(result);
}
React re-renders
// Bad: full list re-render on filter
setFilter(keyword); // 1000 tool cards re-render
// Good: virtualization + useMemo
const filtered = useMemo(
() => tools.filter(t => t.name.includes(keyword)),
[tools, keyword]
);
ToolsKu INP optimizations
- Pre-built search index:
tools-search/zh-CN.jsonat build time—no scanning all tools at runtime - WASM in Workers: FFmpeg.wasm runs off the main thread
- React.lazy: Tool page components load on demand, smaller main bundle
CLS: Cumulative Layout Shift
How CLS is computed
CLS = Σ (impact fraction × distance fraction)
Impact fraction: how much of the viewport shifted
Distance fraction: how far elements moved relative to the viewport
Common CLS sources
| Source | Example | Fix |
|---|---|---|
| Images without dimensions | <img src="..."> |
width/height or aspect-ratio |
| Late-injected content | Ads/recommendations | Reserve space (min-height) |
| Web font FOIT | Text invisible then visible | font-display: swap + metric fallbacks |
| Async UI | Search box/modal late render | min-height reservation |
ToolsKu CLS optimizations
/* Fixed nav height to avoid jump on load */
nav[data-tools-nav] {
/* height set via CSS variable after JS measure */
}
/* Fixed footer height */
:root {
--site-footer-h: 33px;
}
body {
padding-bottom: var(--site-footer-h);
}
Measurement Tools
Lab data
| Tool | Use |
|---|---|
| Lighthouse | CI automated audits |
| Chrome DevTools Performance | Frame-by-frame analysis |
| WebPageTest | Multi-location/device testing |
Field data
| Tool | Use |
|---|---|
| Chrome UX Report (CrUX) | Real-user data from Google |
| web-vitals JS library | Self-hosted RUM |
| Google Search Console | Core Web Vitals report |
Collecting with web-vitals
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP((metric) => {
gtag('event', 'web_vital', {
name: 'LCP',
value: metric.value,
rating: metric.rating,
});
});
onINP((metric) => { /* ... */ });
onCLS((metric) => { /* ... */ });
ToolsKu Performance Numbers
Lighthouse (simulated 4× slowdown, desktop):
| Metric | Value | Rating |
|---|---|---|
| Performance | 95 | 🟢 |
| LCP | 1.2s | 🟢 Good |
| INP | 80ms | 🟢 Good |
| CLS | 0.02 | 🟢 Good |
| TTFB | 50ms | 🟢 Good |
| FCP | 0.8s | 🟢 Good |
| TTI | 1.5s | 🟢 Good |
Where the wins come from
- Pure static HTML: CDN serves pages directly—minimal TTFB
- No client render wait: Server Components pre-rendered
- Lazy WASM: FFmpeg (~30MB) only on video tool pages
- Local fonts: No Google Fonts CDN latency/failures
- Preload critical assets: Search index, tool client JSON
Optimization Checklist
LCP
- Identify LCP element (DevTools → LCP marker)
- Preload LCP images/fonts
- CDN deploy to lower TTFB
- Inline critical CSS for above-the-fold
-
fetchpriority="high"on LCP resources
INP
- Find long tasks (DevTools → Long Tasks)
- Move compute to Web Workers
-
requestIdleCallbackfor non-urgent work - React:
useMemo/useCallback/ virtualization
CLS
- Images:
width+heightoraspect-ratio - Fonts:
font-display: swap+size-adjust - Reserve space for dynamic content
- Avoid
document.body.appendChildlayout shifts
Summary
Core Web Vitals are more than SEO—they measure real user experience. ToolsKu combines static export + CDN + preload + Worker offload + local fonts to hit LCP < 1.5s, INP < 100ms, and CLS < 0.05.
Use the OG preview tool to check share cards and HTTP headers analyzer to verify cache policies.
Try these browser-local tools — no sign-up required →