What I actually did
I implemented the remaining items scored higher than 6 from the combined review. Score 6 and below stayed out. I did not restore PR 10977's posts-and-ads forkJoin, and I left the idle modal defer block alone.
Service cleanup was already on origin/release-0.0.25, so there was nothing to cherry-pick. The worktree started at merge 7a7f894767.
Score cut
| Item | Remaining | This change |
|---|---|---|
| Profile banner priority | 9 | Shipped |
| S3 cache headers | 9 | Shipped. HTML is re-uploaded so old immutable metadata is replaced |
| Visible profile SSR | 8 | Shipped. Server and client both read TransferState, then a logged-in browser refreshes |
| Bootstrap CSS reduction | 7 | Shipped. Compiled. Gzip went from 34.3 KB to 30.6 KB for Bootstrap alone |
| Defer sidebar and welcome video | 7 | Shipped. Component imports stay. Modal defer stays. Placeholders are sized |
| Size the header logo | 7 | Shipped with max width and contain, not a 290×32 crop |
| Delay secondary profile work | 6 | Skipped. Higher than 6 was the cutoff |
| Optional-request resilience, ad skeleton wrappers, skip 2×, font-display swap, translation cookie | 3 or below | Skipped |
Banner priority
The optimization directive already knew how to set fetchpriority="high" and eager loading. The profile banner never asked for it, and it shipped loading="lazy", which blocked the directive from flipping loading. I turned priority on and dropped the lazy attribute. The subscription-modal copy of the banner is still lazy. That image is not LCP.
src/app/modules/shared/components/profile/profile.component.html
Header logo sizing
PR 10977 asked for a 290×32 WebP. That is a 9:1 box. Tenant logos are not all 9:1, and the CSS already does object-fit: contain at 32px tall with a 290px max. Forcing both width and height on the image handler would squash the file before CSS could save it.
I request width only, 290 or 240 for the pro header, with fit="contain". WebP is already the default. I also set [priority]="true" because this logo was already loading="eager" and it is LCP on marketing chrome.
header-layout.component.html
S3 cache headers
Every rclone copy used Cache-Control: public,max-age=31536000,immutable, including index.html and prerendered HTML. Express never sees those objects. A checksum skip also leaves old metadata on unchanged HTML, so a deploy can look like it did nothing.
deployment_script/rclone-upload-dist.sh now does three passes:
- Non-HTML, non-manifest files: immutable, checksum skip as before.
- HTML:
max-age=0,must-revalidate,--ignore-timesso existing objects get new headers. ngsw.json, worker scripts, andmanifest.webmanifest: same revalidate treatment.
TopFans still only excludes explore/**. Tenant scripts set RCLONE_TENANT_EXCLUDES=1 so splash, favicon, and sitemap stay off this path the way they already were. I did not hand-edit generated white-label projects.
After the first deploy, open an existing index.html in the bucket and read Cache-Control. If it still says immutable, the helper did not run, or CloudFront is holding the old object.
Bootstrap CSS
styles.scss used to @use the full Bootstrap barrel. It now @uses _bootstrap-partials.scss. Bootstrap 5.2 is still an @import library, so the partials file uses @import. The app boundary stays @use.
I dropped navbar, list-group, offcanvas, and placeholders after searching the templates. I kept spinners because a few SCSS files still style .spinner-border. I compiled both stacks against the main checkout's node_modules:
| Sheet | Uncompressed | Gzip |
|---|---|---|
Full bootstrap/scss/bootstrap | 276,971 bytes | 34,340 bytes |
| Partials | 237,254 bytes | 30,602 bytes |
About 4 KB gzip. Not a miracle. Enough to be worth the compile check, not enough to pretend we deleted Bootstrap.
Defer sidebar and welcome video
Modals already defer on idle. Sidebar and welcome video did not. Both now defer on viewport and prefetch on idle.
Angular still needs the components in the parent imports array. Removing them is how you get a build failure with a selector still in the template. I left ProfileSidebarComponent and WelcomeVideoPostComponent declared.
The welcome-video placeholder is a 16:9 block in --configSecondaryBgColor, so the feed does not collapse while the chunk loads. The sidebar placeholder reuses app-right-sidebar-skeleton, the same skeleton the loading path already shows.
profile.component.html, abbreviated
Visible profile SSR
The whole profile shell sat behind cs.isBrowser && isBasicProfileLoaded. The server rendered a skeleton and a hidden name/bio. First paint in the document was empty chrome.
Two things had to move together. Un-gating the template without consuming TransferState would refetch on the client and mismatch hydration. Using PRERENDERED on the server and REFRESH_EVERY_N in the browser, which is what PR 10977 did, never reads the TransferState entry. Only PRERENDERED does.
The first request is now always CacheTypes.PRERENDERED. If TransferState has the payload, of(data) emits in the same turn as subscribe, so isBasicProfileLoaded is true before the first client render. If the visitor is logged in and we just consumed transferred anonymous data, a second REFRESH_EVERY_N call updates follow and privacy. That second call does not wipe the feed unless privacy actually flipped. Feed loading, ads, and fetchPostsWithSkyscraperAds stay as service cleanup left them.
profile.component.ts, abbreviated
The hidden prerender name/bio block only renders when the profile has not loaded yet. Once first-paint content is in the document, duplicating it is noise.
What I refused from PR 10977
- Posts waiting on ads through
forkJoin. - Eight-second timeouts on tags, merchandise, and welcome video.
afterNextRenderplus a timer as a stand-in for LCP.- Removing sidebar and welcome video from
imports. - Moving the six modals out of
@defer (on idle). - Skipping 2× srcset globally.
isPlatformBrowser()inside the image directive. That path already usesCommonService.isBrowser.- The generator
inlineCriticalassignment. Prod and QA already enable it.
Checks I ran
| Check | Result |
|---|---|
npx tsc -p tsconfig.app.json --noEmit | Pass. 8.62s, 2581 files |
ng lint on touched profile and header files | 8 template errors and 1 warning, all on pre-existing lines, none in the new hunks |
| Sass compile of partials vs full Bootstrap | Both compiled. Gzip 30,602 vs 34,340 |
bash -n on the four upload scripts | Pass |
| Unit tests | Not run. No specs, TDD not in the matrix |
prerender-qa / live SSR HTML | Not run. No local 4200/4000 server, and a QA prerender is a 10 GB build |
| In-browser LCP, CLS, hydration console | Not run. T3 browser is blocked by AppArmor on this host |
Route generation was not touched. /${username} already comes from generate-static-routes.mjs. White-label prerender lists still come from POST rest/prerender-routes. No backend change.
Human QA checklist
I could not load a profile in a browser from this session. This list is the part a person has to do. Check a box when you have seen it, not when it sounds right.