Siteprestaties verbeteren door meer CSS te gebruiken
Er zit een instinct ingebakken in modern frontendwerk dat zegt dat de snelste pagina het minste van alles verzendt: minder JavaScript, minder lettertypen, minder verzoeken. Dus toen het technische team van GitHub schreef over het verbeteren van siteprestaties door meer CSS te gebruiken, las het als een tegenstrijdigheid. De oplossing voor een trage, JavaScript-zware interface was niet om de styling uit de app te halen. Het was om er meer van terug te stoppen, als gewone, statische, cachebare stylesheets, en om de JavaScript te verwijderen die alleen bestond om stijlen in de browser te genereren.
Het onderscheid is het hele artikel. 'Meer CSS' gebruiken betekent niet een grotere stylesheet die je kritieke pad opzwelt. Het betekent het verplaatsen van stijlgeneratie uit de runtime naar de build, en vervolgens de browser laten doen waar hij echt uitblinkt: een stylesheet eenmaal parsen, cachen en hergebruiken bij elke navigatie.
De verborgen kosten van runtime-styling
CSS-in-JS-bibliotheken losten echte problemen op. Ze gaven componentauteurs scoping, colocatie en de mogelijkheid om props in stijlen te interpoleren zonder een naamgevingsconventie te verzinnen. De afweging was architectonisch: ze verplaatsten een compile-time-probleem naar de browser.
Stijlgeneratie is JavaScript-werk
Elke server-gerenderde pagina die een runtime CSS-in-JS-bibliotheek gebruikt, vraagt de browser om hetzelfde werk twee keer te doen. De server serialiseert stijlen in <style>-tags, waarna de clientbibliotheek ontwaakt, de componentenboom doorloopt, klassennamen opnieuw hasht en het resultaat reconcilieert. Dat werk gebeurt op de hoofdthread en concurreert met de lay-out- en paint-stappen die je juist probeert te versnellen.
// Runtime CSS-in-JS: this stylesheet does not exist until hydration runs
const Button = styled.button`
background: ${(p) => (p.primary ? "#1f883d" : "transparent")};
border: 1px solid #d0d7de;
padding: 6px 16px;
border-radius: 6px;
font: inherit;
`;
Hetzelfde component uitgedrukt als een build-time-stylesheet verwijdert zowel de runtime-afhankelijkheid als het serialisatiewerk:
/* button.module.css, parsed once, cached forever */
.button {
padding: 6px 16px;
border: 1px solid #d0d7de;
border-radius: 6px;
font: inherit;
background: transparent;
}
.buttonPrimary {
background: #1f883d;
color: #ffffff;
}
import styles from "./button.module.css";
export function Button({ primary, ...props }) {
const className = primary
? `${styles.button} ${styles.buttonPrimary}`
: styles.button;
return <button className={className} {...props} />;
}
Niets hiervan is nieuw. Wat gemakkelijk te onderschatten is, is hoeveel van een bundle een volwassen CSS-in-JS-stack stilletjes in beslag neemt: de bibliotheek zelf, de prop-interpolatie-hulpprogramma's, de themaprovider, de serializer en de dubbele frameworkcode die groeit wanneer elk pakket afhankelijk is van zijn eigen kopie.
Het ligt direct op het kritieke weergavepad
Een <link rel="stylesheet"> blokkeert de weergave, maar blokkeert deze eenmaal, en de browser kan het bestand parsen terwijl de HTML-stream nog binnenkomt. Runtime-CSS werkt anders: de browser kan niet eens beginnen met het opbouwen van de componentstijlen totdat de JavaScript is gedownload, geparsed, gecompileerd en uitgevoerd. Op een middenklasse telefoon is dat een seconde of meer aan hoofdthreadwerk voordat een enkele knop zijn achtergrondkleur krijgt.
Het fragmenteert je cache
Gegenereerde klassennamen veranderen telkens wanneer de bron van een component verandert, wat betekent dat de stylesheet bij bijna elke implementatie ongeldig wordt. Verdeel dat over tientallen route-level-bundles en je krijgt een cache-hitratio die er gezond uitziet in een dashboard, maar zelden wordt geactiveerd bij herhaalde bezoeken. Een statische stylesheet met een content-hash kan daarentegen worden geserveerd met Cache-Control: public, max-age=31536000, immutable en wordt effectief nooit twee keer gedownload.
Het verdubbelt het werk tijdens hydratatie
Hydratatieverschillen tussen server-geïnjecteerde en client-gegenereerde stijlen zijn een klassieke bron van flikkering. De browser tekent één set regels, waarna de clientbibliotheek ze halverwege de interactie vervangt, wat een stijlherberekening en soms een lay-outverschuiving forceert. Statische klassennamen die tijdens de buildtijd zijn berekend, zijn aan beide kanten identiek, dus de eerste weergave is ook de laatste weergave.

