perf(client): compile only the Clarity components in use #560

Merged
allan merged 1 commits from perf/curate-clarity-css into release/7.23 2026-11-09 21:38:22 +00:00
Collaborator

Closes #505, partly. The stylesheet ships one file to every route and 86% of its rules match nothing on the screen being rendered; the first of the three changes that reduce that is here.

What this changes

  • client/src/clr.scss compiles Clarity from its own sources: it is @clr/ui/src/utils/_components.clarity.scss - the file clr-ui.min.css is built from - with the components this app never instantiates removed (wizard, timeline, combobox, the login page, stepper, breadcrumbs, badges, datalist, number-input, password, range, file-input). typography/font-metropolis is left out, which is the same CSP reason the same-origin @font-face blocks in styles.scss exist for. Adding a component back is one line and a build.
  • styles.scss imports that instead of @clr/ui/clr-ui.min.css, and the duplicated handsontable.min.css import (it appeared on two consecutive lines) is gone.
  • angular.json gains stylePreprocessorOptions.includePaths: ["node_modules"] so the @clr/ui/src/... specifiers resolve, and a {"type": "bundle", "name": "styles"} budget.

Measurements

The curated entry is the real stylesheet minus the drops, not a different Clarity: compiling a mirror of the unmodified _components.clarity.scss from source produces 627,918 bytes against the shipped clr-ui.min.css at 621,259 - the 6.6 kB difference is the base64 Metropolis font-face blocks that scripts/strip-clr-base64-fonts.mjs strips at postinstall. So the comparison below is the drops alone.

production build, styles bundle
  before   1,117,811 bytes raw   gzip -9 125,807
  after    1,002,159 bytes raw   gzip -9 111,117   (-10.3% raw, -11.7% gzip)
  Angular's own report: 1.12 MB / 90.60 kB transfer -> 1.00 MB / 80.60 kB

Verified in the built CSS: clr-wizard, clr-timeline, login-wrapper, clr-breadcrumb, clr-stepper are absent; clr-datagrid, clr-tree-node, clr-modal, clr-accordion, clr-alert, clr-tooltip and the Handsontable rules are all still there (rule count 6,514 -> 6,373).

The budget was proved to bite rather than assumed: built once with maximumWarning temporarily at 900kb, which failed the way it should -

Warning: styles exceeded maximum budget. Budget 900.00 kB was not met by
102.16 kB with a total of 1.00 MB.

then restored to 1.05mb, which leaves about 48 kB of headroom over today's file.

What this does not do, and why

Handsontable's CSS (162 kB + 27 kB theme) is the other block that is not needed on every route, and it is deliberately not in this change. Moving it into the grid components' styles is not a file move: DC's own grid customisations are interleaved through styles.scss in 55 constructs, inside app-editor, app-viewer, app-viewboxes, body[cds-theme='dark'] and ::ng-deep scopes, and two of them are exact selector ties with Handsontable's own rules (.handsontable .htAutocompleteArrow, .handsontable.listbox). Once the vendor CSS is injected with the component instead of at first paint, those ties resolve the other way. Doing that safely means moving the overrides with the vendor CSS and checking the result on a rendered grid, which needs a seeded estate - a separate PR with screenshots, not a footnote in a perf one.

What could not be verified here: the rendered app. There is no seeded estate on this box, so the visual check is CI's Cypress and Lighthouse runs, and I would want a reviewer's eye on the editor before this is called done. tsc -p tsconfig.app.json --noEmit was clean and two production builds ran to completion (exit 0); the only warnings are the pre-existing template and CommonJS ones.

