Why Core Web Vitals matter for my portfolio
elangodev.com is both a portfolio and a content site preparing for AdSense. Google ranks on experience signals — Largest Contentful Paint for loading, Interaction to Next Paint for responsiveness. Vercel Speed Insights on my layout gives real-user data, but I also optimize proactively because recruiters on mobile networks judge me in the first three seconds.
My stack combines Next.js 16, Framer Motion, Supabase fetches, Google Analytics, and conditional AdSense. Each third party can steal main-thread time. I treat Core Web Vitals as a budget negotiation between marketing scripts and the craft motion I showcase.
LCP: hero image and server-rendered profile
LCP on my homepage is usually the profile photo or headline text block. app/page.jsx server-loads initialProfile so HomeContent renders meaningful HTML immediately. The profile Image uses priority and fetchPriority="high" with explicit sizes so the browser downloads an appropriately sized file — not a desktop hero on a phone.
I lazy-load the decorative banner with loading="lazy" and low opacity so it never wins LCP. Preconnect hints in layout.jsx target Supabase and analytics domains early — small head tweaks that shave DNS and TLS setup off critical paths.
<link rel="preconnect" href="https://wktosroqlanmjwbanssy.supabase.co" />
<link rel="preconnect" href="https://www.googletagmanager.com" />Blog posts use cover_image with Next metadata for OG tags; in-article images get rounded prose styling without loading huge unoptimized assets. When I seed editorial content, I prefer elangodev.com/profile.png over multi-megabyte Unsplash URLs — fewer surprises in Lighthouse.
INP: defer analytics and respect consent
Interaction to Next Paint measures full click-to-paint latency across a page lifetime. AnalyticsTracker.jsx ran too eagerly in an early build — inserting on every pathname change during hydration. I moved tracking into requestIdleCallback with a two-second timeout fallback for Safari.
if ("requestIdleCallback" in window) {
window.requestIdleCallback(async () => {
await performTracking();
}, { timeout: 2000 });
} else {
setTimeout(performTracking, 2000);
}Before any insert, hasAnalyticsConsent() from lib/consent.js must return true. That gate aligns GDPR-style choices with performance: no geo lookup, no Supabase analytics row, no Google Analytics until the user clicks Accept All. Essential Only keeps the site usable without third-party work on the main thread.
AdSense discoverable, personalization gated
AdSenseScript.jsx loads the adsbygoogle library site-wide so Google can verify the publisher tag. Personalized storage stays denied by default via Consent Mode v2. AdUnit.jsx only calls adsbygoogle.push after hasAnalyticsConsent() returns true — so crawlers see the script while visitors who choose Essential Only never fire ad requests.
export default function AdSenseScript() {
const publisherId = resolvePublisherId();
return (
<Script
id="adsense-init"
async
src={`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${publisherId}`}
crossOrigin="anonymous"
strategy="lazyOnload"
/>
);
}strategy="lazyOnload" keeps the library off the critical path. I accept slightly later ad impressions because policy compliance and INP matter more than premature ad slots on a personal engineering blog.
Code splitting the homepage
HomeContent dynamically imports PublicNav, AboutSection, ExperienceTimeline, ContactSection, and Footer. Below-the-fold chunks download after the hero parses. I keep ssr: true on public sections so SEO crawlers still see complete markup — splitting is about JavaScript bytes, not hiding content from bots.
Dashboard routes push further: Chart.js loads only inside dynamically imported AnalyticsSection with ssr: false because canvas metrics are irrelevant to Googlebot. That pattern directly improved interaction responsiveness when toggling sidebar sections.
Motion without jank
Framer Motion drives scroll progress and hero entrances on elangodev.com. I animate transform and opacity — compositor-friendly properties — rather than height or top. useSpring on scrollYProgress smooths the top progress bar without layout thrash.
dashboard login uses spring transitions on the icon badge, but the form itself is plain React state. I avoid animating large subtrees during text input focus — keyboard users felt lag before I simplified.
Blog reading experience and CLS discipline
PostDetailContent wraps articles in prose containers with explicit image rounding but no layout-shifting ad slots above the fold until consent allows ad requests. I reserve min-height on skeleton loaders during client fallback fetch so slug navigation does not jump sidebar widgets. Share and bookmark buttons sit in a stable toolbar row — small cumulative layout shift wins that keep blog sessions comfortable on mobile where INP and CLS both contribute to perceived quality.
Measurement loop I actually run
After deploy, I check Vercel Speed Insights and run Lighthouse on / and a long blog post. I compare INP before and after adding scripts. If AdSense goes live, I re-test with consent accepted to capture worst-case main-thread cost.
I document regressions in commit messages — for example when adding ipapi.co geo lookups to analytics. The five-second AbortController timeout prevents hung fetch from blocking idle callbacks indefinitely.
Practical checklist for similar sites
Server-fetch above-the-fold content. Preconnect to your database and tag manager origins. Keep the AdSense library discoverable while gating personalized ad requests on explicit consent. Defer non-critical tracking with requestIdleCallback. Dynamic-import heavy chart and editor bundles. Animate transforms, not layout.
Core Web Vitals optimization on elangodev.com is the sum of small disciplined choices — not a single webpack tweak. I want the site to feel as polished as the interfaces I build for clients, and Google's metrics give me an honest scorecard for that ambition. I revisit this checklist after every major feature launch.