Wat GitHub daadwerkelijk heeft veranderd
De migratie kan het best worden begrepen als vijf architectonische zetten in plaats van één refactor. Geen van hen vereist het verlaten van een componentmodel en geen vereist een volledige herschrijving.

Designtokens werden CSS custom properties
Kleur, witruimte, radius en typografieschalen zijn verplaatst naar een globaal :root-blok als custom properties. Componenten verwijzen naar var(--color-bg-default) in plaats van een JavaScript-themaobject te importeren. Dit is wat statische CSS expressief genoeg maakt om dynamische stijlen in de meeste gevallen te vervangen, en het maakt runtime-thema's (donkere modus, hoog contrast) mogelijk met nul JavaScript.
Componentstijlen vooraf gecompileerd
Elk component kreeg een echte stylesheet, uitgestuurd tijdens de buildtijd en gescoped door een hash- of moduleconventie. De uitvoer is gewone CSS die elke browser met parsesnelheid kan parsen.
Kritieke CSS inline in de documentkop
Alleen de regels die nodig zijn voor de eerste viewport worden inline in de HTML geplaatst. Dat is typisch 8–14 KB gezipt, en het betekent dat de eerste weergave nooit wacht op een netwerkretour voor de stylesheet die de zichtbare pagina vormgeeft.
<head>
<!-- Critical: exactly what the first viewport needs, nothing more -->
<style>
/* reset + tokens + header + hero + above-the-fold grid */
</style>
<!-- Everything else: one hashed, immutable, long-cached file -->
<link
rel="stylesheet"
href="/assets/app.8f3a1c2d.css"
media="print"
onload="this.media='all'"
/>
<noscript>
<link rel="stylesheet" href="/assets/app.8f3a1c2d.css" />
</noscript>
</head>
Server-gerenderde markup bevat de definitieve klassennamen
Omdat klassennamen bestaan op buildtijd, is de HTML die de server retourneert al opgemaakt. Er is geen tweede pas, geen reconciliatiestap en geen flits van onopgemaakte inhoud op trage verbindingen.
Cascadelagen vervingen specificiteitshacks
Een gedeeld designsysteem staat of valt met een voorspelbare cascadevolgorde. Het eenmalig, vooraf declareren van de laagvolgorde maakt overschrijvingen deterministisch en verwijdert de prikkel om naar !important of steeds diepere selectors te grijpen.
@layer reset, tokens, base, components, utilities;
@layer components {
.card {
background: var(--color-canvas-default);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-2);
padding: var(--space-4);
}
}
@layer utilities {
.mt-4 { margin-top: var(--space-4); }
}
De cijfers: wat er verandert wanneer je meer CSS gebruikt
Het verplaatsen van stijlgeneratie naar buildtijd verandert de vorm van wat je verzendt, niet alleen het volume. De bytes die overblijven zijn goedkoop: ze comprimeren goed, ze worden gedeeld over elke route en ze worden een jaar lang gecachet.