Closes #505, partly. The stylesheet ships one file to every route and 86% of its rules match nothing on the screen being rendered; the first of the three changes that reduce that is here. ## What this changes - `client/src/clr.scss` compiles Clarity from its own sources: it is `@clr/ui/src/utils/_components.clarity.scss` - the file `clr-ui.min.css` is built from - with the components this app never instantiates removed (wizard, timeline, combobox, the login page, stepper, breadcrumbs, badges, datalist, number-input, password, range, file-input). `typography/font-metropolis` is left out, which is the same CSP reason the same-origin `@font-face` blocks in `styles.scss` exist for. Adding a component back is one line and a build. - `styles.scss` imports that instead of `@clr/ui/clr-ui.min.css`, and the duplicated `handsontable.min.css` import (it appeared on two consecutive lines) is gone. - `angular.json` gains `stylePreprocessorOptions.includePaths: ["node_modules"]` so the `@clr/ui/src/...` specifiers resolve, and a `{"type": "bundle", "name": "styles"}` budget. ## Measurements The curated entry is the real stylesheet minus the drops, not a different Clarity: compiling a mirror of the *unmodified* `_components.clarity.scss` from source produces 627,918 bytes against the shipped `clr-ui.min.css` at 621,259 - the 6.6 kB difference is the base64 Metropolis font-face blocks that `scripts/strip-clr-base64-fonts.mjs` strips at postinstall. So the comparison below is the drops alone. ``` production build, styles bundle before 1,117,811 bytes raw gzip -9 125,807 after 1,002,159 bytes raw gzip -9 111,117 (-10.3% raw, -11.7% gzip) Angular's own report: 1.12 MB / 90.60 kB transfer -> 1.00 MB / 80.60 kB ``` Verified in the built CSS: `clr-wizard`, `clr-timeline`, `login-wrapper`, `clr-breadcrumb`, `clr-stepper` are absent; `clr-datagrid`, `clr-tree-node`, `clr-modal`, `clr-accordion`, `clr-alert`, `clr-tooltip` and the Handsontable rules are all still there (rule count 6,514 -> 6,373). The budget was proved to bite rather than assumed: built once with `maximumWarning` temporarily at `900kb`, which failed the way it should - ``` Warning: styles exceeded maximum budget. Budget 900.00 kB was not met by 102.16 kB with a total of 1.00 MB. ``` then restored to `1.05mb`, which leaves about 48 kB of headroom over today's file. ## What this does not do, and why Handsontable's CSS (162 kB + 27 kB theme) is the other block that is not needed on every route, and it is deliberately **not** in this change. Moving it into the grid components' styles is not a file move: DC's own grid customisations are interleaved through `styles.scss` in 55 constructs, inside `app-editor`, `app-viewer`, `app-viewboxes`, `body[cds-theme='dark']` and `::ng-deep` scopes, and two of them are exact selector ties with Handsontable's own rules (`.handsontable .htAutocompleteArrow`, `.handsontable.listbox`). Once the vendor CSS is injected with the component instead of at first paint, those ties resolve the other way. Doing that safely means moving the overrides with the vendor CSS and checking the result on a rendered grid, which needs a seeded estate - a separate PR with screenshots, not a footnote in a perf one. What could not be verified here: the rendered app. There is no seeded estate on this box, so the visual check is CI's Cypress and Lighthouse runs, and I would want a reviewer's eye on the editor before this is called done. `tsc -p tsconfig.app.json --noEmit` was clean and two production builds ran to completion (exit 0); the only warnings are the pre-existing template and CommonJS ones.
hermes added 1 commit 2026-11-09 20:48:09 +00:00
perf(client): compile only the Clarity components in use
Build / Build-and-ng-test (pull_request) Successful in 4m51s
Build / lighthouse (pull_request) Successful in 6m50s
Build / Build-and-test-development (pull_request) Successful in 19m51s
464ea30777
styles.scss imported @clr/ui/clr-ui.min.css, which is Clarity's whole
stylesheet: 621 kB minified, of which 86% of the rules match nothing on the
route being rendered. wizard, timeline, combobox, the login page, stepper,
breadcrumbs, badges and five form widgets are never instantiated here.

src/clr.scss is Clarity's own utils/_components.clarity.scss - the entry
clr-ui.min.css is built from - with those components removed, so the build
carries only what the app uses and adding one back is a line and a build.
typography/font-metropolis is left out for the CSP reason the font-face
blocks in styles.scss exist for.

Stylesheet 1,117,811 -> 1,002,159 bytes (-10.3%), gzip -9 125,807 -> 111,117.
A bundle budget pins it at 1.05 MB so the next import is a warning rather
than a silent 100 kB.

Client unit coverage

