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.
Vitals I track on elangodev.com
LCP on the homepage is dominated by the profile image and hero typography. I preload /profile.webp with fetchPriority high from page.js and keep the hero image sized explicitly so the browser can reserve space. INP regressions usually came from heavy client enhancers on first paint — HomePageEnhancements is dynamic-imported below the fold for that reason.
CLS showed up when featured article covers loaded without dimensions. Next/Image with fill inside a fixed-height container fixed the shift. If you embed AdSense units later, reserve min-height for ad slots before approval so CLS does not spike the day ads turn on.
INP: third-party scripts
AdSenseScript uses strategy lazyOnload. Analytics is consent-gated. Chat is a separate route so the marketing homepage does not pay for realtime client bundles. That isolation is a Core Web Vitals strategy as much as a product decision.
// Bad: hydrate a chat socket on every marketing page
// Good: /chat owns the realtime bundle; / stays editorialHow I verify after each deploy
Field data in Search Console beats lab-only Lighthouse for AdSense-adjacent quality signals. I still run Lighthouse on a mid-tier mobile profile after homepage changes. If LCP jumps, I check hero image weight and font loading before touching React code.
Content length vs performance myth
Long articles are not the enemy of LCP — late-discovered hero media and blocking scripts are. Deep posts on /blog/[slug] stream HTML from the server; readers get text immediately while covers load. That pattern supports both AdSense content depth and vitals.
Before/after notes from the homepage redesign
Moving Work History off the homepage reduced DOM weight and deferred several timeline animations. Featured articles now appear earlier in the HTML, which helps both LCP-adjacent content visibility and human reviewers who scroll for substance. That is a rare case where content strategy and vitals strategy agree.
I keep font loading conservative — no flash of invisible text for the hero headline. System fallbacks remain acceptable for body copy on slower networks; brand display fonts must not block article text.
Checklist after content expansion
Longer articles increase HTML weight. I verify that expansion did not reintroduce blocking scripts, that images keep width/height or fixed aspect wrappers, and that below-the-fold embeds stay lazy. Depth and speed are not opposites when media and third parties stay disciplined.
For elangodev.com the winning combo in this recovery cycle is: deeper keepers, quieter homepage JS, reserved space for future ad slots, consent-gated analytics.
Reader outcomes
After reading this guide on elangodev.com you should be able to implement the pattern in a real Next.js codebase, explain the trade-offs to a teammate, and know which failure modes I already hit in production. That outcome-focused bar is how I decide whether an article is done — not a round word count alone, though we now target roughly 1,500 words of substantive detail for keepers in the AdSense recovery corpus.





