Files
xamxam/docs/archive/css-split-analysis.md

8.6 KiB

CSS split / unusedSymbols — Template → Page-type inventory

Status: analysis (gates define-per-page-type-css-bundle and the unusedSymbols content corpus)

How CSS is loaded today (head.php)

App::render() (app/src/App.php) always includes app/templates/head.php for a full page render. head.php unconditionally emits:

<link rel="stylesheet" href="/assets/dist/base.min.css">

base.min.css = the whole style.css @import chain: reset → colors → typography → base → all components/* → utilities. So every full page ships the entire component+utility set, even pages that barely use it (the cross-page waste the split targets).

On top of it, head.php renders $extraCss array entries (admin prepends admin.min.css via $extraCssAdmin). Page-specific .min.css bundles load as $extraCss.

Fragments / HTMX partials (app/templates/partials/*, app/public/**/fragments) are included directly, not via App::render(), so they do NOT load head.php.

Page-type map (entry → templates → extra css → bodyClass)

Public (non-admin)

Page type Template(s) extraCss (on top of base) bodyClass Notes
home public/home.php public.min.css home-body HomeController
tfe public/tfe.php tfe.min.css tfe-body TfeController
repertoire/search public/repertoire.php, public/search.php repertoire.min.css search-body SearchController
content-page (about / licence / charte) public/about.php, public/licence.php, public/charte.php content-page.min.css apropos-body AboutController, LicenceController, CharteController
not-found public/not-found.php not-found.min.css page-not-found Dispatcher
partage (student form / recap) partage/form-page.php (templates/), partage/recapitulatif.php, partage/retry-email.php, partage/index.php form.min.css (+ filepond) — FormBootstrap injects extraCss = [form.min.css]; partage/index sets $filepondBase

Admin

head.php prepends admin.min.css ($extraCssAdmin) whenever $isAdmin is set.

Page type Entry (app/public/admin/*.php) extra extraCss / extraCssAdmin bodyClass
login / reset login.php, password-reset.php, request-reset.php — admin-body
index index.php filepond*.css (extraCssAdmin) admin-body
recapitulatif recapitulatif.php — admin-body student-body (student mode)
add / edit add.php, edit.php, contenus-edit.php form deps admin-body
contenu mgmt contenus.php, tags.php, acces*.php, cleanup.php, account.php, etc. — admin-body
parametres parametres.php system.min.css (extraCssAdmin) admin-body

Dist bundles present (app/public/assets/dist/)

From scripts/build-css.mjs: base.min.css, admin.min.css, form.min.css, public.min.css, tfe.min.css, repertoire.min.css, content-page.min.css, not-found.min.css, system.min.css, file-access.min.css, common.min.css.

Shared core (potential overlap between bundles)

base.min.css already contains everything: reset, colors, typography, base, components/{links,focus,forms,tables,dialog,details,media,buttons,badges,toast, pagination,header,search,toc}, utilities.

The split must decide which of those move OUT of the global base into page-type bundles; the components used on every page (reset, colors, typography, base, header, footer, search? — see component usage below) stay in a slim base.

Next step

Component-by-component usage audit: which templates actually reference each components/* class / utility. That drives define-per-page-type-css-bundle and feeds the unusedSymbols content corpus.

Content corpus (for unusedSymbols report)

scripts/css-content-sources.mjs builds the content corpus scanned against the CSS symbols:

  • Sources: app/templates/.php, app/public/.php, app/src/.php (controllers + icon.php helper), app/public/assets/js/app/.js (first-party).
  • Vendor JS excluded (htmx/filepond/pdf): their class names are internal to their own bundled CSS, and including them would only under-report project usage — the safe direction.
  • Dynamic-class handling: buildCorpus() returns a safelist of 22 exact runtime class names (status-ok/warn/err/unknown, log-, input-error, active, disabled, btn--, fhb-, admin-icon-btn--, status-published/pending/badge) plus 5 prefix patterns (status-access--, toc-level-, admin-import-log__item--*, admin-body, student-body) whose suffix is DB/state-derived.

Run: node scripts/css-content-sources.mjs (prints inventory). The report script (scripts/css-unused-report.mjs, task 12) imports buildCorpus().

unusedSymbols report — findings (diagnostic, task 12/14 go-no-go)

just css-report rebuilds CSS then runs scripts/css-unused-report.mjs: for each dist/*.min.css it extracts class/id symbols, checks them against the content corpus + dynamic/vendor safelists, and measures bytes lightningcss would reclaim (no stripping applied).

Result (216,383 B total): ~6.2 KB (2.9%) reclaimable. Per bundle:

bundle orig B reclaim B candidate-unused
admin.min.css 55016 3060 admin-import-results*, n-grid, n-section, param-, admin-toggle, admin-dialog--sheet, admin-maintenance-*
base.min.css 21375 484 btn--success, btn--blue, btn--yellow
form.min.css 41855 731 mode-toggle, licence-generalites, file-preview-list
partage-form.min.css 37229 731 (same)
form-base.min.css 19110 751 (same)
public.min.css 4047 228 card__media--placeholder
system.min.css 7408 185 sys-status-section
content-page.min.css 3683 32 heading-permalink

Vendor classes excluded (would over-report + reclaim): filepond--, htmx-. They are assembled by vendor JS at runtime, so they never appear as literals.

See TODO task 14 for the go/no-go evaluation of this data.

Decision (task 14): per-page split and CSS pruning

Data: ~6.2 KB (2.9%) reclaimable across 216 KB total. base.min.css itself only 484 B (2.3%) reclaimable. Waste is concentrated in admin/form/partage-form (shared FilePond + form CSS).

Diagnosis: base.css is already well-used; the per-page SPLIT would shave minimal real weight (base.min.css is ~all-referenced). The unused-symbol PRUNING opportunity is small and concentrated in bundles that are already page-type- specific (form/admin).

Split — NO-GO

Splitting base.min.css into per-page bundles was premised on cross-page waste of component+utility CSS. The report shows that premise is weak: nearly all of base.min.css's classes are referenced somewhere, and shaving a redundant handful is a pruning task, not a split. A split adds build/template complexity for ~2% of one bundle. → Do not split. Park tasks u-w-x-y as deferred.

Pruning — CONDITIONAL-GO (narrow, safe subset only)

The only pruning worth doing is hand-verifiable dead selectors, removed from SOURCE css files (never from dist) so the next build drops them:

  • base.min.css: btn--success, btn--blue, btn--yellow (3, 484 B)
  • public.min.css: card__media--placeholder (228 B)
  • system.min.css: sys-status-section (185 B)
  • content-page.min.css: heading-permalink (32 B)
  • form*.min.css: mode-toggle, mode-toggle--back, licence-generalites, file-preview-list (731 B, duplicated across form-base/form/partage-form)
  • admin.min.css: admin-import-results*, n-grid, n-section, param-, admin-toggle, admin-dialog--sheet, admin-maintenance-* (3060 B)

Safety rules:

  1. Delete from app/public/assets/css/** sources, never dist.
  2. Rebuild + re-run just css-report after each edit; the class must vanish from the candidate list (not just move to needs-review).
  3. Never remove a needs-review (dynamic emitter) or vendor-prefix class (filepond--, htmx-).
  4. btn--success/blue/yellow are safe (no live $-built variants); confirm they are not emitted by a helper before deleting.
  5. Green path: just build-css + smoke admin/login/form/public pages after.

Verdict: park the split (NO-GO); optionally prune the ~6.2 KB dead set as a follow-up, lowest-risk slice first (base/public/system/content-page).