Covered
Statements 84.48%
Branches 76.53%
Functions 82.80%
Lines 85.36%

Lowest covered files (40+ lines, 23 measured):

File Lines Total
src/app/shared/viewboxes/viewboxes.component.ts 55.22% 632
src/app/deploy/sections/automatic/automatic.component.ts 60.47% 210
src/app/services/va-messaging.service.ts 66.12% 62
src/app/services/sas-store.service.ts 68.11% 345
src/app/shared/hot-export/hot-export.util.ts 68.81% 93
src/app/shared/spreadsheet-util/spreadsheet-util.ts 73.45% 324
src/app/services/app.service.ts 78.64% 103
src/app/services/spreadsheet.service.ts 85.00% 60
src/app/shared/dc-validator/dc-validator.ts 85.77% 225
src/app/deploy/sections/sasjs-configurator/sasjs-configurator.component.ts 90.38% 52
src/app/query/query.component.ts 92.39% 368
src/app/deploy/sections/dc-location/dc-location.component.ts 92.47% 93

Run npm run coverage in client for the full html report; this is a map of where the logic is unguarded, not a gate.

Run: https://git.datacontroller.io/dc/dc/actions/runs/3643

<!-- coverage-report --> ### Client unit coverage | | Covered | |---|---| | Statements | 84.48% | | Branches | 76.53% | | Functions | 82.80% | | Lines | 85.36% | Lowest covered files (40+ lines, 23 measured): | File | Lines | Total | |---|---|---| | `src/app/shared/viewboxes/viewboxes.component.ts` | 55.22% | 632 | | `src/app/deploy/sections/automatic/automatic.component.ts` | 60.47% | 210 | | `src/app/services/va-messaging.service.ts` | 66.12% | 62 | | `src/app/services/sas-store.service.ts` | 68.11% | 345 | | `src/app/shared/hot-export/hot-export.util.ts` | 68.81% | 93 | | `src/app/shared/spreadsheet-util/spreadsheet-util.ts` | 73.45% | 324 | | `src/app/services/app.service.ts` | 78.64% | 103 | | `src/app/services/spreadsheet.service.ts` | 85.00% | 60 | | `src/app/shared/dc-validator/dc-validator.ts` | 85.77% | 225 | | `src/app/deploy/sections/sasjs-configurator/sasjs-configurator.component.ts` | 90.38% | 52 | | `src/app/query/query.component.ts` | 92.39% | 368 | | `src/app/deploy/sections/dc-location/dc-location.component.ts` | 92.47% | 93 | Run `npm run coverage` in `client` for the full html report; this is a map of where the logic is unguarded, not a gate. Run: https://git.datacontroller.io/dc/dc/actions/runs/3643

Lighthouse

Route Performance Accessibility Best practices SEO
#/home/tables 0.89 1.00 1.00 1.00
#/editor/DC996664.MPE_X_TEST 0.42 1.00 1.00 1.00
#/view/data 0.71 1.00 1.00 1.00
#/view/data/DC996664 0.76 1.00 1.00 1.00
#/view/data/DC996664.MPE_X_TEST 0.57 1.00 1.00 1.00
#/view/usernav/groups 0.72 1.00 1.00 1.00
#/view/usernav/groups/1 0.77 1.00 1.00 1.00
#/view/usernav/users/1 0.73 1.00 1.00 1.00
#/home/excel-maps 0.84 1.00 1.00 1.00
#/home/excel-maps/BASEL-CR2 0.72 0.95 1.00 1.00
#/home/multi-load 0.81 1.00 1.00 1.00
#/review/submitted 0.88 1.00 1.00 1.00
#/review/approve 0.88 1.00 1.00 1.00
#/review/history 0.88 1.00 1.00 1.00
#/stage/DC20221006T142649516_059582_7169 0.88 1.00 1.00 0.92
#/review/submitted/DC20221006T142649516_059582_7169 0.88 1.00 1.00 1.00
#/system 0.93 1.00 1.00 1.00

Lowest accessibility score: 0.95 (the gate is every route at 0.95).

