Standalone routes: the 10 mid-sized modules

This covers PRs 2 through 5 from the what's left plan, as four commits on one branch. Ten route modules are gone, and 76 components and pipes are standalone now. The routes that leaked in through imported modules are gone with them. The number I care about most is download size. A cold visit to /auth used to fetch 4.5 MB of lazy JavaScript, because AuthModule imported all of EditPreferenceModule for one component. Now it fetches 189 KB.

branch t3code/standalone-mid-sized-modules base aaa15ec916, the PR 10830 commit 130 files, +669 −1775 not pushed yet

What changed

Each module's routes now live in a plain Routes array, and the app route loads that array. Creators has a single route, so its app route uses loadComponent instead. Every component the module declared lists its own imports. The guards and route data on the app routes didn't move.

ModuleURLsConvertedLoaded from
fulfillment-order/order-status/...3FULFILLMENT_ORDER_ROUTES
creators/explore-creators3CreatorContainerComponent
setup-profile/content/...6SETUP_PROFILE_ROUTES
forum/forum/...8FORUM_ROUTES
resources/resources/...9RESOURCES_ROUTES
promotional/, /home/..., /search-result/...4PROMOTIONAL_ROUTES
post-detail/post/...6POST_DETAIL_ROUTES
messages/messages6MESSAGES_ROUTES
insights/insights/...15INSIGHTS_ROUTES
auth/auth/...13AUTH_ROUTES

Three pieces outside these folders became standalone too, because the converted components needed them. CreaterProfileInfoComponent from edit-preference renders on step 2 of creator sign-up. FilterModalComponent from src/app/components and ArticleDescriptionPipe from the shared pipes are both used by insights. The table counts 73, and these three make 76. EditPreferenceModule now imports the profile-info component instead of declaring it, so /edit-preference/become-a-creator/creator-profile-info still routes to it.

The commits follow the plan's grouping:

  • 29ae2c77e2. fulfillment-order, creators, setup-profile, and forum.
  • 1448a67334. resources and promotional.
  • ba3dc6383c. post-detail, messages, and insights.
  • 6c3effddc9. auth.

The leaked routes are gone

A lazy NgModule that imports a routed NgModule picks up that module's routes as its own children. A standalone component's imports don't touch the router. So every leak listed in the plan went away with this branch:

  • /auth lost 49 edit-preference route entries. Auth only needed one component from that module, and it now imports that component directly.
  • /resources lost about 80. It imported WalletModule and DashboardModule and used nothing from either. Two templates did use SearchBarV2Component, which they got through WalletModule's exports. They import it directly now.
  • PostDetailModule's three routes left five importers. Those were messages, insights, dashboard, monetize, and portfolio. The module had an empty exports list, so the three NgModules that stay gained nothing from it. I removed the import from them.

I searched for anything that links to those URLs. Post links in the app build absolute /post/... paths. The relativeTo navigations only change query params, and the relative routerLinks are one segment long. Nothing I found points at a leaked URL.

Most leaked URLs sit behind authGuard, so I couldn't request them logged out. /auth is open to logged-out users, which made it the one place to see the change. On base, /auth/privacy and /auth/edit-content match a route under /auth. On this branch they fall through to the app's /:username/:page route and render the sub-page view, the same thing /about/extra-segment does.

Providers moved onto the routes

A service in a lazy module's providers gets one instance per module, shared by every page under it. A route's providers gives the same scope, so that's where these went:

{
  path: '',
  component: ForumContainerComponent,
  providers: [BsModalService],
  children: [ ... ]
}
  • Forum keeps BsModalService on its root route.
  • Resources keeps ResourcesService on its root route. The service is also providedIn: 'root', but the module gave resources its own copy, which holds the unread counts for announcements and tips. The route keeps that separate copy.
  • Messages keeps AudioRecordingService on its route, next to the canDeactivate guard it already had.
  • ReelsMobileViewComponent injects ContentLengthHandlerPipe as a service, so the component provides it itself.

I dropped two things. Forum called NgCircleProgressModule.forRoot({}), but AppModule imports SharedModule, which makes the same call, so the options already exist at the root. The DatePipe providers went too, because the root injector already has one. To check nothing else fell off, I listed every token the converted classes inject that isn't providedIn: 'root'. Each one is either provided at the root through AppModule or provided by the component itself.