Het architectonische voordeel verschijnt op vier plaatsen tegelijk: minder JavaScript om te downloaden, minder hoofdthreadwerk voor de eerste weergave, een cachebare stylesheet die wordt gedeeld over navigaties en identieke markup op server en client.
| Approach | JS shipped (gzip) | CSS shipped (gzip) | Runtime style work | Cacheable across deploys |
|---|---|---|---|---|
| Runtime CSS-in-JS | ~180 KB | ~12 KB | High | No |
| Single monolithic stylesheet | ~96 KB | ~88 KB | None | Yes |
| Inline critical + split static sheets | ~78 KB | ~54 KB | None | Yes |
| Utility-first CSS | ~85 KB | ~40 KB | None | Yes |
De snelste stylesheet is degene die de browser eenmaal parst, een jaar lang cached en nooit meer hoeft te genereren.
Een praktisch migratieplan
Je hebt de codebase van GitHub niet nodig om dezelfde redenering toe te passen. De migratie is incrementeel en elke stap is onafhankelijk verzendbaar.

1. Inventariseer wat er daadwerkelijk wordt weergegeven tijdens runtime
Open DevTools, laad een pagina en tel document.styleSheets. Als je tientallen geïnjecteerde <style>-tags met gehashte klassennamen vindt, heb je je doelwit gevonden. Kruisverwijs met het Performance-paneel: zoek naar lange taken tussen DOMContentLoaded en de eerste contentvolle weergave.
2. Extraheer CSS boven de vouw
Begin met het kleinst mogelijke inline-blok. Een betrouwbare heuristiek is om reset, tokens en de regels die door de eerste twee viewporthoogtes worden gebruikt, inline te plaatsen. Alles onder de vouw wordt geladen vanuit de uitgestelde stylesheet.
3. Converteer runtime-componenten naar statische klassen
Werk component voor component, te beginnen met de componenten met het meeste verkeer. Prop-gestuurde varianten worden ofwel modifierklassen ofwel custom properties die op een wrapper worden ingesteld. Houd de component-API identiek, zodat aanroepende locaties niet veranderen.
// Genuinely dynamic values still belong in an inline style, not a generated sheet
<div
className={styles.progressBar}
style={{ "--progress": `${percent}%` }}
role="progressbar"
aria-valuenow={percent}
/>
.progressBar {
width: var(--progress, 0%);
transition: width 200ms ease-out;
}
Dit patroon is het werkpaard van de hele migratie: de structuur van de stijl is statisch en cachebaar, terwijl de waarde dynamisch blijft en niets kost om bij te werken.
4. Stel de laagvolgorde vroeg vast
Declareer de @layer-volgorde bovenaan je invoer-stylesheet voordat je een specificiteitsprobleem hebt, niet erna. Het achteraf aanbrengen van lagen op een verouderde cascade is veel pijnlijker dan ermee beginnen.
5. Hash bestandsnamen en cache onveranderlijkContent hashing plus immutable is wat "meer CSS" omzet in "gratis CSS". De tweede paginaweergave in een sessie zou de HTTP-cache moeten raken voor elke stijlregel op de site.
6. Houd echt dynamische styling dynamisch
Transform-, scrollpositie- en meetgestuurde waarden horen thuis in inline stijlen, requestAnimationFrame of de Web Animations API. Herintroduceer geen runtime-stijlengine om een probleem op te lossen dat CSS-eigen eigenschappen al oplossen.
Hoe de verbetering te meten
Meet dezelfde drie dingen voor en na: paint-timing, lange taken op de hoofdthread en het aantal stylesheets. Een klein PerformanceObserver-script geeft je alle drie zonder een monitoringafhankelijkheid toe te voegen.
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry.name, Math.round(entry.startTime));
}
}).observe({ type: "paint", buffered: true });
new PerformanceObserver((list) => {
const long = list.getEntries().filter((e) => e.duration > 50);
console.log(`${long.length} long tasks after load`);
}).observe({ type: "longtask", buffered: true });
console.log("stylesheets:", document.styleSheets.length);
Verwacht de grootste beweging in time-to-first-paint bij herhaalde bezoeken en bij beperkte mobiele profielen, waar de geëlimineerde JavaScript-parsekosten domineren. Largest Contentful Paint verbetert meestal geleidelijker, release na release.

