Written by
Halkwinds Editorial Team
Halkwinds Research & Editorial

Frontend Performance Optimization: Core Web Vitals and Beyond
Practical techniques for LCP, CLS, INP, bundle splitting, image optimization, and rendering strategies that move the needle.
Frontend performance is no longer a nice-to-have you address in a pre-launch cleanup sprint. It directly shapes conversion rates, search ranking, and how your engineering team spends its time. For engineering managers, the challenge is rarely a lack of awareness — everyone knows Core Web Vitals matter. The real problem is prioritization: which of the dozens of possible optimizations actually move the needle, how do you measure improvement reliably, and how do you keep performance from regressing after every feature ships? This guide walks through the techniques that consistently deliver results — LCP, CLS, and INP improvements, bundle splitting, image optimization, and rendering strategy — with enough specificity that your team can act on Monday morning.
- Background / Why This Matters
- Core Concepts and Architecture
- Implementation Strategy
- Scaling and Operational Considerations
- Common Mistakes / What to Avoid
- Frequently Asked Questions
- Conclusion
Background / Why This Matters
Google's Core Web Vitals became a ranking signal in 2021, and the metrics themselves have evolved since. As of March 2024, Interaction to Next Paint (INP) replaced First Input Delay (FID) as the responsiveness metric. That change matters because INP is harder to pass — it measures the latency of all interactions across a page session, not just the first one. Teams that comfortably passed FID often fail INP on interaction-heavy pages.
The three current Core Web Vitals are:
- Largest Contentful Paint (LCP) — loading performance. Good: ≤ 2.5s.
- Cumulative Layout Shift (CLS) — visual stability. Good: ≤ 0.1.
- Interaction to Next Paint (INP) — responsiveness. Good: ≤ 200ms.
Why should an engineering manager care beyond SEO? Research consistently suggests that faster pages correlate with higher conversion and lower bounce rates, and case studies from major retailers have reported meaningful revenue lifts from load-time improvements — though exact figures vary by industry and baseline. Just as importantly, a slow frontend is a symptom of accumulating technical debt: oversized bundles, unoptimized third-party scripts, and render-blocking resources that also make your codebase harder to maintain.
Actionable takeaway: Establish your current baseline before doing anything else. Pull field data from the Chrome User Experience Report (CrUX) via PageSpeed Insights, and set up lab testing with Lighthouse. Field data reflects real users; lab data gives you a controlled, repeatable benchmark. You need both.
Core Concepts and Architecture
Before optimizing, understand what each metric actually measures at the browser level — this is where most teams go wrong by treating symptoms instead of causes.
LCP: what's really slow
LCP is almost always one of four things: slow server response (TTFB), render-blocking JavaScript and CSS, slow resource load time (usually a hero image or web font), or client-side rendering delay. On a client-rendered React SPA, the LCP element frequently can't even begin rendering until a large JavaScript bundle downloads, parses, and executes. This is why rendering strategy is foundational to LCP.
Rendering strategies compared
Your choice of rendering strategy sets a performance ceiling. Here's how the common approaches stack up:
| Strategy | LCP characteristics | Best for | Tooling example |
|---|---|---|---|
| Client-Side Rendering (CSR) | Slow — blocked by JS download/parse | Auth-gated dashboards, internal apps | Create React App, Vite SPA |
| Server-Side Rendering (SSR) | Fast first paint, TTFB-dependent | Dynamic, personalized content | Next.js, Remix |
| Static Site Generation (SSG) | Fastest — served from CDN edge | Marketing, blogs, docs | Next.js, Astro |
| Incremental Static Regeneration (ISR) | Fast, with fresh data | Catalogs, high-traffic content | Next.js |
| React Server Components (RSC) | Fast — ships less client JS | Mixed static/dynamic apps | Next.js App Router |
CLS: the layout stability model
CLS accumulates every time visible content shifts unexpectedly. The usual culprits are images without dimensions, ads or embeds injected after load, web fonts causing FOIT/FOUT reflow, and dynamically injected content pushing the page down. The mental model is simple: reserve space for anything that loads late.
INP: the main-thread bottleneck
INP is fundamentally about main-thread availability. When a user clicks and your app runs 300ms of synchronous JavaScript before painting, that's your INP. Long tasks — anything over 50ms — are the enemy. Hydration in SSR frameworks is a common INP killer because the entire page becomes interactive in one expensive pass.
Actionable takeaway: Map each of your key pages to a rendering strategy deliberately. A marketing homepage rendered as CSR is an unforced error; move it to SSG or SSR. Halkwinds regularly audits application architectures and finds that a rendering-strategy realignment alone resolves a large share of LCP failures before any micro-optimization begins.
Implementation Strategy
With the concepts clear, here is a concrete, sequenced plan. Work top-down — the earlier items have the highest leverage.
1. Fix bundle size with code splitting
Ship less JavaScript. Start by analyzing your bundle with webpack-bundle-analyzer or the Next.js bundle analyzer. Look for:
- Route-based splitting — load code per route, not all upfront. Next.js does this automatically per page; in a plain React SPA use
React.lazyandSuspense. - Heavy dependencies — a 300KB date library or charting package loaded on every page. Replace moment.js with date-fns or day.js, and lazy-load charts.
- Duplicate dependencies — multiple versions of the same library pulled in by transitive dependencies. Deduplicate with your package manager.
Component-level dynamic imports (next/dynamic or React.lazy) let you defer non-critical UI like modals, rich-text editors, and below-the-fold widgets until they're needed.
2. Optimize images aggressively
Images are the LCP element on most content pages. Non-negotiables:
- Serve modern formats — WebP or AVIF — with fallbacks.
- Set explicit
widthandheight(or CSS aspect-ratio) to prevent CLS. - Use responsive
srcsetso mobile users don't download desktop-sized images. - Add
loading="lazy"for below-the-fold images, but never lazy-load the LCP image — it delays your most important paint. - Preload the hero image with
<link rel="preload">or Next.jspriority.
The Next.js <Image> component handles most of this automatically, including format negotiation and lazy-loading defaults.
3. Break up long tasks for INP
Audit interactions with the Chrome DevTools Performance panel. When you find a long task, break it up:
- Defer non-urgent state updates with
startTransitionin React 18+. - Yield to the main thread with
scheduler.postTaskor asetTimeout-based yield for heavy loops. - Debounce expensive handlers (search-as-you-type is a classic offender).
- Memoize expensive renders with
useMemoandReact.memo— but profile first; premature memoization adds noise.
4. Tame third-party scripts
Analytics, chat widgets, tag managers, and A/B testing tools are frequently the single largest source of main-thread blocking. Use Next.js <Script> with the strategy="lazyOnload" or afterInteractive" options, load non-critical scripts after interaction, and consider running heavy third parties in a web worker via Partytown.
5. Optimize fonts
Use font-display: swap, self-host fonts (or use next/font which does this and eliminates layout shift by computing fallback metrics), preload the primary font file, and subset fonts to the characters you actually use.
Actionable takeaway: Sequence the work by leverage: rendering strategy and bundle size first, then images, then INP long tasks, then third parties and fonts. Measure after each change so you can attribute gains and avoid shipping optimizations that don't help.
Scaling and Operational Considerations
A one-time optimization sprint is worthless if performance silently regresses over the next quarter. The goal is to make performance a durable property of your delivery pipeline.
Continuous measurement in CI
Integrate Lighthouse CI into your pull-request pipeline. Configure performance budgets — for example, fail the build if the main bundle exceeds 200KB gzipped, or if LCP in the lab exceeds a threshold. This turns performance from a periodic fire drill into a guardrail that catches regressions before merge.
Field monitoring with RUM
Lab tests miss real-world conditions: slow devices, poor networks, and geographic latency. Deploy Real User Monitoring using the web-vitals JavaScript library to capture LCP, CLS, and INP from actual sessions, and segment by device type and route. You'll often discover that your INP problem is concentrated on mid-tier Android devices your team never tests on.
Budgets and ownership
Assign an owner for each Core Web Vital, or better, make performance a shared standard enforced by tooling rather than heroics. Establish budgets per route category — a data-heavy dashboard has a different realistic budget than a landing page.