What the custom-elements schema was hiding

Six of the old modules set CUSTOM_ELEMENTS_SCHEMA. It tells the compiler to accept any unknown tag or property. With the schema gone, the build failed in four templates. None of these bindings or tags ever did anything:

  • insights.component.html bound [showConfirmationBtn] and [cancelBtnLabel] on app-send-email-modal, which has neither input.
  • insight-fan-management.component.html bound [isContinueDisabled] on app-import-confirm-modal.
  • import-confirm-modal.component.html bound [failedImports] on app-failed-imports-modal.
  • login.component.html had an <ng-adsense> tag. No installed package defines it, so the browser rendered an empty unknown element. Its parent is a column flex container with no gap, so removing it moves nothing.

I deleted all four. With the schema gone, the next binding or tag that doesn't match anything fails the build instead of doing nothing quietly.

Six dead promotional components deleted

PromotionalModule declared six components that no template or route used. They were direct-to-fans, trusted-brands, user-profile, explore-topfan-pro, feature-display-grid, and featured-categories. Converting them would have been wasted work, and user-profile had a real problem. Its selector is app-user-profile, the same as a shared component's. That was harmless inside an NgModule, but it would clash in a standalone import list. featured-categories.constants.ts stays, because the header imports it.

One leftover is a ::ng-deep app-explore-topfan-pro rule in promotional.component.scss. It matches nothing now. I left it for a cleanup PR.

Auth route strings moved to constants

The auth routes had inline SEO text in their data. It now lives in authConstants.ts under LOGIN_PAGE.SEO_DESCRIPTION, SIGN_UP_PAGE, and FORGOT_PASSWORD_PAGE. The sign-up description was a template literal with a line break and 14 spaces in the middle. I joined it into one line.

Like the public pages in the last PR, nothing reads this data. LoginComponent only reads loginAudience. Each page sets its real title and description itself. On both builds, /auth/login ends up with the title "Login Page" and the description "Login in your TopFan.com Account or Reset your Password". The strings cost about 540 bytes in the initial bundle, since main already imports authConstants.ts.

How it was tested

Every template resolves the same things

A missing import often compiles. Drop RouterLink and routerLink="/" turns into a plain attribute, and the link loses its href. So a passing build proves very little about 76 hand-written import lists.

I compiled base and branch with ngc in full AOT mode. The output lists, for every component, the directives, components, and pipes the compiler matched in its template. A script pulled that list out for each component on both sides and diffed them. 655 components match exactly. The only differences are the six deleted promotional components, plus FailedImportsModal, whose import path has a double slash on one side. It's the same class. To make sure the check works, I removed RouterLink from one component. It compiled, and the diff flagged it.

Builds and lint

I built base in a separate worktree and the branch in the main checkout, both with ng build --configuration=qa --prerender=false. Both passed. ESLint on the 91 changed TypeScript files went from 99 errors and 26 warnings to 23 errors and 26 warnings. All 76 errors that disappeared were prefer-standalone. The 49 that remain are the same ones base has, under the same rules.

Bundle size

I rebuilt both sides with --stats-json and walked the static imports from main to find the initial files.

Initial loadBaseThis branch
JavaScript files1815
JavaScript bytes10,472,00410,481,271, +9,267
main bytes287,523287,458
CSS bytes2,134,9052,134,905
All browser JavaScript23,523,53423,521,040, −2,494

The initial growth is 0.09%, and I checked where it comes from. No module entered or left the initial load. The same source files are in it on both sides. esbuild regrouped the initial chunks, merging two of them into one 7.2 MB file, and the minifier picked different names across 605 files. About 540 bytes of the growth is the auth strings. The rest is that renaming.

The lazy side is where it pays off. This table shows the JavaScript a first visit to each route fetches on top of the initial load. It's raw bytes before compression. Some of those files may already be cached from another page.

RouteBaseThis branchChange
/auth4,526,115189,025−4,337,090
/resources4,236,24385,459−4,150,784
/insights2,513,5701,995,786−517,784
/messages2,464,1662,037,576−426,590
/ and /home1,127,4571,123,962−3,495
/post425,635425,444−191
/forum131,906131,634−272
/content109,781109,602−179
/explore-creators39,14738,615−532
/order-status16,06215,578−484