Valkuilen die het vermijden waard zijn
De aanpak is niet risicovrij en de meeste mislukkingen komen door het te agressief toepassen.

- **Onvolledige kritieke CSS.**Als je het eerste viewport verkeerd inschat, krijg je een zichtbare herschikking wanneer het uitgestelde blad arriveert. Genereer het kritieke blok automatisch op basis van een echte weergave in plaats van handmatig. -**Alles inlinen.**Het inlinen van het volledige stylesheet in elk HTML-antwoord ondermijnt caching en vergroot de documentgrootte. Inline het minimum, stel de rest uit. -**Alles-in-één-bundel instincten.**Het oude advies om alle CSS in één groot bestand te concateneren dateert van vóór HTTP/2. Splitsen per route is nu vaak beter, zolang gedeelde primitieven apart worden gecachet. -Twee systemen mengen. Het te lang naast elkaar laten draaien van runtime CSS-in-JS en statische stylesheets verdubbelt je stijloppervlak en maakt cascade-debuggen ellendig. Stel een migratiedeadline per componentenmap in.
De meest voorkomende niet-technische valkuil is dit behandelen als een big-bang herschrijving. De teams die slagen, converteren een handvol componenten met veel verkeer, meten de delta en gebruiken die gegevens om de volgende batch te rechtvaardigen.
DivMagic gebruiken om echte CSS-architectuur reverse-engineeren
Een ondergewaardeerd onderdeel van deze migratie is het zien hoe volwassen productiesites daadwerkelijk hun stylesheets structureren. Wanneer je een site inspecteert die statische CSS goed levert, kun je in vijf minuten meer leren dan in een uur documentatie lezen: hoe ze de cascade in lagen aanbrengen, hoe ze tokens scopen, hoe ze kritiek van uitgesteld splitsen en hoe ze modifiers benoemen.
DivMagic is gebouwd voor precies die workflow. In plaats van handmatig elementstijlen uit DevTools te kopiëren, regel voor regel, selecteer je elke UI op elke pagina en haal je de resulterende HTML en CSS rechtstreeks in je editor, klaar om aan te passen. Dat is echt nuttig in twee situaties: het prototypen van een component op basis van een referentieontwerp en het bestuderen hoe een high-performance site die je bewondert zijn cascade organiseert, zodat je het patroon in je eigen designsysteem kunt spiegelen.
Het verkort ook het saaie deel van stap drie hierboven. Wanneer je een runtime-component omzet naar statische klassen, verwijdert het hebben van de uiteindelijke berekende stijlen in een bewerkbare vorm, in plaats van als verspreide inline stijlen en geïnjecteerde <style> blokken, het meeste transcriptiewerk.
Belangrijkste conclusies
- Runtime-stijlgeneratie is JavaScript-werk op de hoofdthread dat plaatsvindt na het downloaden van JavaScript, direct op je kritieke weergavepad.
- Statische CSS wordt eenmaal geparseerd, een jaar gecachet bij content-hashing en gedeeld over elke route en elke navigatie.
- Inline alleen de regels die het eerste viewport nodig heeft; stel de rest uit achter een niet-blokkerende
<link>en een lang cachebeleid. - CSS-eigen eigenschappen plus
@layerdekken de overgrote meerderheid van gevallen die voorheen prop-gestuurde stijlinterpolatie vereisten. - Migreer incrementeel, component voor component, en meet p75 paint-timing en lange taken in plaats van gemiddelden.
Meer CSS verzenden klinkt als een achteruitgang totdat je beseft dat je een gecached, eenmalig geparseerd stylesheet inruilt voor megabytes aan JavaScript die het bij elke paginalading opnieuw opbouwt.
De les generaliseert ver buiten GitHub. Elke abstractie die in de browser draait, is een kostenpost die de gebruiker betaalt, op hun apparaat, op hun netwerk, elke keer opnieuw. Het verplaatsen van stijlgeneratie naar buildtijd is een van de meest hefboomrijke ruilen beschikbaar voor een componentgestuurde frontend: je levert een statisch bestand dat de browser al weet te verwerken, en je verwijdert de machine die in de weg stond.
