← Back to Work

Balaconah — Core Web Vitals

Built public pages and backend services on a large Arabic media platform, and reduced mobile LCP from 3.8s to 1.2s (68%) through systematic Next.js performance work.

Role
Senior Frontend Engineer
Published
April 2025
LCP before
3.8s
LCP after
1.2s
Reduction
−68%
Next.js 16React 19TypeScriptContentfulSupabaseAnt Designnext/imageISR
View live site →

Context

Balaconah is a Next.js App Router platform combining an Arabic media surface (news, articles, opinion, live reports, video, glossary) with a brand directory and identities registry, plus events, partnerships, calendars, and brand-comparison tooling — 50+ public routes in total, with Contentful as the editorial CMS.

Because it is content-heavy, Arabic-first, and SEO-driven, performance is not cosmetic: real-user monitoring showed LCP consistently above 3.5s on mobile — well inside Google's "Needs Improvement" band and approaching the "Poor" threshold. Core Web Vitals directly affect organic search rankings and first-impression bounce rate, so this workstream was prioritised.

Problem

The brief was clear: get LCP below 2.5s (Google's "Good" threshold) on mobile CrUX data. Starting measurement: 3.8s. The investigation needed to identify root causes, not just apply a generic checklist.

Hypotheses going in:

  1. Hero image not using next/image — likely largest contentful paint element unoptimised
  2. Render-blocking third-party scripts above the fold
  3. Client-rendered above-the-fold content (SSR gap)
  4. Fonts loaded synchronously with no font-display strategy

Architecture — before and after

Implementation

1. Hero image — next/image with priority

The hero image was served as a plain <img> tag with a full-resolution URL. No width/height, no responsive sizes, no preload. Replacing it with next/image and adding priority immediately triggers a <link rel="preload"> in the document <head>, so the browser fetches it before it even parses the JS bundle.

// Before
<img src="/hero.jpg" className="hero" />
 
// After
<Image
  src="/hero.jpg"
  alt="Balaconah"
  width={1200}
  height={630}
  priority
  sizes="(max-width: 768px) 100vw, 50vw"
  className="hero"
/>

The sizes attribute was the second lever — without it, Next.js generates the wrong srcset and the browser downloads the 1200px image on a 375px screen.

2. Dynamic imports for below-the-fold widgets

Three heavy components (a charting library, a rich-text renderer, and a carousel) were rendering synchronously as part of the initial bundle, adding ~180kb to the critical path.

const Chart = dynamic(() => import('@/components/Chart'), { ssr: false })
const RichText = dynamic(() => import('@/components/RichText'))

ssr: false on the chart component removes it from the SSR output entirely — it doesn't contribute to LCP at all, and any hydration cost is deferred until after first paint.

3. ISR tuning — why 60s revalidate

The page was fully SSR on every request. For content that changes at most a few times per day, this burns server CPU and adds per-request latency. Switching to ISR with revalidate: 60:

60s was chosen by looking at the content update frequency in the CMS — editors publish at most 3–4 times per day, so even a 300s revalidation would be fine. 60s was the conservative choice.

4. Font loading

Fonts were loaded via a standard @import in the global CSS file — a well-known render-blocker. Migrated to next/font/google with display: 'swap', which:

Outcome

MetricBeforeAfter
LCP (mobile)3.8s1.2s
LCP categoryNeeds ImprovementGood
Improvement−68%

Real-user CrUX data confirmed the improvement within one week of deployment. The page moved from the "Needs Improvement" band to Google's "Good" threshold, which has measurable downstream effects on organic search ranking.

The investigation also surfaced two additional CLS issues (layout shifts from late-loading images without explicit dimensions) that were fixed as part of the same pass.

← All case studies