Auth and resources no longer drag in edit-preference, wallet, and dashboard. Messages and insights no longer pull in everything PostDetailModule imported. Insights still loads WalletModule's code, because InsightsEarningComponent renders app-transaction-history. It imports the module for that one component, which brings the code but not the routes.

Server-rendered HTML

I started the SSR server from each build and requested the same 28 URLs on both. I replaced the chunk hashes and Angular's style IDs with placeholders, then diffed the full HTML.

URLStatusResult
/home, /search-result/a/b, /creators, /post/someuser/image/123200same HTML
/, /post/forum/someuser/123200same markup, new component IDs
/order-status/confirmed, /order-status/dispute200same markup, new component IDs
Nine /auth/... URLs200same HTML
/forum, /forum/list, /messages, /insights, /resources, /content, /explore-creators200same HTML
/setup-profile, /promotional404same HTML
/auth/privacy, /auth/edit-content200sub-page view instead of the auth shell

On every URL the preload list and the integrity hash on main differ, since the chunks changed. The "new component IDs" rows need a word. Angular hashes each component's metadata into an ID, and the standalone flag is part of that hash. PromotionalComponent and the order-status components changed that flag, so their IDs changed in the hydration data. The server and the browser run the same build, so they still agree.

This check has a blind spot. The auth pages and the guarded pages come back as the app shell with an empty outlet on both builds. The diff says they match, but it says nothing about their templates. The dependency diff and the browser cover those.

In the browser

I opened the branch build and loaded /, /auth/login, /auth/signup, step 1 of /auth/creators-signup, and /order-status/confirmed. Each rendered the same components as base, and the console had no Angular errors. Two failures showed up on both builds. PUT /api/orders returns 400 when there's no order in the URL, and the reCAPTCHA endpoint returns 404 on localhost. Order status also says "from undefined undefined" on both, for the same missing-params reason.

Affected areas

Most of these pages need a logged-in account, and I didn't have one in this session. So the risk tags lean on how much each page does and how little of it I could click.

AreaRiskWhy
Auth High Every login, sign-up, OTP, password reset, and social callback goes through it. SSR renders only the shell here, and I loaded three pages without submitting anything.
Home and category pages Medium The busiest URLs on the site. I deleted six components from this module. SSR markup matches.
Insights Medium 15 declarations, four removed bindings, the filter modal, and the fan import flow. Never opened.
Messages Medium The audio recorder and the leave-page guard moved onto the route. Never opened.
Forum and resources Medium Their module-level services now sit on the root route. Resources lost two module imports. Never opened.
Post detail Medium Dashboard, monetize, and portfolio dropped the module import. The mobile reel view provides its pipe itself now.
Order status, content setup, explore creators Low Small, self-contained, and nothing else imported them.
Leaked URLs Low They stop resolving, but nothing in the app links to them.

Verification checklist

Use a QA creator account. Load each page directly once, then reach it from another page so the browser fetches the lazy chunk. Keep the console open the whole time. Checks are saved in this browser.

0 of 0 checked

High Auth

Medium Home and category pages

Medium Insights

Medium Messages

Medium Forum and resources

Medium Post detail

Low Small modules

Leaked URLs and every page

Not tested

  • Anything behind login. I compiled and diffed messages, insights, forum, resources, content, explore creators, and the post pages, but never opened them.
  • Form submissions. I didn't log in, sign up, reset a password, or finish a social login.
  • The leaked URLs behind authGuard. I only confirmed the change on /auth.
  • A phone-sized viewport, which is the only way to reach the mobile reel view.
  • A white-label build or a prerender run.

What's next

The branch is ready to split into the four PRs the plan suggested, one per commit, each based on the one before. Auth should go last and get the full checklist above.

After that, the large modules are left. Those are edit-preference, profile, monetize, dashboard, and wallet, at 40 to 75 components each. The plan says to split SharedModule first, and this branch backs that up. 66 of the 76 import lists here include SharedModule, and it brings all 147 of its declarations whether a page uses them or not.