Waarom performance ertoe doet voor je SaaS
Snelheid is geen luxe — het is een concurrentievoordeel. Google's onderzoek toont aan dat 53% van mobiele gebruikers een pagina verlaat als deze langer dan 3 seconden laadt. Voor SaaS-producten is de impact nog groter: trage applicaties leiden tot hogere churn, lagere conversies en frustratie bij je gebruikers.
Core Web Vitals — Google's set van metrics die echte gebruikerservaring meten — zijn niet alleen belangrijk voor SEO. Ze geven je een objectief beeld van hoe je applicatie presteert in de echte wereld.
In dit artikel duiken we diep in de drie Core Web Vitals, laten we zien hoe je ze meet in je SaaS, en geven we concrete optimalisaties die je vandaag nog kunt toepassen.
De drie Core Web Vitals
1. Largest Contentful Paint (LCP)
LCP meet hoe lang het duurt voordat het grootste zichtbare element op de pagina is gerenderd. Dit is typisch een hero-image, een grote heading, of een data-tabel.
Doel: < 2,5 seconden
Veelvoorkomende problemen in SaaS:
- Zware dashboards die veel data ophalen bij de eerste load
- Ongeoptimaliseerde afbeeldingen in marketing-pagina's
- Render-blocking JavaScript bundles
Oplossingen:
// ❌ Slecht: alles laden bij mount
useEffect(() => {
const data = await fetch('/api/dashboard/all-widgets');
setWidgets(data);
}, []);
// ✅ Beter: kritieke data eerst, rest lazy
useEffect(() => {
// Laad alleen de bovenste widget direct
const hero = await fetch('/api/dashboard/hero-stats');
setHeroStats(hero);
// Rest pas na interactie of met IntersectionObserver
}, []);
Server-side optimalisaties:
// Next.js: gebruik streaming SSR voor snellere TTFB
// app/dashboard/page.tsx
import { Suspense } from 'react';
import { HeroStats } from './hero-stats';
import { WidgetGrid } from './widget-grid';
export default function Dashboard() {
return (
<div>
{/* Kritiek pad - direct gerenderd */}
<Suspense fallback={<HeroSkeleton />}>
<HeroStats />
</Suspense>
{/* Secundair - streamed later */}
<Suspense fallback={<WidgetSkeleton />}>
<WidgetGrid />
</Suspense>
</div>
);
}
2. Interaction to Next Paint (INP)
INP vervangt sinds maart 2024 de oude First Input Delay (FID) metric. Het meet de responsiviteit van je hele pagina — niet alleen de eerste interactie, maar álle interacties gedurende het bezoek.
Doel: < 200 milliseconden
Veelvoorkomende problemen in SaaS:
- Zware client-side filtering/sorting van grote datasets
- Complexe formuliervalidatie die de main thread blokkeert
- Te veel re-renders in React/Vue componenten
Oplossingen:
// ❌ Slecht: filteren op de main thread
function FilterableTable({ data }: { data: Row[] }) {
const [filter, setFilter] = useState('');
// Dit blokkeert de main thread bij 10.000+ rijen
const filtered = data.filter(row =>
row.name.toLowerCase().includes(filter.toLowerCase())
);
return <Table data={filtered} />;
}
// ✅ Beter: gebruik useDeferredValue of Web Workers
function FilterableTable({ data }: { data: Row[] }) {
const [filter, setFilter] = useState('');
const deferredFilter = useDeferredValue(filter);
const filtered = useMemo(
() => data.filter(row =>
row.name.toLowerCase().includes(deferredFilter.toLowerCase())
),
[data, deferredFilter]
);
return (
<>
<input
value={filter}
onChange={e => setFilter(e.target.value)}
// Input reageert direct, tabel update deferred
/>
<Table data={filtered} />
</>
);
}
Voor écht zware berekeningen, gebruik een Web Worker:
// worker.ts
self.onmessage = (event) => {
const { data, filter } = event.data;
const result = data.filter(row =>
row.name.toLowerCase().includes(filter.toLowerCase())
);
self.postMessage(result);
};
// component.tsx
const worker = new Worker(new URL('./worker.ts', import.meta.url));
worker.postMessage({ data, filter });
worker.onmessage = (e) => setFiltered(e.data);
3. Cumulative Layout Shift (CLS)
CLS meet hoeveel de layout onverwacht verschuift tijdens het laden. Niets is frustrerender dan op een knop willen klikken die plotseling verspringt.
Doel: < 0,1
Veelvoorkomende problemen in SaaS:
- Dynamische banners of notificaties die bovenaan worden ingevoegd
- Afbeeldingen zonder expliciete afmetingen
- Fonts die laat laden en tekst laten verspringen (FOUT)
- Skeleton loaders met verkeerde afmetingen
Oplossingen:
/* Reserveer ruimte voor dynamische content */
.notification-bar {
min-height: 48px; /* Voorkom shift als banner verschijnt */
}
/* Gebruik aspect-ratio voor afbeeldingen */
.dashboard-chart {
aspect-ratio: 16 / 9;
width: 100%;
}
// Next.js Image component handelt dit automatisch af
import Image from 'next/image';
<Image
src="/chart-placeholder.png"
width={800}
height={450}
alt="Dashboard grafiek"
priority // Voor above-the-fold content
/>
Font-optimalisatie:
// next.config.js / layout.tsx
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap', // Toont fallback font direct, swap bij laden
preload: true,
});
Meten is weten: performance monitoring opzetten
Real User Monitoring (RUM)
De Core Web Vitals in je lokale dev-omgeving zijn zinloos — je moet meten wat echte gebruikers ervaren. Hier zijn drie aanpakken, van simpel tot uitgebreid:
1. Vercel Analytics (als je op Vercel host):
// app/layout.tsx
import { Analytics } from '@vercel/analytics/react';
import { SpeedInsights } from '@vercel/speed-insights/next';
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Analytics />
<SpeedInsights />
</body>
</html>
);
}
2. Custom web-vitals reporting:
// lib/vitals.ts
import { onCLS, onINP, onLCP } from 'web-vitals';
function sendToAnalytics(metric) {
// Stuur naar je eigen endpoint of analytics service
fetch('/api/vitals', {
method: 'POST',
body: JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating, // 'good' | 'needs-improvement' | 'poor'
navigationType: metric.navigationType,
url: window.location.pathname,
}),
});
}
onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);
3. Performance budget in je CI/CD pipeline:
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [pull_request]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: treosh/lighthouse-ci-action@v11
with:
urls: |
http://localhost:3000/
http://localhost:3000/dashboard
budgetPath: ./lighthouse-budget.json
uploadArtifacts: true
// lighthouse-budget.json
[{
"path": "/*",
"timings": [
{ "metric": "largest-contentful-paint", "budget": 2500 },
{ "metric": "interactive", "budget": 3500 }
],
"resourceSizes": [
{ "resourceType": "script", "budget": 300 },
{ "resourceType": "total", "budget": 800 }
]
}]
SaaS-specifieke optimalisaties
Bundle splitting per role
De meeste SaaS-applicaties hebben verschillende gebruikersrollen (admin, gebruiker, viewer). Laad niet de admin-modules voor reguliere gebruikers:
// Lazy load admin-only features
const AdminPanel = lazy(() => import('./admin/AdminPanel'));
const BillingSettings = lazy(() => import('./admin/BillingSettings'));
function AppRouter() {
const { role } = useUser();
return (
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
{role === 'admin' && (
<Route
path="/admin/*"
element={
<Suspense fallback={<Loading />}>
<AdminPanel />
</Suspense>
}
/>
)}
</Routes>
);
}
Database query optimalisatie
Trage API's zijn de #1 oorzaak van slechte LCP in SaaS-dashboards:
// ❌ N+1 probleem
const tenants = await prisma.tenant.findMany();
for (const tenant of tenants) {
tenant.users = await prisma.user.findMany({
where: { tenantId: tenant.id }
});
}
// ✅ Eager loading
const tenants = await prisma.tenant.findMany({
include: {
users: {
select: { id: true, name: true, email: true },
take: 10, // Pagineer in de query, niet in de frontend
},
_count: { select: { users: true } }
},
});
Edge caching voor statische en semi-dynamische content
// Next.js route handler met ISR
// app/api/public/pricing/route.ts
export const revalidate = 3600; // Cache 1 uur
export async function GET() {
const plans = await prisma.plan.findMany({
where: { active: true }
});
return Response.json(plans);
}
// Voor per-tenant data: gebruik stale-while-revalidate
export async function GET(request: Request) {
const tenantId = getTenantFromRequest(request);
return Response.json(data, {
headers: {
'Cache-Control': 'private, s-maxage=60, stale-while-revalidate=300',
'Vary': 'Authorization', // Cache per gebruiker
},
});
}
Een performance-cultuur bouwen
Technische optimalisaties zijn slechts de helft. De andere helft is een cultuur waarin performance een eersteklas burger is:
- Maak performance zichtbaar: hang een dashboard met Core Web Vitals p75 in je Slack/Teams channel
- Stel budgetten in: blokkeer PR's die de bundle size met meer dan 10% verhogen
- Test op echte apparaten: je M3 MacBook Pro is niet representatief voor je gebruikers
- Monitor per pagina: je marketing-site en je dashboard hebben heel verschillende profielen
- Review regelmatig: plan een maandelijks "performance review" moment in
Conclusie
Performance-optimalisatie voor je SaaS is geen eenmalig project — het is een doorlopend proces. Begin met meten (je kunt niet verbeteren wat je niet meet), focus op de drie Core Web Vitals, en implementeer de optimalisaties die het meeste impact hebben voor jouw specifieke applicatie.
De investering betaalt zich dubbel en dwars terug: betere SEO-rankings, hogere conversies, lagere churn, en — misschien het belangrijkst — gebruikers die daadwerkelijk plezier hebben in het gebruiken van je product.
Start vandaag: voeg web-vitals toe aan je applicatie, meet een week lang, en pak de grootste bottleneck aan. Je gebruikers zullen je dankbaar zijn.