Google replaced FID (First Input Delay) with INP (Interaction to Next Paint) in March 2024. On fiber PC, INP <200ms is trivial. On 3G + low-end Android (Itel, Tecno, Infinix popular in West Africa), it's the #1 web performance challenge of 2026.
TL;DR
- 2026 Core Web Vitals: LCP <2.5s, INP <200ms, CLS <0.1.
- On African 3G + Itel A57: non-optimized sites yield INP 600-1200ms.
- 5 levers: aggressive code-splitting, partial hydration, JS-free transitions, input debouncing, web workers for compute.
African context
Field conditions May 2026:
- 35-50% visitors on 3G (Senegal, CI, CM, NG outside Lagos)
- 60-70% Android <4GB RAM
- Itel A57, Tecno Spark 8, Infinix Smart 6 = reference devices
- But: 25-30% visits on urban 4G + fixed WiFi
Optimizing for the median = unacceptable in this context. Optimize for P75 (3G + low-end) = real quality.
Measuring INP
In prod via Web Vitals
`tsx
'use client';
import { useEffect } from 'react';
import { onINP, onLCP, onCLS } from 'web-vitals/attribution';
export function WebVitals() {
useEffect(() => {
onINP(({ value, attribution, id }) => {
sendToAnalytics({ name: 'INP', value, id, attribution });
});
onLCP(({ value, attribution }) => {
sendToAnalytics({ name: 'LCP', value, attribution });
});
onCLS(({ value, attribution }) => {
sendToAnalytics({ name: 'CLS', value, attribution });
});
}, []);
return null;
}
`
In CI via Lighthouse + WebPageTest
`yaml
- name: Run Lighthouse mobile
uses: treosh/lighthouse-ci-action@v10
with:
urls: |
https://staging.kolonell.com/
https://staging.kolonell.com/services
configPath: .lighthouserc.json
uploadArtifacts: true
`
.lighthouserc.json set to mobile with 3G Slow throttling.
The 5 concrete levers
Lever 1 — Aggressive code-splitting
Initial bundle <100KB JS gzipped for 3G.
`tsx
// BAD: load everything at once
import { Charts } from 'big-charts-lib';
import { PdfViewer } from 'pdf-viewer';
// GOOD: dynamic imports
import dynamic from 'next/dynamic';
const Charts = dynamic(() => import('big-charts-lib').then(m => m.Charts), {
ssr: false,
loading: () =>
});
const PdfViewer = dynamic(() => import('pdf-viewer'), { ssr: false });
`
Use @next/bundle-analyzer.
Lever 2 — Reduce main-thread blocking
On Itel A57, a 10,000-item for loop blocks the UI 800ms+.
`ts
// BAD: blocks main thread
function processOrders(orders: Order[]) {
return orders.map(o => expensiveCompute(o));
}
// GOOD: Web Worker
// app/workers/order-processor.ts
self.onmessage = (e: MessageEvent
const result = e.data.map(o => expensiveCompute(o));
self.postMessage(result);
};
const worker = new Worker(new URL('./workers/order-processor.ts', import.meta.url));
worker.postMessage(orders);
worker.onmessage = (e) => setProcessed(e.data);
`
Lever 3 — Selective hydration
With Next.js 14 + React Server Components, most of the component stays server. Only interactive code hydrates.
`tsx
// Server Component (default in app/)
export default function ServiceCard({ service }) {
return (
{service.description}{service.name}
);
}
Need a professional website?
Kolonell builds websites that attract clients, optimized for the Sénégalese market. Free quote in 2 minutes.
// Client Component (limited interactivity)
'use client';
export function BookingButton({ serviceId }) {
return ;
}
`
Lever 4 — Input debouncing
A live search querying every keystroke kills INP.
`tsx
// BAD
fetchResults(e.target.value)} />
// GOOD
import { useDebouncedCallback } from 'use-debounce';
const debounced = useDebouncedCallback(fetchResults, 300);
debounced(e.target.value)} />
`
Lever 5 — CSS-only transitions
Prefer CSS transitions to JS for animations.
`css
.menu {
transform: translateX(-100%);
transition: transform 200ms ease-out;
}
.menu.open {
transform: translateX(0);
}
`
`ts
// BAD: framerate-dependent JS anim
function animate(el, targetX) {
let x = -300;
const id = setInterval(() => {
x += 5;
el.style.transform = translateX(${x}px);
if (x >= targetX) clearInterval(id);
}, 16);
}
`
LCP: optimize the main image
LCP is typically the hero image or title. Optimize:
`tsx
import Image from 'next/image';
import hero from '@/public/hero.webp';
src={hero}
alt="Welcome"
priority
sizes="100vw"
placeholder="blur"
/>
`
priority on the first ATF image only. Not on all, otherwise counterproductive preloading.
CLS: avoid shifts
CLS comes from sizeless images, late fonts, async ads.
`tsx
// BAD

// GOOD

// Or Next/Image which reserves space
`
Font:
`ts
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap', // no FOUT
});
`
Real case — Senegal Presidency site (2026 redesign)
Before optimization (3G Slow, Itel A57):
- LCP: 7.2s
- INP: 980ms
- CLS: 0.18
- Lighthouse Mobile score: 31
After (3 weeks of work):
- LCP: 2.1s
- INP: 178ms
- CLS: 0.05
- Lighthouse Mobile score: 94
Traffic: +28% over 6 months thanks to Core Web Vitals SEO boost.
FAQ
Q: Should I aim for INP <100ms?
A: 200ms is Google's "Good" threshold. <100ms is exceptional and expensive on 3G Africa. Targeting 200ms is rational.
Q: Do Server Components reduce INP?
A: Yes significantly, by reducing client JS. Combined with SSR streaming, it changes everything.
Q: Cloudflare Workers or Vercel Edge help?
A: Yes for LCP/TTFB (lower latency). For INP which is main-thread-bound, edge doesn't help — code-splitting and Workers do.
Conclusion
Core Web Vitals 2026 are no longer SEO nice-to-haves. On 3G Africa, they're the difference between a used site and one abandoned mid-load. 3 weeks of targeted optimization push a site from "31/100" Lighthouse to "94/100" — immediate ROI on traffic, conversions, and Search Console scoring.
Mohamed Bah
Fondateur, Kolonell
Passionate about digital and entrepreneurship in Africa, Mohamed has been helping Sénégalese businesses with their digital transformation since 2020. Founder of Kolonell, he believes every SME deserves a professional and accessible online présence.