Failing accessibility audits

  • #/home/excel-maps/BASEL-CR2 - aria-required-children: Elements with an ARIA [role] that require children to contain a specific [role] are missing some or all of those required children. (div.ht-root-wrapper > div.ht-grid > div.ht-grid-content > div#ht_9c104245062ceb82)

Run: https://git.datacontroller.io/dc/dc/actions/runs/3643

<!-- lighthouse-report --> ### Lighthouse | Route | Performance | Accessibility | Best practices | SEO | |---|---|---|---|---| | `#/home/tables` | 0.89 | 1.00 | 1.00 | 1.00 | | `#/editor/DC996664.MPE_X_TEST` | 0.42 | 1.00 | 1.00 | 1.00 | | `#/view/data` | 0.71 | 1.00 | 1.00 | 1.00 | | `#/view/data/DC996664` | 0.76 | 1.00 | 1.00 | 1.00 | | `#/view/data/DC996664.MPE_X_TEST` | 0.57 | 1.00 | 1.00 | 1.00 | | `#/view/usernav/groups` | 0.72 | 1.00 | 1.00 | 1.00 | | `#/view/usernav/groups/1` | 0.77 | 1.00 | 1.00 | 1.00 | | `#/view/usernav/users/1` | 0.73 | 1.00 | 1.00 | 1.00 | | `#/home/excel-maps` | 0.84 | 1.00 | 1.00 | 1.00 | | `#/home/excel-maps/BASEL-CR2` | 0.72 | 0.95 | 1.00 | 1.00 | | `#/home/multi-load` | 0.81 | 1.00 | 1.00 | 1.00 | | `#/review/submitted` | 0.88 | 1.00 | 1.00 | 1.00 | | `#/review/approve` | 0.88 | 1.00 | 1.00 | 1.00 | | `#/review/history` | 0.88 | 1.00 | 1.00 | 1.00 | | `#/stage/DC20221006T142649516_059582_7169` | 0.88 | 1.00 | 1.00 | 0.92 | | `#/review/submitted/DC20221006T142649516_059582_7169` | 0.88 | 1.00 | 1.00 | 1.00 | | `#/system` | 0.93 | 1.00 | 1.00 | 1.00 | Lowest accessibility score: **0.95** (the gate is every route at 0.95). **Failing accessibility audits** - `#/home/excel-maps/BASEL-CR2` - `aria-required-children`: Elements with an ARIA `[role]` that require children to contain a specific `[role]` are missing some or all of those required children. (`div.ht-root-wrapper > div.ht-grid > div.ht-grid-content > div#ht_9c104245062ceb82`) Run: https://git.datacontroller.io/dc/dc/actions/runs/3643
hermes left a comment
Author
Collaborator

Reviewed the full diff at 464ea307: the curated Clarity entry file, its import, and the new styles budget.

Findings:

  • client/src/clr.scss:92 //Badges - the badges component is dropped from the curated list, but the app still uses Clarity's badge classes: client/src/app/shared/user-nav-dropdown/header-actions.component.html:40 and :47 (class="badge badge-danger", class="badge badge-info") and client/src/app/review/approve-details/approve-details.component.html:305, 309, 313 (<span class="badge">). In @clr/ui 17.9.0, .badge and the color modifiers come only from src/emphasis/_badges.clarity.scss; the app's own rules for the class (styles.scss:3985, 4010, 4077, 4855) set visibility and position only. With the import gone the failed-request and debug counters render as plain unstyled text. Re-add @forward '@clr/ui/src/emphasis/badges.clarity'; and its properties partial, or replace the six usages.

Checked and clean: no clr-breadcrumb, clr-badge, clr-wizard, clr-timeline, clr-stepper, clr-login or clr-combobox element remains in client/src, signpost and accordion (both kept) are the two of that group that do appear, and the login sidebar restyles itself in styles.scss:2136-2245 rather than leaning on Clarity's login component. The duplicated handsontable stylesheet import removed at styles.scss:3 was a genuine duplicate and the remaining one is the right one. CI at this head: all three checks success.

1 finding above for review.

