Remove Unused JS/CSS on Static Sites to Cut CLS and TBT
Purged 142KB CSS and 89KB JS from a VitePress docs site. CLS went from 0.19 to 0.02. Step-by-step with coverage tools.
Site: VitePress tutorial site, default theme + custom CSS + Prism syntax theme. Lighthouse flags: 89KB unused JS, 142KB unused CSS, CLS 0.19 from late-loading stylesheet.
Find what's unused
Chrome DevTools → Coverage tab → reload. Sort by unused bytes.
My top offenders:
vitepress/theme-default/styles/vars.css— 60% unused (dark mode vars I don't use)shikitheme bundle — loaded two themes, used one- Dead
client:idlehydration on components never in viewport
CSS: split critical from deferred
/* critical.css — inlined in <head> */
:root { --vp-c-bg: #fff; --vp-font-family-base: 'Inter', sans-serif; }
.VPContent { max-width: 960px; margin: 0 auto; }
.ad-slot { min-height: 250px; }
<link rel="preload" href="/assets/deferred.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/assets/deferred.css"></noscript>
Late CSS injection causes FOUC and CLS if it changes layout. Reserve ad slots in critical CSS.
VitePress: trim Shiki
.vitepress/config.ts:
export default defineConfig({
markdown: {
theme: { light: 'github-light', dark: 'github-light' }, // single theme
},
vite: {
build: {
cssCodeSplit: true,
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('shiki')) return 'shiki';
},
},
},
},
},
});
Disabling dark mode removed 40KB CSS variables.
Purge unused Tailwind / custom CSS
If using Tailwind:
// tailwind.config.js
content: ['./.vitepress/**/*.{vue,ts,js}', './**/*.md'],
Custom CSS — manual audit:
npx purgecss --css dist/assets/*.css --content dist/**/*.html --output dist/purged/
Review diff before shipping — purge can be aggressive.
JS: audit imports
Bad:
import _ from 'lodash'; // entire library for .debounce
Good:
import debounce from 'lodash/debounce';
VitePress search: if you use Algolia DocSearch, remove local fuse.js entirely.
Hugo-specific purge
{{ $css := resources.Match "css/**/*.css" | resources.Concat "bundle.css" | resources.Minify | resources.Fingerprint }}
Don't concat theme CSS you don't need — override head.html and drop unused <link> tags.
CLS connection
Unused CSS often includes rules that override ad slot dimensions after paint. When I purged a .content img { max-width: 100%; height: auto } duplicate, ad containers stopped resizing.
Fixed with:
.ad-slot ins { display: block !important; min-height: inherit; }
Before / after
CSS transfer: 186KB → 44KB
JS transfer: 124KB → 35KB
TBT: 420ms → 95ms
CLS: 0.19 → 0.02
Performance: 71 → 96
Ongoing: CI guard
# fail build if main chunk > 50KB
MAIN_SIZE=$(stat -f%z dist/assets/index*.js)
[ "$MAIN_SIZE" -gt 51200 ] && exit 1
Static sites shouldn't ship SPA-sized bundles. Treat every KB as an LCP tax.