feat(core): Add only-include-used-components: opt-in trimming of unused DSFR component CSS - #505
Open
kevbarns wants to merge 3 commits into
Open
feat(core): Add only-include-used-components: opt-in trimming of unused DSFR component CSS#505kevbarns wants to merge 3 commits into
kevbarns wants to merge 3 commits into
Conversation
… CSS Opt-in script, modeled after only-include-used-icons, that rebuilds dsfr.css and dsfr.min.css in node_modules (and public/dsfr when applicable) with only the CSS of the DSFR components actually used by the project, plus the core and scheme which are always included. Usage is detected from @codegouvfr/react-dsfr/<Component> imports and from raw fr-* class names found in the sources. Components can also be forced via "react-dsfr"."additionalComponents" in package.json. Any unknown component import falls back to including every component. The stylesheets are rebuilt from the granular files shipped in dsfr/ (core, scheme, component/*, print variants) preserving the upstream cascade order, rewriting relative asset urls and reapplying the Mui compat patch, so no individual CSS rule is ever dropped or rewritten. See codegouvfr#304
Covers import detection, module to DSFR components resolution, raw class name detection and stylesheet generation (cascade order, url rewriting, charset stripping, Mui compat patch, determinism).
A direct import of an unrecognized dsfr/component/<x> stylesheet, and any unknown lowercase-starting react-dsfr module, returned [] instead of undefined. This silently skipped the "include every component" fail-safe and its warning for modules this script does not know about, instead of only affecting genuinely non-component modules.
There was a problem hiding this comment.
Pull request overview
This PR adds a new opt-in CLI script (only-include-used-components) to rebuild DSFR CSS bundles by concatenating only the granular component stylesheets that correspond to components detected as used in the target codebase, reducing unused CSS while keeping DSFR JS runtime class toggles safe.
Changes:
- Add
src/bin/only-include-used-components.tsplus CLI wiring viareact-dsfrand a dedicated bin entry. - Implement component usage detection (react-dsfr imports, raw
fr-*class prefixes, andpackage.jsonescape hatch) and rebuilddsfr.css/dsfr.min.css(and SPA public patch + hash busting). - Add unit tests covering module resolution, detection, URL rewriting, MUI core patching, and CSS generation behavior.
Reviewed changes
Copilot reviewed 5 out of 7 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| test/runtime/scripts/onlyIncludeUsedComponents/resolveModuleIdToDsfrComponents.test.ts | Adds unit tests for moduleId→DSFR component mapping and fail-safe behavior. |
| test/runtime/scripts/onlyIncludeUsedComponents/getReactDsfrImportedModuleIds.test.ts | Adds unit tests for detecting react-dsfr module IDs from source text. |
| test/runtime/scripts/onlyIncludeUsedComponents/generateDsfrCssCode.test.ts | Adds unit tests for CSS reconstruction (order, inclusion/exclusion, URL rewriting, MUI patching). |
| test/runtime/scripts/onlyIncludeUsedComponents/detectDsfrComponentsFromClassNames.test.ts | Adds unit tests for detecting DSFR components via raw fr-* class usage. |
| src/bin/react-dsfr.ts | Wires the new command into the react-dsfr CLI dispatcher. |
| src/bin/only-include-used-components.ts | Implements the new trimming/rebuild script and supporting helpers/constants. |
| package.json | Exposes only-include-used-components as a published bin entry. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add
only-include-used-components: opt-in trimming of unused DSFR component CSSCloses #304 (or at least addresses its main pain point)
Problem
dsfr.min.cssweighs ~600kB raw / ~76kB gzip and is loaded render-blocking, while most apps use a small subset of the DSFR components. On Lighthouse mobile audits this is consistently flagged as the main "reduce unused CSS" offender (~95% unused on our app, La Bonne Alternance).Classic PurgeCSS-style tree shaking is not safe here because the DSFR JS adds classes and attributes at runtime (
data-fr-js-*,fr-collapse--expanded, ...), as discussed in #304.Approach
A new opt-in script,
only-include-used-components, modeled afteronly-include-used-icons(same CLI ergonomics:--projectDir,--silent, same project/public dir discovery, same cache clearing, same idempotence).Instead of purging individual rules, it rebuilds
dsfr/dsfr.cssanddsfr/dsfr.min.cssinnode_modules(andpublic/dsfr/dsfr.min.css+index.htmlhash busting for SPAs) by concatenating the granular stylesheets already shipped in the package (dsfr/core/*,dsfr/scheme/*,dsfr/component/<name>/*, including the print variants):DSFR_COMPONENTS_CASCADE_ORDER, determined empirically from the section order ofdsfr.main.css).url(...)asset paths are rewritten from the granular file location to thedsfr/root, so fonts and icons keep resolving (copy-dsfr-to-publicpicks up the correct asset subset when it runs after this script).:not([class^="Mui"])onbutton:not(:disabled):hover/active, cfscripts/build/patchCssForMui.ts) is reapplied to the core chunk, string-based so the script needs no new runtime dependency.I validated the reconstruction empirically against the shipped bundle: with all components selected,
core.main.css + scheme.css + component/*/*.main.css + print variantscovers 100% of the rules ofdsfr.main.css + dsfr.print.css(the only structural difference is the dark-mode custom properties being declared in two blocks instead of one merged block).Detection of used components
@codegouvfr/react-dsfr/<Module>occurrence in the sources (same crawling asonly-include-used-icons, extended to.css/.scss/.sass/.lessfiles), resolved through a static tableREACT_DSFR_MODULE_TO_DSFR_COMPONENTSthat includes transitive dependencies (e.g.Header→ header, navigation, modal, logo, button, link, search, input, form). The table was built by extracting thefr-*classes each component (and its internal imports) renders, and mapping them to the owning DSFR stylesheet. When in doubt, a dependency is included (too much CSS is a size cost, not enough is a rendering bug).fr-table→ table,fr-btn→ button, ...) catchesfr.cx("fr-table")/ plain JSX class usage without the React component.Fail-safe: if the sources import a react-dsfr module the static table does not know (e.g. a component added in a newer release), the script warns and includes every component — output equivalent to the original bundle, never a broken page.
What is tested
test/runtime/scripts/onlyIncludeUsedComponents/): import detection (default/named/deep/require/dynamic imports,blocks/, directdsfr/component/*css imports), module resolution (components, non-components, unknown → fail-safe), raw class detection, stylesheet generation (cascade order, exclusion, url rewriting, charset/sourcemap stripping, Mui patch,main→plain css fallback for components likedownload, determinism).yarn buildOK, eslint + prettier clean.Button,Alert,Accordion+"additionalComponents": ["table"]:dsfr.min.css: 600kB → 282kB raw (-53%), 76kB → 36kB gzip (-52%) with 5/45 components. Most of the remainder is the core (typography, grid, color tokens, Marianne font-faces), which is incompressible without breaking things.fr-header,fr-footer,fr-tabs...) absent from the output, core/scheme/print/fr-grid-rowpresent, nourl("../...")left, Mui patch applied twice (hover + active).No change since last run, and a run after a fail-safe run correctly restores the trimmed output.public/dsfr/dsfr.min.csspatched,index.htmlhref gets?hash=<fnv1a>.Follow-up fix
While re-reading the fail-safe logic in
resolveModuleIdToDsfrComponents, found that two paths returned[](silently "not a component") instead ofundefined(triggers the warn-and-include-everything fail-safe): a directdsfr/component/<x>stylesheet import for an<x>unknown toDSFR_COMPONENTS_CASCADE_ORDER, and any unrecognized lowercase-starting module id. Both are nowundefined, so a future DSFR/react-dsfr release that adds a component this script doesn't know about degrades to "include everything" with a warning, instead of silently shipping incomplete CSS.Known limitations
only-include-used-icons, detection is textual: dynamically composed import paths or class names are not seen — that is whatadditionalComponentsis for.dsfr.min.cssconcatenates the upstream-minified granular files instead of re-minifying the whole bundle with thecsspackage, so its formatting differs slightly from the original (semantically identical).utility/colorsandutility/iconsare not part ofdsfr.cssupstream and are left untouched (icons are already handled byonly-include-used-icons).Usage
npx react-dsfr only-include-used-components # or the standalone bin npx only-include-used-componentsTypically as a
prebuild/predevstep, next toupdate-icons:Happy to iterate on naming, the config location, or to add documentation to the website if the approach suits you.