Reviewed the full diff at 464ea307: the curated Clarity entry file, its import, and the new styles budget. Findings: - client/src/clr.scss:92 `//Badges` - the badges component is dropped from the curated list, but the app still uses Clarity's badge classes: client/src/app/shared/user-nav-dropdown/header-actions.component.html:40 and :47 (`class="badge badge-danger"`, `class="badge badge-info"`) and client/src/app/review/approve-details/approve-details.component.html:305, 309, 313 (`<span class="badge">`). In @clr/ui 17.9.0, `.badge` and the color modifiers come only from src/emphasis/_badges.clarity.scss; the app's own rules for the class (styles.scss:3985, 4010, 4077, 4855) set visibility and position only. With the import gone the failed-request and debug counters render as plain unstyled text. Re-add `@forward '@clr/ui/src/emphasis/badges.clarity';` and its properties partial, or replace the six usages. Checked and clean: no clr-breadcrumb, clr-badge, clr-wizard, clr-timeline, clr-stepper, clr-login or clr-combobox element remains in client/src, signpost and accordion (both kept) are the two of that group that do appear, and the login sidebar restyles itself in styles.scss:2136-2245 rather than leaning on Clarity's login component. The duplicated handsontable stylesheet import removed at styles.scss:3 was a genuine duplicate and the remaining one is the right one. CI at this head: all three checks success. 1 finding above for review.
allan merged commit 17f8039b4f into release/7.23 2026-11-09 21:38:22 +00:00
allan deleted branch perf/curate-clarity-css 2026-11-09 21:38:23 +00:00
Author
Collaborator

Addressed on the other branch so this PR can merge unmodified: #559, commit 1b5e469a.

I took the second option you offered - replace the usages - rather than re-adding the module, because the module then has no users at all and this PR's premise holds without exception:

.badge.badge-danger  -> .dc-badge.dc-badge--danger    header-actions.component.html (2)
.badge               -> .dc-badge                     approve-details.component.html (6, not 3 -
                                                      the table has a second block at 531/535/539)

styles.scss now owns .dc-badge and its two modifiers. The values are not invented - they are Clarity 17.9.0's rendered values, read from the running app by injecting an element with the old classes and asking for its computed style:

                        old .badge          new .dc-badge
background              rgb(79,97,105)      rgb(79,97,105)
.badge-danger           rgb(224,34,0)       rgb(224,34,0)
.badge-info             rgb(0,121,173)      rgb(0,121,173)
height / min-width      16px / 16px         16px / 16px
padding                 2px 4px             2px 4px
border-radius           8px                 8px
font-size / weight      10px / 500          10px / 500
letter-spacing          0.5px               0.5px

Measured again after the swap on the deployed build, and the two columns match exactly. Unit suite 1403 passing.

One thing your review implies but does not say, worth noting for the follow-up on #505: styles.scss still carries .badge rules at 3983, 4008, 4075 and .badge-warning at 4853 that now match nothing - dead once this lands, and they can go with the next slice.

Addressed on the other branch so this PR can merge unmodified: **#559**, commit `1b5e469a`. I took the second option you offered - replace the usages - rather than re-adding the module, because the module then has no users at all and this PR's premise holds without exception: ``` .badge.badge-danger -> .dc-badge.dc-badge--danger header-actions.component.html (2) .badge -> .dc-badge approve-details.component.html (6, not 3 - the table has a second block at 531/535/539) ``` `styles.scss` now owns `.dc-badge` and its two modifiers. The values are not invented - they are Clarity 17.9.0's rendered values, read from the running app by injecting an element with the old classes and asking for its computed style: ``` old .badge new .dc-badge background rgb(79,97,105) rgb(79,97,105) .badge-danger rgb(224,34,0) rgb(224,34,0) .badge-info rgb(0,121,173) rgb(0,121,173) height / min-width 16px / 16px 16px / 16px padding 2px 4px 2px 4px border-radius 8px 8px font-size / weight 10px / 500 10px / 500 letter-spacing 0.5px 0.5px ``` Measured again after the swap on the deployed build, and the two columns match exactly. Unit suite 1403 passing. One thing your review implies but does not say, worth noting for the follow-up on #505: `styles.scss` still carries `.badge` rules at 3983, 4008, 4075 and `.badge-warning` at 4853 that now match nothing - dead once this lands, and they can go with the next slice.
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dc/dc#560