Core Web Vitals uden panik (12 små hacks jeg ville ønske jeg kendte før)
“Performance score: 32.”
Det stod der i Lighthouse, første gang jeg kørte testen på et ellers ganske uskyldigt lille Vite-projekt en sen aften ved spisebordet. Ingen kæmpe framework, ingen tusind komponenter. Bare et lille hobby-dashboard. Alligevel føltes det som om jeg havde bygget siden i bly.
Jeg gjorde det, mange gør: lukkede rapporten, sukkede og tænkte “det må være noget avanceret, jeg ikke forstår endnu”. Det var det ikke. Det var mest billeder, fonts og lidt for meget JavaScript.
I dag griber jeg det helt anderledes an. Og det er faktisk ret begyndervenligt, hvis man tager det i små bidder.
1. Start altid med en måling, ikke en mavefornemmelse
Jeg starter altid samme sted nu: i Chrome DevTools med Lighthouse.
Sådan kører du en hurtig Lighthouse-rapport
Åbn din side i Chrome. Højreklik og vælg “Inspicer”. Gå til fanen “Lighthouse”.
Her gør jeg typisk:
- Vælger “Mobile” (mobil afslører problemer hurtigst)
- Sætter kun flueben i “Performance” for at spare tid
- Klikker “Analyze page load” og venter 10-20 sekunder
Så får du en score og nogle grafer. Det er fristende at stirre sig blind på tallet, men til Core Web Vitals kigger jeg på tre ting:
- Largest Contentful Paint (LCP) – hvor hurtigt den største synlige ting loader
- Cumulative Layout Shift (CLS) – hvor meget layoutet hopper rundt
- Interaction to Next Paint (INP) – hvor hurtigt siden reagerer på klik/tastetryk
Du finder dem under “Metrics” og nogle gange i toppen som små badges. Jeg skriver dem ned i en lille tekstfil: f.eks. LCP: 3.8s, CLS: 0.21, INP: 320ms. Så kan jeg se, om mine ændringer faktisk hjælper.
2. LCP, CLS og INP forklaret uden lærebog
Jeg plejede at tænke: “Så længe siden ser hurtig ud på min maskine, er det fint.” Det var før jeg forstod, hvad de tre tal egentlig repræsenterer.
LCP: Den største ting foldet ud
LCP er bare svaret på: “Hvornår er det vigtigste indhold synligt?” Typisk:
- et stort hero-billede
- en kæmpe overskrift
- en stor kort-komponent eller lignende
Hvis det først dukker op efter 4-5 sekunder, føles siden sløv, uanset hvor meget spinner-animation du viser.
CLS: Den irriterende hoppe-linje
CLS er det, du mærker, når du er ved at klikke på en knap, og hele siden hopper, fordi et billede eller en annonce pludselig loader. Det føles billigt og ufærdigt.
INP: Hvor lang tid føles et klik
INP handler om, hvor hurtigt siden reagerer, når du gør noget. Klikker du på en knap, og der går et halvt sekund, før noget sker? Så er din INP sandsynligvis dårlig.
Jeg tænker på det som tre spørgsmål:
- LCP: Hvor hurtigt ser siden “rigtig” ud?
- CLS: Holder tingene op med at hoppe rundt?
- INP: Føles det responsivt, når jeg bruger det?
3. Tre hurtige LCP-gevinster du kan teste i Lighthouse
Mit første projekt tabte næsten al LCP-score på et gigantisk hero-billede. Jeg gjorde alt det forkerte: fuld opløsning, ingen komprimering, og jeg lod CSS skalere det ned.
Win 1: Brug et mindre, moderne billedformat
Før havde jeg noget i den her stil:
<img src="/images/hero-large.jpg" alt="Dashboard" />
Billedet var 4000px bredt. Min skærm er ikke.
Efter skiftede jeg til WebP og en fornuftig bredde, f.eks. 1600px, og gav browseren et hint:
<img
src="/images/hero-1600.webp"
alt="Dashboard"
width="1600"
height="900"
loading="lazy"
/>
Ja, loading="lazy" er mere en LCP-gevinst for ting nederst på siden, men det hjælper generelt. Det vigtigste her er: mindre fil, fornuftigt format.
Vil du nørde billedstørrelser, har jeg også skrevet en artikel om at optimere billeder til web, men du kan nå langt bare med WebP og realistiske dimensioner.
Win 2: Sørg for at dit hero-billede ligger i HTML, ikke bliver hentet via JavaScript
En typisk rookie-fejl i SPA-projekter er at lade JavaScript kontrollere alt, også det helt statiske.
Før brugte jeg et React-component som først blev loaded efter bundlen:
// Hero.tsx
export function Hero() {
return <img src="/images/hero-1600.webp" alt="Dashboard" />
}
Og så blev hele appen mounted på et tomt <div id="root">. Browseren kan ikke begynde at hente billedet, før JavaScript er downloaded og kørt.
En bedre løsning i små projekter er at lade det vigtigste indhold ligge direkte i HTML’en serveret af hosten. Hvis du bruger Vite med en enkel entry, kan du f.eks. have en server-renderet side eller en statisk HTML med dit hero-billede, så browseren begynder at hente det med det samme.
Win 3: Gør dine webfonts mindre irriterende
Jeg har flere gange ødelagt LCP med én ting: tunge Google Fonts uden fallback.
Før:
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" />
Efter jeg forstod, hvad der foregik, gik jeg over til:
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" />
Og i CSS:
body {
font-family: "Roboto", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
}
display=swap gør, at siden viser fallback-fonten med det samme og skifter, når fonten er hentet. Det forbedrer både oplevelsen og ofte LCP.
4. Tre nemme CLS-fix: stop hoppe-layoutet
CLS tør jeg godt indrømme, at jeg ignorerede længe. “Det er da fint, at ting flytter sig lidt”, tænkte jeg. Indtil jeg prøvede min egen side på mobilen og ramte den forkerte knap tre gange.
Win 4: Sæt bredde og højde på billeder
Den klassiske synder er billeder uden dimensioner.
Før:
<img src="/avatars/user1.webp" alt="Bruger" />
Efter:
<img
src="/avatars/user1.webp"
alt="Bruger"
width="80"
height="80"
loading="lazy"
/>
Bare det at sætte width/height giver browseren pladsen at arbejde med, så layoutet ikke hopper, når billedet endelig er hentet.
Win 5: Undgå at indsætte ting ovenfor eksisterende indhold
En fejl jeg lavede i et lille dashboard: Jeg havde en notifikation-bar der først blev vist efter et async fetch.
Før (pseudo):
// Efter data er hentet
setShowBanner(true)
return (
<main>
{showBanner && <Banner />}
<Dashboard />
</main>
)
Når banneret blev vist, skubbede det hele dashboardet ned. CLS gik i vejret.
Efter flyttede jeg banneret under headeren og gav det en reserveret højde fra start, f.eks. via CSS:
.banner-placeholder {
min-height: 48px;
}
Og i JSX:
<header>...</header>
<div className="banner-placeholder">
{showBanner && <Banner />}
</div>
<main>
<Dashboard />
</main>
Nu hopper resten ikke.
Win 6: Vær forsigtig med fonts og FOUT/FOIT
Nogle gange kommer CLS faktisk fra fonts, ikke fra billeder. Hvis din tekst først vises med én font og så skifter til en bredere/smalere font, kan det flytte linjer.
Her hjælper:
- system-fonts (ingen ekstern fetch)
font-display: swapi selvhostede fonts- ikke at loade 5 forskellige font-familier og 10 vægte
Jeg er endt med i mange små projekter bare at bruge system-ui. Det ser fint ud og er lynhurtigt. Ikke fancy, men behageligt.
5. Tre simple greb til bedre INP i små projekter
INP fik jeg først øje på, da en knap i min lille SPA føltes sløv, selv om alt “virkede”. Lighthouse brokkede sig, og jeg opdagede, hvor meget en enkelt tung funktion kan trække oplevelsen ned.
Win 7: Flyt tunge beregninger væk fra klik-øjeblikket
Jeg havde noget i den her stil:
button.addEventListener("click", () => {
const result = heavyCalculation(data)
renderResult(result)
})
Når du laver alt arbejdet inde i click-handleren, blokerer du UI-tråden. Et lille delay føles hurtigt tungt.
En bedre strategi er at:
- forberede data tidligere (f.eks. efter fetch)
- dele arbejdet op i mindre bidder
- vise et lille “beregner”-state hurtigt og lave tunge ting en anelse bagefter
Ikke alt kan magisk optimeres, men bare at flytte store loops væk fra click-handleren hjælper.
Win 8: Debounce input-events
Et typisk mønster: du kalder API for hvert tastetryk i et søgefelt.
Før:
input.addEventListener("input", (e) => {
fetch(`/api/search?q=${e.target.value}`)
})
Efter med simple debounce:
let timeoutId
input.addEventListener("input", (e) => {
clearTimeout(timeoutId)
const value = e.target.value
timeoutId = setTimeout(() => {
fetch(`/api/search?q=${value}`)
}, 300)
})
Nu reagerer UI stadig med det samme (du skriver frit), men netværks-arbejdet sker sjældnere. Mindre pres på browseren, bedre INP.
Win 9: Ryd op i unødvendige event listeners
I et af mine projekter havde jeg bundet scroll-events flere steder uden at tænke over det. Det kan hurtigt blive til meget arbejde for browseren.
Jeg gør nu det, at jeg:
- samler scroll-handling ét sted
- bruger
passive: truepå scroll/ touch, når muligt - fjerner lyttere igen, når komponenter unmountes
Det er små ting, men især på mobil kan det mærkes.
6. Vite-specifikke ting jeg ofte glemmer (og som koster score)
Jeg bruger Vite til næsten alle mine små projekter. Det er hurtigt i dev, men jeg glemte længe at tjekke, hvad jeg egentlig sendte i produktion.
Win 10: Kør en rigtig production build og tjek størrelsen
I Vite er første skridt altid:
npm run build
npm run preview
Så tester du den version, brugerne faktisk ser. Når du bygger, får du også et output om bundle-størrelser.
Hvis din index.js pludselig er på 800 kB, er det et hint. Gå koden igennem og kig efter:
- hele UI-biblioteker for én knaps skyld
- store ikonsæt hvor du kun bruger 2 ikoner
- moment.js eller lignende tunge libs, hvor du kunne klare dig med noget simplere
Lazy loading af sjældne views
I en lille SPA med Vite og React havde jeg en admin-side, som kun jeg selv brugte. Alligevel blev den loaded for alle.
Før:
import { AdminPage } from "./AdminPage";
<Routes>
<Route path="/admin" element={<AdminPage />} />
</Routes>
Efter med lazy loading:
import { lazy, Suspense } from "react";
const AdminPage = lazy(() => import("./AdminPage"))
<Routes>
<Route
path="/admin"
element={
<Suspense fallback={<p>Loader...</p>}>
<AdminPage />
</Suspense>
}
/>
</Routes>
Så kommer admin-koden først ned, når nogen faktisk besøger /admin. Det hjælper både LCP og INP på forsiden.
7. Billederne: den nemmeste performance-gevinst du kan få
Hvis jeg kun måtte optimere én ting i et begynderprojekt, ville jeg tage billederne. De er næsten altid for store.
Basis-regler jeg selv følger
- Eksporter til WebP eller AVIF, hvis muligt
- Brug realistiske størrelser (ingen 4000px brede helskærmsbilleder)
- Brug
loading="lazy"på alle billeder under folden
Et simpelt eksempel:
<img
src="/images/card-400.webp"
alt="Projekt"
width="400"
height="250"
loading="lazy"
/>
Vil du være ekstra flittig, kan du bruge srcset og sizes, så mobilen får en mindre version, men for de fleste små projekter er det vigtigste bare at slippe for de helt groteske filstørrelser.
8. Caching basics på simple hosts
Her bliver mange nervøse, fordi “cache headers” lyder som noget, man skal være DevOps for at røre ved. I praksis er det ret simpelt på de fleste statiske hosts.
På Netlify, Vercel og lignende sættes der ofte fornuftige defaults, især for hashed assets (filer med .hash.js i navnet). De kan typisk caches længe.
Det jeg kigger efter, er typisk:
- at
index.htmlikke caches for længe (så deploys slår igennem) - at bundler og billeder med hash får lang cache
Nogle hosts kan styres via en _headers-fil eller lignende. Et simpelt eksempel kunne være:
/*
Cache-Control: public, max-age=0, must-revalidate
/assets/*
Cache-Control: public, max-age=31536000, immutable
Så siger du: HTML skal altid tjekkes igen, men assets (med hash i filnavnet) kan ligge længe i cachen.
Hvis du er helt ny i det her, starter jeg ofte bare med standard-opsætningen på f.eks. Netlify og tjekker via DevTools > Network, hvilke cache-headers der kommer retur. Så kan du bygge videre derfra, når du er tryg.
9. Mini-tjek før du viser dit projekt frem i en ansøgning
Hvis du bygger noget til din portfolio, er Core Web Vitals faktisk en ret fin måde at skille sig lidt ud på. Du behøver ikke 100 i score, men du kan vise, at du har tænkt over performance.
Min egen lille tjekliste
Jeg kører typisk det her igennem, inden jeg sender linket til nogen:
- Kør Lighthouse på mobil, skriv LCP, CLS og INP ned
- Tjek startsiden i DevTools > Network: hvor stor er total download?
- Åbn siden på min egen mobil på 4G og mærk, om den føles “tung”
- Tjek hurtigt om billeder har width/height og loading=”lazy”
- Sørg for at jeg bruger
npm run buildog ikke dev-serveren til deploy
Hvis du vil gå dybere, ligger der også andre artikler på Coding Class om f.eks. SPA vs server routing og om at forstå bundler og deploy, som spiller fint sammen med det her.
10. En sidste bemærkning om score-jagt
Jeg jagtede selv 100/100 længe. Det er meget tilfredsstillende, når cirklen bliver grøn. Men i små projekter er det efter min mening vigtigere at forstå, hvad der trækker ned, end at få alle tal perfekte.
Hvis du kan forklare: “Min LCP er høj, fordi jeg loader et stort hero-billede, og jeg har valgt at prioritere kvalitet her” eller “min INP lider lidt på den her side, fordi jeg laver tunge beregninger, som jeg endnu ikke har fået flyttet ud”, så er du allerede foran rigtig mange.
Og ja, jeg mener stadig, at en portfolio-side med en stabil 80’er-score og tydelig forståelse af Core Web Vitals er mere imponerende end en kunstigt optimeret 100’er uden at vide hvorfor. Selv om det sikkert ikke er det, LinkedIn gerne vil høre.









1 kommentar