The routes already declared loadChildren, but in the eager form - a function returning the module class - so Angular bundled every screen into the initial chunk and loaded it before the first paint. The only chunk that was ever split was exceljs, because it uses a dynamic import.
Each route now returns a dynamic import, which is the whole change.
Measured
Production build, before and after:
before
after
main
8.96 MB
3.50 MB
initial total (raw)
10.29 MB
4.83 MB
initial total (transfer)
2.29 MB
849.30 kB
And the lazy side, which is what a route now costs when it opens:
So the login screen no longer downloads the grid, the Excel stack or the viewer, and a user who never opens the editor never fetches it.
Why it is safe
No route module declares providers, so a lazy module cannot introduce a second instance of a service the shell already holds - the classic way this change breaks an app.
ReviewRouteComponent and NotFoundComponent stay eager: the first is a route's own shell component, the second is the wildcard.
client/src/app/app.routing.ts is the only file that changes; no component, service or template is touched.
Verification
Production build compiles clean, with the chunk table above.
Karma, locally: 896 tests, all passing.
The Cypress suite runs on this pull request and navigates every route, which is the assertion that lazy loading did not leave a screen unreachable; the published Lighthouse table in the pull request will show what it does to the score.
Note on where this leaves the bundle
home-home-module is 1.98 MB and home is what / redirects to, so the landing route still pays most of a bundle's worth of cost - the grid and the viewer are out of it now, but the home screen itself is the next thing to look at once this lands and the audit number is read. The smaller items (501 icons, 503 @sheet/crypto, 505 CSS) are separate issues and mostly land inside these chunks now, so their effect on the initial load is smaller than the sizes suggest - except 501 and 505, which move bytes out of the initial stylesheet and the eager icon registry.
The routes already declared `loadChildren`, but in the eager form - a function returning the module class - so Angular bundled every screen into the initial chunk and loaded it before the first paint. The only chunk that was ever split was `exceljs`, because it uses a dynamic import.
Each route now returns a dynamic import, which is the whole change.
## Measured
Production build, before and after:
| | before | after |
|---|---|---|
| main | 8.96 MB | 3.50 MB |
| initial total (raw) | 10.29 MB | 4.83 MB |
| initial total (transfer) | 2.29 MB | 849.30 kB |
And the lazy side, which is what a route now costs when it opens:
```
home-home-module 1.98 MB (the landing route: `/` redirects to home)
viewer-viewer-module 1.06 MB + 809 kB + 731 kB + 111 kB
editor-editor-module 687 kB
licensing 61 kB review 51 kB deploy 41 kB
system 16 kB stage 11 kB
```
So the login screen no longer downloads the grid, the Excel stack or the viewer, and a user who never opens the editor never fetches it.
## Why it is safe
- No route module declares `providers`, so a lazy module cannot introduce a second instance of a service the shell already holds - the classic way this change breaks an app.
- `ReviewRouteComponent` and `NotFoundComponent` stay eager: the first is a route's own shell component, the second is the wildcard.
- `client/src/app/app.routing.ts` is the only file that changes; no component, service or template is touched.
## Verification
- Production build compiles clean, with the chunk table above.
- Karma, locally: 896 tests, all passing.
- The Cypress suite runs on this pull request and navigates every route, which is the assertion that lazy loading did not leave a screen unreachable; the published Lighthouse table in the pull request will show what it does to the score.
## Note on where this leaves the bundle
`home-home-module` is 1.98 MB and `home` is what `/` redirects to, so the landing route still pays most of a bundle's worth of cost - the grid and the viewer are out of it now, but the home screen itself is the next thing to look at once this lands and the audit number is read. The smaller items (501 icons, 503 `@sheet/crypto`, 505 CSS) are separate issues and mostly land inside these chunks now, so their effect on the initial load is smaller than the sizes suggest - except 501 and 505, which move bytes out of the *initial* stylesheet and the eager icon registry.
app.routing.ts already declared loadChildren, but as the eager form - a
function returning the module class - so Angular bundled every screen into
the initial chunk and loaded it before the first paint. Only exceljs, which
uses a dynamic import, was ever split.
Each route now returns a dynamic import, so webpack emits a chunk per screen:
before after
main 8.96 MB 3.50 MB
initial total 10.29 MB 4.83 MB
initial transfer 2.29 MB 849.30 kB
The modules declare no providers, so a lazy module cannot introduce a second
instance of a service the shell already holds - the failure this change would
otherwise risk.
Reviewed client/src/app/app.routing.ts at e309cc17 (both commits: the lazy conversion and the prettier pass). Angular is 20.3, every dynamic import names a class the module exports, and nothing else under client/src imports these modules, so the split itself is clean. Build / Build-and-ng-test is green at this head; Build / Build-and-test-development and lighthouse were still running, so the emitted chunk layout is not verified from here.
client/src/app/home/home-routing.module.ts:15 - { path: 'excel-maps', loadChildren: () => XLMapModule }, and :16 { path: 'multi-load', loadChildren: () => MultiDatasetModule } keep the type form with the static imports at :5-6, so both modules stay in the home chunk. "every routed module" is not reached: convert them to () => import('./xlmap/xlmap.module').then((m) => m.XLMapModule) and the same for multi-dataset.
1 finding above for review.
Reviewed client/src/app/app.routing.ts at e309cc17 (both commits: the lazy conversion and the prettier pass). Angular is 20.3, every dynamic import names a class the module exports, and nothing else under client/src imports these modules, so the split itself is clean. Build / Build-and-ng-test is green at this head; Build / Build-and-test-development and lighthouse were still running, so the emitted chunk layout is not verified from here.
- client/src/app/home/home-routing.module.ts:15 - `{ path: 'excel-maps', loadChildren: () => XLMapModule },` and :16 `{ path: 'multi-load', loadChildren: () => MultiDatasetModule }` keep the type form with the static imports at :5-6, so both modules stay in the home chunk. "every routed module" is not reached: convert them to `() => import('./xlmap/xlmap.module').then((m) => m.XLMapModule)` and the same for multi-dataset.
1 finding above for review.
Review found two more loadChildren in the eager form, in
home-routing.module.ts: excel-maps (XLMapModule) and multi-load
(MultiDatasetModule). They were inside the home chunk, so opening the app's
default screen /home/tables pulled both.
Both now return dynamic imports, so each gets its own chunk (47 kB apiece) and
the tables screen no longer pays for them. The initial total is unchanged -
home was already lazy - which is why the earlier measurement did not show
them.
Right, and the claim in the title was too strong. Fixed at cc43512e: home-routing.module.ts now returns dynamic imports for excel-maps (XLMapModule) and multi-load (MultiDatasetModule), and the static imports are gone.
grep -rn "loadChildren" client/src | grep -v "import(" now returns only the wrapped loadChildren: () => lines, so "every routed module" is true at this head.
The initial total does not move, which is why my earlier measurement missed them: home was already lazy, so those two only ever cost the landing route. What changes is that /home/tables - the redirect target for / - no longer pulls the Excel-map and multi-load screens.
Right, and the claim in the title was too strong. Fixed at cc43512e: `home-routing.module.ts` now returns dynamic imports for `excel-maps` (`XLMapModule`) and `multi-load` (`MultiDatasetModule`), and the static imports are gone.
`grep -rn "loadChildren" client/src | grep -v "import("` now returns only the wrapped `loadChildren: () =>` lines, so "every routed module" is true at this head.
Measured after the change:
```
editor-editor-module 1.46 MB + 687 kB + 512 kB
viewer-viewer-module 1.06 MB + 809 kB + 720 kB + 123 kB
multi-dataset-multi-dataset-module 47 kB
xlmap (own chunk) 47 kB
initial total 4.83 MB (unchanged)
```
The initial total does not move, which is why my earlier measurement missed them: home was already lazy, so those two only ever cost the landing route. What changes is that `/home/tables` - the redirect target for `/` - no longer pulls the Excel-map and multi-load screens.
Re-review at cc43512e of the full base...head diff. Delta since e309cc17 is one commit ("lazy-load the home children too"): client/src/app/home/home-routing.module.ts now imports xlmap.module and multi-dataset.module dynamically and drops both static imports, so the earlier finding at that file no longer applies. No routed module is statically imported anywhere under client/src/app now - app.routing.ts and home-routing.module.ts are the only referrers and all ten are dynamic. The remaining cross-folder imports are type/util files (client/src/app/viewer/viewer.component.ts:42, client/src/app/models/CellValidationSource.ts:4, client/src/app/services/app.service.ts:8) and pull no module into the initial chunk. CI at this head: Build / Build-and-ng-test success, Build-and-test-development and Build / lighthouse still in progress - code review only.
No blocking issues.
Re-review at cc43512e of the full base...head diff. Delta since e309cc17 is one commit ("lazy-load the home children too"): client/src/app/home/home-routing.module.ts now imports xlmap.module and multi-dataset.module dynamically and drops both static imports, so the earlier finding at that file no longer applies. No routed module is statically imported anywhere under client/src/app now - app.routing.ts and home-routing.module.ts are the only referrers and all ten are dynamic. The remaining cross-folder imports are type/util files (client/src/app/viewer/viewer.component.ts:42, client/src/app/models/CellValidationSource.ts:4, client/src/app/services/app.service.ts:8) and pull no module into the initial chunk. CI at this head: Build / Build-and-ng-test success, Build-and-test-development and Build / lighthouse still in progress - code review only.
No blocking issues.
allan
merged commit 7d5f1e8505 into release/7.232026-11-07 16:53:08 +00:00
allan
deleted branch perf/lazy-routes2026-11-07 16:53:09 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The routes already declared
loadChildren, but in the eager form - a function returning the module class - so Angular bundled every screen into the initial chunk and loaded it before the first paint. The only chunk that was ever split wasexceljs, because it uses a dynamic import.Each route now returns a dynamic import, which is the whole change.
Measured
Production build, before and after:
And the lazy side, which is what a route now costs when it opens:
So the login screen no longer downloads the grid, the Excel stack or the viewer, and a user who never opens the editor never fetches it.
Why it is safe
providers, so a lazy module cannot introduce a second instance of a service the shell already holds - the classic way this change breaks an app.ReviewRouteComponentandNotFoundComponentstay eager: the first is a route's own shell component, the second is the wildcard.client/src/app/app.routing.tsis the only file that changes; no component, service or template is touched.Verification
Note on where this leaves the bundle
home-home-moduleis 1.98 MB andhomeis what/redirects to, so the landing route still pays most of a bundle's worth of cost - the grid and the viewer are out of it now, but the home screen itself is the next thing to look at once this lands and the audit number is read. The smaller items (501 icons, 503@sheet/crypto, 505 CSS) are separate issues and mostly land inside these chunks now, so their effect on the initial load is smaller than the sizes suggest - except 501 and 505, which move bytes out of the initial stylesheet and the eager icon registry.app.routing.ts already declared loadChildren, but as the eager form - a function returning the module class - so Angular bundled every screen into the initial chunk and loaded it before the first paint. Only exceljs, which uses a dynamic import, was ever split. Each route now returns a dynamic import, so webpack emits a chunk per screen: before after main 8.96 MB 3.50 MB initial total 10.29 MB 4.83 MB initial transfer 2.29 MB 849.30 kB The modules declare no providers, so a lazy module cannot introduce a second instance of a service the shell already holds - the failure this change would otherwise risk.Reviewed client/src/app/app.routing.ts at
e309cc17(both commits: the lazy conversion and the prettier pass). Angular is 20.3, every dynamic import names a class the module exports, and nothing else under client/src imports these modules, so the split itself is clean. Build / Build-and-ng-test is green at this head; Build / Build-and-test-development and lighthouse were still running, so the emitted chunk layout is not verified from here.{ path: 'excel-maps', loadChildren: () => XLMapModule },and :16{ path: 'multi-load', loadChildren: () => MultiDatasetModule }keep the type form with the static imports at :5-6, so both modules stay in the home chunk. "every routed module" is not reached: convert them to() => import('./xlmap/xlmap.module').then((m) => m.XLMapModule)and the same for multi-dataset.1 finding above for review.
Right, and the claim in the title was too strong. Fixed at
cc43512e:home-routing.module.tsnow returns dynamic imports forexcel-maps(XLMapModule) andmulti-load(MultiDatasetModule), and the static imports are gone.grep -rn "loadChildren" client/src | grep -v "import("now returns only the wrappedloadChildren: () =>lines, so "every routed module" is true at this head.Measured after the change:
The initial total does not move, which is why my earlier measurement missed them: home was already lazy, so those two only ever cost the landing route. What changes is that
/home/tables- the redirect target for/- no longer pulls the Excel-map and multi-load screens.Lighthouse
#/home/tables#/editor/DC996664.MPE_X_TEST#/view/data#/view/data/DC996664#/view/data/DC996664.MPE_X_TEST#/view/usernav/groups#/view/usernav/groups/1#/view/usernav/users/1#/home/excel-maps#/home/excel-maps/BASEL-CR2#/home/multi-load#/review/submitted#/review/approve#/review/history#/stage/DC20221006T142649516_059582_7169#/review/submitted/DC20221006T142649516_059582_7169#/systemLowest accessibility score: 0.87 (the gate is a median of 0.85 while issue 497 is open).
Run: https://git.datacontroller.io/dc/dc/actions/runs/3441
Re-review at
cc43512eof the full base...head diff. Delta sincee309cc17is one commit ("lazy-load the home children too"): client/src/app/home/home-routing.module.ts now imports xlmap.module and multi-dataset.module dynamically and drops both static imports, so the earlier finding at that file no longer applies. No routed module is statically imported anywhere under client/src/app now - app.routing.ts and home-routing.module.ts are the only referrers and all ten are dynamic. The remaining cross-folder imports are type/util files (client/src/app/viewer/viewer.component.ts:42, client/src/app/models/CellValidationSource.ts:4, client/src/app/services/app.service.ts:8) and pull no module into the initial chunk. CI at this head: Build / Build-and-ng-test success, Build-and-test-development and Build / lighthouse still in progress - code review only.No blocking issues.