PR 10977. Performance hotfix into release-0.0.25. English first paint drops Translate, a second Roboto sheet, CDN Bootstrap, Cropme, and a duplicate IVS player. The profile card paints before posts, merch, and welcome video.

What moved

ThingBeforeAfter
Google Translateelement.js on every loadLocaleService.ensureGoogleTranslateLoaded()
Robotopreload 400/700 plus the full sheetone print/onload stylesheet
CDN scriptsBootstrap, Cropme, IVSgone; IVS is npm, Veriff stays idle
Profile markupcs.isBrowser && isBasicProfileLoadedisBasicProfileLoaded, including SSR
Posts, merch, welcome videosame turn as the cardscheduleAfterFirstPaint()
Sidebar, welcome video JSeager imports@defer (on viewport; prefetch on idle)
User GET cacheREFRESH_EVERY_N everywherePRERENDERED on server
Bootstrap CSSbootstrap/scss/bootstrapcurated _bootstrap-partials.scss
S3 Cache-ControlHTML immutable for a yearHTML must-revalidate

Where the effort went

index.html third partiesfirst paint only English visitors no longer download Translate or a second font file before Angular boots.
profile waterfallcard, then the rest LCP is the banner and bio. Posts, ads, tracking, and merch wait for afterNextRender.
one rclone headersplit cache Hashed assets stay immutable. index.html and prerendered HTML must revalidate.

21 files, +452 / −344. Head cb09e9d27b. Diffs are grouped by concern, not GitHub file-alpha order.

Order of work

Shell
First paint
  • Cut Translate, CDN scripts
  • One Roboto sheet
Profile
Card, then rest
  • SSR the card
  • @defer sidebar and welcome video
CSS
Bootstrap diet
  • Curated partials
  • Icomoon font-display: swap
Deploy
Split S3 cache
  • HTML must-revalidate
  • Critical CSS on WLS prod/qa

Each page ahead is one concern: what changed, then the hunks. A hunk that does two jobs is shown in both pages.

The document shell was downloading scripts and styles the first screen does not need. This is the change a visitor feels before Angular boots.

What left index.html

  • English visitors no longer pay for Google Translate on every load. The script itself moves to the next page.
  • One Roboto stylesheet, loaded as print then swapped to all. The extra 400/700 preload is gone.
  • CDN Bootstrap, Cropme, and the Amazon IVS player script are removed. IVS already ships as npm amazon-ivs-player.
  • Veriff still idle-loads, and only on creator-signup routes.
  • Critical CSS now uses --configBgColor / --configBgTextColor so the splash matches the tenant.
  • Also deleted: the large Organization JSON-LD block, iPhone 17 splash links, apple-mobile-web-app-capable, a duplicate 192 favicon, and the IVS dns-prefetch. Treat the JSON-LD and splash removals as review items.

Still on the first-paint path

  • One Roboto sheet, print then all
  • GTM dns-prefetch
  • Veriff, idle, signup routes only
  • The googtrans cookie, if locale is not en

Splash, icons, JSON-LD, splash colors

src/index.html@@ -42,60 +42,49 @@    <!-- Favicon and App Icons -->    <link rel="apple-touch-icon" sizes="120x120" href="./assets/splashscreen/apple-icon-120.png">    <link rel="apple-touch-icon" sizes="152x152" href="./assets/splashscreen/apple-icon-152.png">    <link rel="apple-touch-icon" sizes="167x167" href="./assets/splashscreen/apple-icon-167.png">    <link rel="apple-touch-icon" sizes="180x180" href="./assets/splashscreen/apple-icon-180.png">    <link rel="icon" type="image/x-icon" href="./assets/splashscreen/apple-icon-180.png"/>    <link rel="icon" sizes="192x192" href="./assets/splashscreen/apple-icon-192.png">-    <link rel="icon" sizes="192x192" href="./assets/splashscreen/apple-icon-192.png">    <link id="app-manifest" href="./assets/Json/manifest.json" rel="manifest"/>-    <!-- DNS prefetch for Amazon IVS player to speed up connection -->-    <link rel="dns-prefetch" href="//player.live-video.net">-    <meta name="apple-mobile-web-app-capable" content="yes">   <!-- Splash Image (for iOS) -->    <!-- iPad Pro -->    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-2048-2732.png" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-2732-2048.png" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-1668-2388.png" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-2388-1668.png" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-1536-2048.png" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-2048-1536.png" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-1640-2360.png" media="(device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-2360-1640.png" media="(device-width: 820px) and (device-height: 1180px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-1668-2224.png" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-2224-1668.png" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-1620-2160.png" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">-    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-2160-1620.png" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-1488-2266.png" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">+    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-2160-1620.png" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">+    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-1488-2266.png" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-2266-1488.png" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">    <!-- iPhone 16 Series - 2024 -->    <!-- iPhone 16 Pro Max (6.9") -->    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-1320-2868.png" media="(device-width: 440px) and (device-height: 956px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-2868-1320.png" media="(device-width: 440px) and (device-height: 956px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">    <!-- iPhone 16 Pro (6.3") -->    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-1206-2622.png" media="(device-width: 402px) and (device-height: 874px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-2622-1206.png" media="(device-width: 402px) and (device-height: 874px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">    <!-- iPhone 16 Plus (6.7") -->    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-1290-2796.png" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-2796-1290.png" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">    <!-- iPhone 16 (6.1") -->    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-1179-2556.png" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-2556-1179.png" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">-    <!-- iPhone 17 Series - 2025 -->-    <!-- iPhone 17 Pro Max (6.9") -->-    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-1320-2868.png" media="(device-width: 440px) and (device-height: 956px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">-    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-2868-1320.png" media="(device-width: 440px) and (device-height: 956px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">-    <!-- iPhone 17 Pro (6.3") -->-    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-1206-2622.png" media="(device-width: 402px) and (device-height: 874px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">-    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-2622-1206.png" media="(device-width: 402px) and (device-height: 874px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">-    <!-- iPhone 15 Series and earlier -->    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-1170-2532.png" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-2532-1170.png" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-1284-2778.png" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-2778-1284.png" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-1125-2436.png" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-2436-1125.png" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">    <link rel="apple-touch-startup-image" href="./assets/splashscreen/apple-splash-1242-2688.png" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">src/index.html@@ -135,119 +124,27 @@  <script type="application/ld+json">    {      "@context": "https://schema.org",      "@type": "WebSite",      "name": "TopFan",      "alternateName": "TopFan Platform",      "url": "https://www.topfan.com"    }-    </script>    --    <script type="application/ld+json">-      {-        "@context": "https://schema.org",-        "@type": "Organization",-        "name": "TopFan",-        "alternateName": "TopFan",-        "url": "https://www.topfan.com",   -        "logo": "https://cdn-www.topfans.com/TopFans/1768394518_topfan_logo.jpg",-        "description": "The leading creator monetization and fan engagement platform that provides creators with their own personal branded website and app, enabling direct-to-fan content, community and commerce.",-        "foundingDate": "2013",-        "founder": {-          "@type": "Organization",-          "name": "TopFan Inc."-        },-        "address": {-          "@type": "PostalAddress",-          "addressLocality": "Denver",-          "addressRegion": "Colorado",-          "addressCountry": "US"-        },-        "contactPoint": {-          "@type": "ContactPoint",-          "contactType": "customer service",-          "url": "https://www.topfan.com/help-faq"-        },-        "sameAs": [-          "https://www.topfan.com"-        ],-        "keywords": "creator monetization, fan engagement, subscription platform, direct-to-fan, content creators, influencers, musicians, athletes, fitness, wellness",-        "award": "Leading Creator Monetization Platform",-        "slogan": "The Best Platform for Creators",-        "hasOfferCatalog": {-          "@type": "OfferCatalog",-          "name": "Creator Services",-          "itemListElement": [-            {-              "@type": "Offer",-              "itemOffered": {-                "@type": "Service",-                "name": "Premium Content Subscriptions",-                "description": "Tiered membership subscriptions for exclusive content"-              }-            },-            {-              "@type": "Offer", -              "itemOffered": {-                "@type": "Service",-                "name": "Merchandise Sales", -                "description": "Integrated e-commerce for creator merchandise"-              }-            },-            {-              "@type": "Offer",-              "itemOffered": {-                "@type": "Service",-                "name": "Live Streaming",-                "description": "Live streaming capabilities with monetization"-              }-            },-            {-              "@type": "Offer",-              "itemOffered": {-                "@type": "Service",-                "name": "Direct Messaging",-                "description": "Paid direct messaging between creators and fans"-              }-            },-            {-              "@type": "Offer",-              "itemOffered": {-                "@type": "Service",-                "name": "Content Publishing",-                "description": "Multi-format content publishing tools"-              }-            }-          ]-        },-        "knowsAbout": [-          "Creator Economy",-          "Fan Engagement", -          "Content Monetization",-          "Direct-to-Fan Platforms",-          "Subscription Services",-          "Musicians",-          "Athletes", -          "Fitness Professionals",-          "Entertainers",-          "Content Creators"-        ]-      }    </script>  <!-- Critical CSS for above-the-fold content -->    <style>      /* Critical CSS for initial paint */      app-root { display: block; min-height: 80vh; }      body {        margin: 0;        font-family: Roboto, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;-        background-color: #013956;-        color: #ffffff;+        background-color: var(--configBgColor, #013956);+        color: var(--configBgTextColor, #ffffff);      }      @keyframes loading { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }      /* Hide non-critical elements during initial load */      .non-critical { opacity: 0; transition: opacity 0.3s ease; }      .loaded .non-critical { opacity: 1; }      /* Prevent CLS by reserving space for hero section */      .promotional-banner {

Fonts, IVS, Bootstrap, Cropme

src/index.html@@ -274,58 +171,38 @@      html.social-callback-boot body {        transition: none !important;        background-color: var(--configSecondaryBgColor, #000);      }    </style>  <!-- Remove non-critical preconnects; keep as dns-prefetch below -->    -    <!-- Fonts and Styles with optimized loading -->-  <!-- Preload critical font weights (400 and 700) to prevent CLS -->-  <link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap">+    <!-- Fonts: one Roboto stylesheet (print/onload). Do not add a second 400/700 preload. -->  <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@100;300;400;500;700;900&display=swap" rel="stylesheet" media="print" onload="this.media='all'"/>  <noscript><link href="https://fonts.googleapis.com/css2?family=Roboto:wght@100;300;400;500;700;900&display=swap" rel="stylesheet"></noscript>        <!-- DNS prefetch for external domains -->    <link rel="dns-prefetch" href="//www.googletagmanager.com">    <link rel="dns-prefetch" href="//cdn.veriff.me">-    <link rel="dns-prefetch" href="//cdn.jsdelivr.net">-    <!-- 3rd-party SDKs - Idle-until-urgent -->+    <!-- Veriff only — IVS is npm amazon-ivs-player; Bootstrap/Cropme are bundled or unused -->    <script>      function loadScript(src, attrs) {        var s = document.createElement('script');        s.src = src;        if (attrs) Object.keys(attrs).forEach(function(k){ s.setAttribute(k, attrs[k]); });        document.head.appendChild(s);      }      function onIdle(cb){        if ('requestIdleCallback' in window) requestIdleCallback(cb, {timeout: 3000});        else setTimeout(cb, 1500);      }-      // Lazy-load Amazon IVS player on first user interaction or navigation-      var ivsLoaded = false;-      function loadIVS() {-        if (ivsLoaded) return;-        ivsLoaded = true;-        loadScript('https://player.live-video.net/1.45.0/amazon-ivs-player.min.js', {defer: ''});-      }-      // Load IVS on first user interaction (click/scroll/touch)-      ['click','scroll','touchstart'].forEach(function(evt){-        document.addEventListener(evt, function onInteraction(){-          onIdle(loadIVS);-          ['click','scroll','touchstart'].forEach(function(e){-            document.removeEventListener(e, onInteraction);-          });-        }, {once: true, passive: true});-      });      window.addEventListener('load', function() {        onIdle(function(){-          // Load Veriff only on specific routes          var veriffLoaded = false;          function shouldLoadVeriff(path){            return /^\/auth\/creators-signup(\/|$)?/.test(path) ||                   /^\/wallet\/current-balance(\/|$)?/.test(path) ||                   /^\/edit-preference\/become-a-creator\/creator-profile-info(\/|$)?/.test(path) ||                   /^\/edit-preference\/become-a-creator\/confirm-creator(\/|$)?/.test(path) ||                   /^\/edit-preference\/become-a-creator\/verify-identity(\/|$)?/.test(path);          }src/index.html@@ -333,34 +210,24 @@            if (veriffLoaded) return;            var p = (window.location && window.location.pathname) || '';            if (shouldLoadVeriff(p)) {              loadScript('https://cdn.veriff.me/sdk/js/1.5/veriff.min.js');              loadScript('https://cdn.veriff.me/incontext/js/v1/veriff.js');              veriffLoaded = true;            }          }-          // initial check          maybeLoadVeriff();-          // observe SPA navigations          (function(){            var origPush = history.pushState;            var origReplace = history.replaceState;            history.pushState = function(){ origPush.apply(this, arguments); setTimeout(maybeLoadVeriff, 0); };            history.replaceState = function(){ origReplace.apply(this, arguments); setTimeout(maybeLoadVeriff, 0); };            window.addEventListener('popstate', function(){ setTimeout(maybeLoadVeriff, 0); });          })();-          loadScript('https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js', {-            integrity: 'sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM',-            crossorigin: 'anonymous'-          });-          // Skip Cropme on home route-          if (window.location && window.location.pathname !== '/') {-            loadScript('https://cdn.jsdelivr.net/npm/cropme@latest/dist/cropme.min.js');-          }        });      });    </script></head>  <body>    <script>      (function() {

Translate is a large third-party script. The PR keeps the off-screen host node and the googtrans cookie, but moves element.js behind LocaleService.

Who loads the script now

index.html injects element.jsLocaleService.ensureGoogleTranslateLoaded() No-op on the server. English users who never open the picker never download the script.
cookie only if locale ≠ ensame cookie, later script index.html still sets googtrans early so a restored locale can apply once the script arrives.
LanguageSwitcher openensureGoogleTranslateLoaded() The picker is the other trigger besides a saved non-English locale.
src/index.html@@ -401,37 +268,25 @@    <div id="google_translate_element"></div>    <main id="main-content" role="main">      <app-root>      </app-root>    </main>    <noscript>Please enable JavaScript to continue using this application.</noscript>-    <!-- Set Google Translate cookie early so translation applies on load -->+    <!-- Set googtrans early; element.js loads on demand via LocaleService -->    <script>      (function() {        var lang = '';        try { lang = localStorage.getItem('tf_locale') || ''; } catch(e) {}        if (lang && lang !== 'en') {          var val = '/en/' + lang;          var parts = location.hostname.split('.');          var root = parts.length > 2 ? '.' + parts.slice(-2).join('.') : location.hostname;          document.cookie = 'googtrans=' + val + ';path=/';          document.cookie = 'googtrans=' + val + ';path=/;domain=' + location.hostname;          document.cookie = 'googtrans=' + val + ';path=/;domain=' + root;        }      })();    </script>--    <!-- Google Website Translator -->-    <script>-      function googleTranslateElementInit() {-        new google.translate.TranslateElement({-          pageLanguage: 'en',-          autoDisplay: false,-          layout: google.translate.TranslateElement.InlineLayout.SIMPLE-        }, 'google_translate_element');-      }-    </script>-    <script src="//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit"></script></body>-</html>\ No newline at end of file+</html>src/app/components/language-switcher/language-switcher.component.ts@@ -51,16 +51,17 @@ export class LanguageSwitcherComponent implements OnInit, OnDestroy {    return lang ? lang.name : LOCALE_CONSTANTS.SUPPORTED_LANGUAGES[0].name;  }  toggleDropdown(): void {    this.isDropdownOpen = !this.isDropdownOpen;    if (this.isDropdownOpen) {      this.searchTerm = '';      this.filteredLanguages = [...this.languages];+      void this.localeService.ensureGoogleTranslateLoaded();    }  }  selectLanguage(langCode: string): void {    this.localeService.setLocale(langCode);    this.isDropdownOpen = false;  }

LocaleService

src/app/core/i18n/locale.service.ts@@ -4,48 +4,127 @@ import { BehaviorSubject } from 'rxjs';import { LOCALE_CONSTANTS } from './locale.constants';declare const google: any;@Injectable({ providedIn: 'root' })export class LocaleService {  private readonly isBrowser: boolean;  private readonly localeSubject = new BehaviorSubject<string>(LOCALE_CONSTANTS.DEFAULT_LOCALE);+  private translateScriptLoading: Promise<void> | null = null;  readonly locale$ = this.localeSubject.asObservable();  constructor(@Inject(PLATFORM_ID) platformId: Object) {    this.isBrowser = isPlatformBrowser(platformId);    if (this.isBrowser) {      const savedLocale = this.getSavedLocale();      this.localeSubject.next(savedLocale);      if (savedLocale !== LOCALE_CONSTANTS.DEFAULT_LOCALE) {        this.applyOnLoad(savedLocale);      }    }  }+  /**+   * Loads Google Translate element.js on demand (picker open or non-English restore).+   * Not injected from index.html so English first paint stays free of the third-party script.+   */+  ensureGoogleTranslateLoaded(): Promise<void> {+    if (!this.isBrowser) {+      return Promise.resolve();+    }++    if (typeof google !== 'undefined' && google?.translate?.TranslateElement) {+      this.initTranslateElement();+      return Promise.resolve();+    }++    if (this.translateScriptLoading) {+      return this.translateScriptLoading;+    }++    this.translateScriptLoading = new Promise<void>((resolve, reject) => {+      const existing = document.querySelector(+        'script[data-google-translate]'+      ) as HTMLScriptElement | null;+      if (existing) {+        existing.addEventListener('load', () => {+          this.initTranslateElement();+          resolve();+        });+        existing.addEventListener('error', () => reject());+        return;+      }++      (window as any).googleTranslateElementInit = () => {+        this.initTranslateElement();+        resolve();+      };++      const script = document.createElement('script');+      script.src =+        '//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit';+      script.async = true;+      script.setAttribute('data-google-translate', 'true');+      script.onerror = () => {+        this.translateScriptLoading = null;+        reject();+      };+      document.body.appendChild(script);+    });++    return this.translateScriptLoading;+  }++  private initTranslateElement(): void {+    try {+      if (+        typeof google === 'undefined' ||+        !google?.translate?.TranslateElement ||+        !document.getElementById('google_translate_element')+      ) {+        return;+      }+      if (document.querySelector('#google_translate_element .goog-te-combo')) {+        return;+      }+      new google.translate.TranslateElement(+        {+          pageLanguage: 'en',+          autoDisplay: false,+          layout: google.translate.TranslateElement.InlineLayout.SIMPLE+        },+        'google_translate_element'+      );+    } catch (_) {+      // Element may already be initialized+    }+  }+  /**   * Waits for Google Translate to initialize, then applies the saved locale.   */  private applyOnLoad(langCode: string): void {    this.setGoogTransCookie(langCode);-    const poll = setInterval(() => {-      const combo = document.querySelector('.goog-te-combo') as HTMLSelectElement;-      if (combo) {-        clearInterval(poll);-        combo.value = langCode;-        combo.dispatchEvent(new Event('change'));-      }-    }, 500);+    void this.ensureGoogleTranslateLoaded().then(() => {+      const poll = setInterval(() => {+        const combo = document.querySelector('.goog-te-combo') as HTMLSelectElement;+        if (combo) {+          clearInterval(poll);+          combo.value = langCode;+          combo.dispatchEvent(new Event('change'));+        }+      }, 500);-    setTimeout(() => clearInterval(poll), 10000);+      setTimeout(() => clearInterval(poll), 10000);+    });  }  get currentLocale(): string {    return this.localeSubject.value;  }  /**   * Sets the active locale:src/app/core/i18n/locale.service.ts@@ -145,17 +224,19 @@ export class LocaleService {  /**   * Re-applies the current locale after Angular route changes.   * Call from app.component on NavigationEnd.   */  reapplyTranslation(): void {    if (!this.isBrowser) return;    const locale = this.currentLocale;    if (locale && locale !== LOCALE_CONSTANTS.DEFAULT_LOCALE) {-      setTimeout(() => this.triggerGoogleTranslate(locale), 300);+      void this.ensureGoogleTranslateLoaded().then(() => {+        setTimeout(() => this.triggerGoogleTranslate(locale), 300);+      });    }  }  /**   * Clears saved locale on logout, re-detects browser language,   * and reloads if translation needs to change.   */  resetOnLogout(): void {

The profile used to wait for the browser, then fire posts, merch, welcome video, ads, and tracking in the same turn as the card. LCP is the banner and bio.

Paint the card first

  • Real profile markup renders when isBasicProfileLoaded is true, including during SSR. It no longer requires cs.isBrowser.
  • The hidden prerender-only h1/bio stub is used only when the server still does not have basic profile data.
  • The user GET uses CacheTypes.PRERENDERED on server/prerender; the browser still uses REFRESH_EVERY_N.
  • scheduleAfterFirstPaint() runs work after afterNextRender plus setTimeout(0). It no-ops on the server and during the prerender build.
  • Posts, merch, auto-follow, white-label referral, topic tags, welcome video, and profile-view tracking are scheduled there.
  • The banner is priority and no longer loading=lazy.
src/app/modules/shared/components/profile/profile.component.html@@ -1,9 +1,9 @@-@if (cs.isBrowser && isBasicProfileLoaded) {+@if (isBasicProfileLoaded) {  <div class="main-content">    <div class="inner-content">      @if (!isMyProfile && !isWhiteLabelWebsiteOrEmbedWLSMode) {        <div class="myprofile-head profile-head">          @if (            authService?.isLogin ||            (!authService?.isLogin && !isWhiteLabelWebsiteOrEmbedWLSMode)          ) {src/app/modules/shared/components/profile/profile.component.html@@ -93,23 +93,23 @@          class="profile-card-banner"          [ngClass]="{ 'default-banner-img': !userProfile?.banner_image_url }"        >          @if (userProfile?.banner_image_url) {            <img              alt="banner-image"              appDisableRightClick              appMobileOptimization+              [priority]="true"              [originalUrl]="userProfile?.banner_image_url"              [src]="userProfile?.banner_image_url"              [width]="800"              [height]="400"              [format]="'webp'"              class="img-fluid"-              loading="lazy"              decoding="async"            />          }          <div class="profile-card-action">            @if (              isMyProfile &&              (permissionKeys.EDIT_MY_PREFERENCES                | hasPermission: PERMISSION_TYPE.READ : truesrc/app/modules/shared/components/profile/profile.component.html@@ -5683,14 +5694,14 @@    #unlockUserModal    [headerLabel]="PROFILE_CONSTANT.UNLOCK_USER.HEADER"    [bodyContent]="PROFILE_CONSTANT.UNLOCK_USER.MESSAGE"    [confirmBtnLabel]="CommonEnums.OK"    [modalOptions]="{ backdrop: 'static' }"  ></app-alert-modal>}-@if (!cs.isBrowser) {+@if (!cs.isBrowser && !isBasicProfileLoaded) {  <div class="opacity-0 only-for-prerender">    <h1>{{ userProfile?.first_name }} {{ userProfile?.last_name }}</h1>    <p>{{ userProfile?.bio }}</p>  </div>}

scheduleAfterFirstPaint and the user GET

This hunk also appears on Timeouts. Same block adds the deferred, timed-out profile-view POST.

src/app/modules/shared/components/profile/profile.component.ts@@ -16,17 +16,18 @@ import {  Inject,  Input,  OnDestroy,  OnInit,  TemplateRef,  ViewChild,  DOCUMENT,  input,-  viewChild+  viewChild,+  afterNextRender} from '@angular/core';import { Store } from '@ngrx/store';import { NgbModal } from '@ng-bootstrap/ng-bootstrap';import { APIENDPOINT, API_URLS, CREATE_POST_TYPE } from '@core/api-url.enum';import { CommonService } from '@shared/services/common.service';import { ApiCallService } from '@services/api-call.service';import { CacheService } from '@shared/services/cache.service';import * as fromUserProfile from '@shared/user-profile.reducer';src/app/modules/shared/components/profile/profile.component.ts@@ -1251,43 +1250,45 @@ export class ProfileComponent implements OnInit, AfterViewInit, OnDestroy {      ) {        this.isPrivate = !is_follower;      } else {        this.isPrivate = false;      }    }    if (this.cs.isBrowser) {-      if (this.authService.isLogin && !this.userProfile?.is_private) {-        this.cs.automaticFollowUser();-        if (this.cs.isWhiteLabelWeb) {-          this.triggerWhiteLabelReferral();+      this.scheduleAfterFirstPaint(() => {+        if (this.authService.isLogin && !this.userProfile?.is_private) {+          this.cs.automaticFollowUser();+          if (this.cs.isWhiteLabelWeb) {+            this.triggerWhiteLabelReferral();+          }        }-      }-      this.userPosts = [];-      this.loadedPostPageNo = Application.ZERO;-      if (!this.isPrivate) {-        this.getMerchandiseListOfUser();-        if (-          this.paramsTab &&-          this.paramsTab !== PROFILE_CONSTANT.WELCOME_VIDEO-        ) {-          this.filterPost(this.paramsTab);-          this.paramsTab = '';-        } else if (-          this.filterPostType &&-          this.filterPostType !== POST_TYPE_FILTER_LABELS.ALL_TYPES_VALUE-        ) {-          this.filterPost(this.filterPostType);+        this.userPosts = [];+        this.loadedPostPageNo = Application.ZERO;+        if (!this.isPrivate) {+          this.getMerchandiseListOfUser();+          if (+            this.paramsTab &&+            this.paramsTab !== PROFILE_CONSTANT.WELCOME_VIDEO+          ) {+            this.filterPost(this.paramsTab);+            this.paramsTab = '';+          } else if (+            this.filterPostType &&+            this.filterPostType !== POST_TYPE_FILTER_LABELS.ALL_TYPES_VALUE+          ) {+            this.filterPost(this.filterPostType);+          } else {+            this.getOtherPosts(this.userProfile?.gsi4pk);+          }        } else {-          this.getOtherPosts(this.userProfile?.gsi4pk);+          this.isLoader = false;        }-      } else {-        this.isLoader = false;-      }+      });    }  }  private triggerWhiteLabelReferral() {    const [recruiterPart, creatorPart] =      this.cs        .getValueFromLocalStorage(Application.RECRUITER_STORAGE_KEY)        ?.split('&') ?? [];src/app/modules/shared/components/profile/profile.component.ts@@ -1340,36 +1341,59 @@ export class ProfileComponent implements OnInit, AfterViewInit, OnDestroy {      name: `${user?.['data'].first_name} ${user?.['data'].last_name}`,      location: user?.['data'].general_location    };  }  getUserProfile(userId) {    this.isSuspendedOrPaused = false;    this.cs.setUserId(userId);+    const cacheType =+      this.platformService.isServer || this.platformService.isPrerendered+        ? CacheTypes.PRERENDERED+        : CacheTypes.REFRESH_EVERY_N;    this.apiCacheService-      .get(APIENDPOINT.user(userId), CacheTypes.REFRESH_EVERY_N)+      .get(APIENDPOINT.user(userId), cacheType)      .pipe(takeUntil(this.destroy$))      .subscribe({        next: (res) => {          this.handleUserProfileResponse(res);          if (!this.isMyProfile && this.cs.isBrowser) {-            this.apiCallService-              .post(APIENDPOINT.PROFILE_VIEW(res.data?.gsi4pk), {}, true)-              .pipe(takeUntil(this.destroy$))-              .subscribe();+            this.scheduleAfterFirstPaint(() => {+              this.apiCallService+                .post(APIENDPOINT.PROFILE_VIEW(res.data?.gsi4pk), {}, true)+                .pipe(+                  timeout(8000),+                  catchError(() => of(null)),+                  takeUntil(this.destroy$)+                )+                .subscribe();+            });          }        },        error: (err) => {          this.isBasicProfileLoaded = true;          this.handleUserProfileError(err, userId);        }      });  }+  /** Run non-critical profile work after the card/bio can paint (LCP). */+  private scheduleAfterFirstPaint(work: () => void): void {+    if (!this.cs.isBrowser || this.platformService.isPrerendered) {+      return;+    }+    afterNextRender(+      () => {+        setTimeout(work, 0);+      },+      { injector: this._injector }+    );+  }+  private handleUserProfileResponse(res: any) {    if (!res.status && (res?.data?.is_paused || res?.data?.is_suspended)) {      this.isUserFound = true;      this.isBasicProfileLoaded = true;      this.isLoader = false;      this.checkSuspendAndPauseStatus(res);    } else if (res.status) {      this.currentProfileUserData = res.data;src/app/modules/shared/components/profile/profile.component.ts@@ -1396,20 +1420,20 @@ export class ProfileComponent implements OnInit, AfterViewInit, OnDestroy {        if (this.userProfile.gsi4pk !== this.whiteLabelConfig.user.gsi4pk) {          this.selectTab({ id: this.Home_String.AWARDS });        }      }      this.checkPrivacy();      this.isUserFound = true;      this.isSuspendedOrPaused = false;-      // Initialize topic tags filter+      // Topic tags are non-critical — load after first paint.      if (this.cs.isBrowser) {        this.initTopicTagsFilterForm();-        this.fetchTopicTags();+        this.scheduleAfterFirstPaint(() => this.fetchTopicTags());      }      if (this.userProfile.is_deleted) {        this.router.navigate(['/']);      }      this.cs.setValueInocalStorage('gsi4pk', JSON.stringify(res.data.gsi4pk));      if (!this.isCreator && this.tabType === this.Home_String.POST_TYPE) {        this.tabType = 'following';      }src/app/modules/shared/components/profile/profile.component.ts@@ -1471,17 +1495,19 @@ export class ProfileComponent implements OnInit, AfterViewInit, OnDestroy {          (sub) => sub.sk === this.cs.currentSubscriptionPlan.sk        );        if (this.cs.status && this.authService.isAuthenticated()) {          this.showSelctedSubscription(selectedSub);          this.cs.status = false;        }      }      if (this.userProfile?.gsi4pk && !this.platformService.isPrerendered) {-        this.getWelcomeVideoMessage(this.userProfile.gsi4pk);+        this.scheduleAfterFirstPaint(() =>+          this.getWelcomeVideoMessage(this.userProfile.gsi4pk)+        );      }    } else {      this.isBasicProfileLoaded = true;      this.router.navigate(['/not-found']);    }  }  private handleUserProfileError(_err: any, userId: string) {

Welcome video and the right sidebar are heavy standalone components. Leaving them in the profile imports array puts them on the first JS of every profile view.

Welcome video and sidebar

  • Both are wrapped in @defer (on viewport; prefetch on idle), so Angular emits separate chunks.
  • They are removed from the profile imports array. That is required for @defer to lazy-load them.
  • The sidebar keeps a right-sidebar-skeleton placeholder. Welcome video has no placeholder.
src/app/modules/shared/components/profile/profile.component.ts@@ -220,21 +222,19 @@ import { FollowingsComponent } from '@shared/components/followings/followings.coimport { FundContributeModalComponent } from '@shared/components/fund-contribute-modal/fund-contribute-modal.component';import { FundraiserProgressSectionComponent } from '@shared/components/fundraiser-progress-section/fundraiser-progress-section.component';import { GiveawayResponseModal } from '@shared/components/giveaway-response-modal/giveaway-response-modal.component';import { LoaderComponent } from '@shared/components/loader/loader.component';import { PollOptionsComponent } from '@shared/components/poll-options/poll-options.component';import { PostCardFooterComponent } from '@shared/components/post-card/components/post-card-footer/post-card-footer.component';import { PostCardStatsComponent } from '@shared/components/post-card/components/post-card-stats/post-card-stats.component';import { SensitiveOverlayComponent } from '@shared/components/post-card/components/sensitive-overlay/sensitive-overlay.component';-import { ProfileSidebarComponent } from '@shared/components/profile-sidebar/profile-sidebar.component';import { UserImageComponent } from '@shared/components/user-image/user-image.component';import { VideoPlayerV2Component } from '@shared/components/video-player-v2/video-player-v2.component';import { CustomWelcomeModalTemplate } from '@shared/components/welcome-video-modal/welcome-video-modal.component';-import { WelcomeVideoPostComponent } from '@shared/components/welcome-video-post/welcome-video-post.component';import { AudioSliderPatchDirective } from '@shared/directive/audio-slider-patch.directive';import { DisableRightClickDirective } from '@shared/directive/disable-right-click.directive';import { IntersectionObserverDirective } from '@shared/directive/intersection-observer.directive';import { MobileBridgeFileInputDirective } from '@shared/directive/mobile-bridge-file-input.directive';import { ToolTipDirective } from '@shared/directive/tool-tip.directive';import { TranslateDirective } from '@shared/directive/translate.directive';import { AudioPlayer } from '@pipes/audio-player.pipe';import { CapitalizeFirstPipe } from '@pipes/capitalizeFirst.pipe';src/app/modules/shared/components/profile/profile.component.ts@@ -295,22 +295,20 @@ import { MobileOptimizationDirective } from '@app-shared/directives/mobile-optim    FundraiserProgressSectionComponent,    GiveawayResponseModal,    ImageOverlayComponent,    LoaderComponent,    PollOptionsComponent,    PostCardFooterComponent,    PostCardStatsComponent,    SensitiveOverlayComponent,-    ProfileSidebarComponent,    UserGroupsModalComponent,    UserImageComponent,    VideoPlayerV2Component,    CustomWelcomeModalTemplate,-    WelcomeVideoPostComponent,    AudioSliderPatchDirective,    DisableRightClickDirective,    IntersectionObserverDirective,    MobileBridgeFileInputDirective,    ToolTipDirective,    TranslateDirective,    AudioPlayer,    CapitalizeFirstPipe,

Template: @defer

src/app/modules/shared/components/profile/profile.component.html@@ -664,53 +664,55 @@                  <div class="post-content">                    @if (                      welcomeVideoMessageData &&                      welcomeVideoMessageData?.media_url &&                      ((welcomeVideoMessageData?.is_active &&                        !welcomeVideoMessageData?.admin_marked_disabled) ||                        isMyProfile)                    ) {-                      <app-welcome-video-post-                        [items]="welcomeVideoMessageData"-                        [userProfile]="userProfile"-                        [isMyProfile]="isMyProfile"-                        [isWhiteLabelWebsite]="-                          isWhiteLabelWebsiteOrEmbedWLSMode-                        "-                        [postTypeKey]="postTypeKey"-                        [scheduleTag]="scheduleTag"-                        [collabScheduleTag]="collabScheduleTag"-                        [isLikeDisabled]="isLikeDisabled"-                        [whiteLabelOwnerEamil]="whiteLabelOwnerEamil"-                        [ROUTE_PATHS]="ROUTE_PATHS.EDIT_WELCOME_MESSAGE"-                        (blockUser)="blockUser($event)"-                        (reportPost)="reportWelcomeVideo($event.item)"-                        (pinPost)="pinPost($event.item, $event.pinned)"-                        (onTranslationChanged)="-                          onTranslationChanged($event.item, $event.event)-                        "-                        (openCustomWelcomeModal)="openCustomWelcomeModal()"-                        (showDeleteWelcomeVideoDialog)="-                          showDeleteWelcomeVideoDialog()-                        "-                        (seeLessMore)="seeWelcomeMesLessMore($event)"-                        (onComments)="onComments($event.item, $event.isComment)"-                        (showLikes)="showLikes($event.item)"-                        (likePost)="likePost($event.event, $event.item)"-                        (sendTipModal)="sendTipModal($event.item)"-                        (callParentFn)="parentSubscribePlanFunction()"-                        (openReferralModal)="-                          openReferralModal(-                            $event.type,-                            $event.item,-                            $event.isShare-                          )-                        "-                      ></app-welcome-video-post>+                      @defer (on viewport; prefetch on idle) {+                        <app-welcome-video-post+                          [items]="welcomeVideoMessageData"+                          [userProfile]="userProfile"+                          [isMyProfile]="isMyProfile"+                          [isWhiteLabelWebsite]="+                            isWhiteLabelWebsiteOrEmbedWLSMode+                          "+                          [postTypeKey]="postTypeKey"+                          [scheduleTag]="scheduleTag"+                          [collabScheduleTag]="collabScheduleTag"+                          [isLikeDisabled]="isLikeDisabled"+                          [whiteLabelOwnerEamil]="whiteLabelOwnerEamil"+                          [ROUTE_PATHS]="ROUTE_PATHS.EDIT_WELCOME_MESSAGE"+                          (blockUser)="blockUser($event)"+                          (reportPost)="reportWelcomeVideo($event.item)"+                          (pinPost)="pinPost($event.item, $event.pinned)"+                          (onTranslationChanged)="+                            onTranslationChanged($event.item, $event.event)+                          "+                          (openCustomWelcomeModal)="openCustomWelcomeModal()"+                          (showDeleteWelcomeVideoDialog)="+                            showDeleteWelcomeVideoDialog()+                          "+                          (seeLessMore)="seeWelcomeMesLessMore($event)"+                          (onComments)="onComments($event.item, $event.isComment)"+                          (showLikes)="showLikes($event.item)"+                          (likePost)="likePost($event.event, $event.item)"+                          (sendTipModal)="sendTipModal($event.item)"+                          (callParentFn)="parentSubscribePlanFunction()"+                          (openReferralModal)="+                            openReferralModal(+                              $event.type,+                              $event.item,+                              $event.isShare+                            )+                          "+                        ></app-welcome-video-post>+                      }                    }                  </div>                }                @if (!isPrivate) {                  <div                    class="profile-tabsHead filter-post-container"                    [ngClass]="{                      'single-row': !(src/app/modules/shared/components/profile/profile.component.html@@ -5369,26 +5371,35 @@        <div class="sidebar-creator sidebar-profile scr-lg">          <app-right-sidebar-skeleton            [showAdSkeleton]="true"            [showViewAllButtonSkeleton]="true"          ></app-right-sidebar-skeleton>        </div>      }      @if (!isSidebarLoading) {-        <app-profile-sidebar-          [dataList]="merchandiseList"-          [userProfile]="userProfile"-          [selfProfile]="isMyProfile"-          [isStoreConnected]="(isStoreConnected || hasStoreItems) && !isPrivate"-          [showUpcomingEventCountdown]="showUpcomingEventCountdown"-          [upcomingEventDetails]="upcomingEventDetails"-          [creatorEventName]="creatorEventDetails?.[0]?.event_calendar_name"-          (goToDownloadableStore)="viewStore(Fields.downloadablesType)"-        ></app-profile-sidebar>+        @defer (on viewport; prefetch on idle) {+          <app-profile-sidebar+            [dataList]="merchandiseList"+            [userProfile]="userProfile"+            [selfProfile]="isMyProfile"+            [isStoreConnected]="(isStoreConnected || hasStoreItems) && !isPrivate"+            [showUpcomingEventCountdown]="showUpcomingEventCountdown"+            [upcomingEventDetails]="upcomingEventDetails"+            [creatorEventName]="creatorEventDetails?.[0]?.event_calendar_name"+            (goToDownloadableStore)="viewStore(Fields.downloadablesType)"+          ></app-profile-sidebar>+        } @placeholder {+          <div class="sidebar-creator sidebar-profile scr-lg">+            <app-right-sidebar-skeleton+              [showAdSkeleton]="true"+              [showViewAllButtonSkeleton]="true"+            ></app-right-sidebar-skeleton>+          </div>+        }      }    }  </div>} @else {  <app-profile-skeleton></app-profile-skeleton>}<div  #ViewSubscriptionModal="bs-modal"

Side requests fail closed

A hung merch, welcome-video, posts, ads, or topic-tags call used to keep the profile in a bad state or surface a raw error. Those calls now fail closed after 8 seconds.

  • timeout(8000) plus catchError on topic tags, profile-view POST, other posts, skyscraper ads, welcome video, and merchandise.
  • Error toasts only show when the response actually has a message.
  • The getUserProfile hunk below also contains the PRERENDERED cache-type change from Profile.

timeout(8000) on the side path

src/app/modules/shared/components/profile/profile.component.ts@@ -54,17 +55,18 @@ import {  forkJoin,  of} from 'rxjs';import {  map,  catchError,  distinctUntilChanged,  debounceTime,-  finalize+  finalize,+  timeout} from 'rxjs/operators';import { DialogBoxComponent } from '@shared/components/dialog-box/dialog-box.component';import { DashboardModuleService } from '@modules/dashboard/services/dashboard-module.service';import { Messages } from '@enums/messages.enums';import { UploadService } from '@services/upload.service';import { ActivatedRoute, Router, RouterLink } from '@angular/router';import { CommonModule } from '@angular/common';import { UnfollowDialogComponent } from '@shared/components/unfollow-dialog/unfollow-dialog.component';src/app/modules/shared/components/profile/profile.component.ts@@ -1197,30 +1195,31 @@ export class ProfileComponent implements OnInit, AfterViewInit, OnDestroy {      return;    }    this.apiCallService      .get(        `${API_URLS.postTopics}?creator_id=${this.userProfile.gsi4pk}`,        false      )-      .pipe(takeUntil(this.destroy$))+      .pipe(+        timeout(8000),+        catchError(() => of(null)),+        takeUntil(this.destroy$)+      )      .subscribe({        next: (res) => {-          if (res.status && res.data?.length) {+          if (res?.status && res.data?.length) {            this.topicTags = res.data.map((tag: any) => ({              sk: tag.sk,              title: tag.title,              pk: tag.pk,              id: tag.id            }));          }-        },-        error: (err) => {-          this.cs.showErrorMessage(err.message);        }      });  }  navigateToForum(url) {    if (this.authService.isAuthenticated()) {      const redirectUrl = this.isMyProfile        ? [url]src/app/modules/shared/components/profile/profile.component.ts@@ -1340,36 +1341,59 @@ export class ProfileComponent implements OnInit, AfterViewInit, OnDestroy {      name: `${user?.['data'].first_name} ${user?.['data'].last_name}`,      location: user?.['data'].general_location    };  }  getUserProfile(userId) {    this.isSuspendedOrPaused = false;    this.cs.setUserId(userId);+    const cacheType =+      this.platformService.isServer || this.platformService.isPrerendered+        ? CacheTypes.PRERENDERED+        : CacheTypes.REFRESH_EVERY_N;    this.apiCacheService-      .get(APIENDPOINT.user(userId), CacheTypes.REFRESH_EVERY_N)+      .get(APIENDPOINT.user(userId), cacheType)      .pipe(takeUntil(this.destroy$))      .subscribe({        next: (res) => {          this.handleUserProfileResponse(res);          if (!this.isMyProfile && this.cs.isBrowser) {-            this.apiCallService-              .post(APIENDPOINT.PROFILE_VIEW(res.data?.gsi4pk), {}, true)-              .pipe(takeUntil(this.destroy$))-              .subscribe();+            this.scheduleAfterFirstPaint(() => {+              this.apiCallService+                .post(APIENDPOINT.PROFILE_VIEW(res.data?.gsi4pk), {}, true)+                .pipe(+                  timeout(8000),+                  catchError(() => of(null)),+                  takeUntil(this.destroy$)+                )+                .subscribe();+            });          }        },        error: (err) => {          this.isBasicProfileLoaded = true;          this.handleUserProfileError(err, userId);        }      });  }+  /** Run non-critical profile work after the card/bio can paint (LCP). */+  private scheduleAfterFirstPaint(work: () => void): void {+    if (!this.cs.isBrowser || this.platformService.isPrerendered) {+      return;+    }+    afterNextRender(+      () => {+        setTimeout(work, 0);+      },+      { injector: this._injector }+    );+  }+  private handleUserProfileResponse(res: any) {    if (!res.status && (res?.data?.is_paused || res?.data?.is_suspended)) {      this.isUserFound = true;      this.isBasicProfileLoaded = true;      this.isLoader = false;      this.checkSuspendAndPauseStatus(res);    } else if (res.status) {      this.currentProfileUserData = res.data;src/app/modules/shared/components/profile/profile.component.ts@@ -1627,28 +1653,34 @@ export class ProfileComponent implements OnInit, AfterViewInit, OnDestroy {      cacheKey,      {}    );    const skyscraperAds$ = this.getSkyscraperAdsObservable(      SPONSORED_AD_CONSTANTS.REQUEST_FROM_PROFILE_FEED    );    forkJoin({-      otherPosts: otherPosts$,-      skyscraperAds: skyscraperAds$+      otherPosts: otherPosts$.pipe(+        timeout(8000),+        catchError(() => of({ status: false, data: [], message: null }))+      ),+      skyscraperAds: skyscraperAds$.pipe(+        timeout(8000),+        catchError(() => of([]))+      )    })      .pipe(takeUntil(this.destroy$))      .subscribe({        next: (results) => {          const res = results.otherPosts;-          if (res.status) {+          if (res?.status) {            // Store skyscraper ads            this.skyscraperAds = results.skyscraperAds;            // Merge ads with posts-            const updatedPosts = this.insertSkyscraperAds([...res.data]);+            const updatedPosts = this.insertSkyscraperAds([...(res.data || [])]);            this.isLoader = false;            this.userPosts = updatedPosts;            this.loadedPostPageNo = Application.ONE;            // this.userProfile.post_count = res.data.length;            this.postPages = res.LastEvaluatedKey || { pk: '', sk: '' };            if (this.authService.isAuthenticated()) {              this.fetchPostStatus();src/app/modules/shared/components/profile/profile.component.ts@@ -1657,23 +1689,29 @@ export class ProfileComponent implements OnInit, AfterViewInit, OnDestroy {            }            this.restoreUnlockReturnIfNeeded();            this.restoreScrolledPostIfNeeded();            this.isFirstLoad = false;          } else {            this.isLoader = false;            this.loadedPostPageNo = Application.ZERO;            this.postPages = { pk: '', sk: '' };-            this.cs.showErrorMessage(res.message);+            const message = res?.message;+            if (message) {+              this.cs.showErrorMessage(message);+            }          }        },        error: (err) => {          this.isLoader = false;          this.loadedPostPageNo = Application.ZERO;-          this.cs.showErrorMessage(err.message);+          const message = err?.error?.message ?? err?.message;+          if (message) {+            this.cs.showErrorMessage(message);+          }        }      });  }  onMentionUserSelect = (selection) => {    this.setTeggedUsers(selection);    return `@${selection.first_name} ${selection.last_name} `;  };

Welcome video and merch

src/app/modules/shared/components/profile/profile.component.ts@@ -2006,23 +2044,29 @@ export class ProfileComponent implements OnInit, AfterViewInit, OnDestroy {        post.description = post.cta_title;      }    });  }  getWelcomeVideoMessage(id: string): void {    const url = `${API_URLS.welcomeMessage}/${id}`;    this.apiCacheService      .get(url, CacheTypes.REFRESH_EVERY_N)-      .pipe(takeUntil(this.destroy$))+      .pipe(+        timeout(8000),+        catchError((err) => {+          this.handleWelcomeVideoError(err);+          return of(null);+        }),+        takeUntil(this.destroy$)+      )      .subscribe({        next: (res) => {-          this.handleWelcomeVideoResponse(res);-        },-        error: (err) => {-          this.handleWelcomeVideoError(err);+          if (res) {+            this.handleWelcomeVideoResponse(res);+          }        }      });  }  private handleWelcomeVideoResponse(res: any) {    if (res.status) {      this.welcomeVideoMessageData = {        ...res?.data,src/app/modules/shared/components/profile/profile.component.ts@@ -2036,17 +2080,20 @@ export class ProfileComponent implements OnInit, AfterViewInit, OnDestroy {      this.cs.isBrowser    ) {      this.openCustomWelcomeModal();      this.paramsTab = '';    }  }  private handleWelcomeVideoError(err: any) {-    this.cs.showErrorMessage(err.message);+    const message = err?.error?.message ?? err?.message;+    if (message) {+      this.cs.showErrorMessage(message);+    }  }  fetchStatusForSelectedPost(idx) {    this.userPosts.forEach((el, id) => {      if (idx === id && el.is_locked) {        el.media_url = el?.lock_status?.media_url          ? el?.lock_status?.media_url          : el.media_url;        el.is_locked = !el?.lock_status?.is_unlocked;src/app/modules/shared/components/profile/profile.component.ts@@ -4218,27 +4265,31 @@ export class ProfileComponent implements OnInit, AfterViewInit, OnDestroy {    if (!this.userProfile?.is_white_label || this.userProfile?.is_host_owner) {      this.isSidebarLoading = true;      this.apiCacheService        .get(          APIENDPOINT.getFeatureListOfUser(this.userProfile?.gsi4pk),          CacheTypes.REFRESH_EVERY_N        )        .pipe(+          timeout(8000),+          catchError((err) => {+            this.handleMerchandiseError(err);+            return of(null);+          }),          takeUntil(this.destroy$),          finalize(() => {            this.isSidebarLoading = false;          })        )        .subscribe({          next: (res) => {-            this.handleMerchandiseResponse(res);-          },-          error: (err) => {-            this.handleMerchandiseError(err);+            if (res) {+              this.handleMerchandiseResponse(res);+            }          }        });    }  }  private handleMerchandiseResponse(res: any) {    if (res.status) {      this.isStoreConnected = res.is_store_connected;src/app/modules/shared/components/profile/profile.component.ts@@ -4250,17 +4301,20 @@ export class ProfileComponent implements OnInit, AfterViewInit, OnDestroy {        this.checkHasStoreItems();      }    }  }  private handleMerchandiseError(err: any) {    this.merchandiseList = [];    this.hasStoreItems = false;-    this.cs.showErrorMessage(err.message);+    const message = err?.error?.message ?? err?.message;+    if (message) {+      this.cs.showErrorMessage(message);+    }  }  private checkHasStoreItems(): void {    const userId = this.userProfile?.gsi4pk;    if (!userId) {      return;    }

Smaller images, less layout shift

The mobile-optimization directive was generating a 2x srcset whenever the original was large enough. That wastes bytes on the LCP image and posters. The loaded skyscraper card is 500px tall; the skeleton was shorter.

  • 2x srcset is skipped for priority/LCP images. The size cap drops from 1000px to 640px.
  • The marketing header logo now requests 290×32 webp.
  • Both profile-skeleton and right-sidebar-skeleton give the ad slot a 500px min-height.
src/app/modules/profile/component/marketing-page-layout-container/header-layout/header-layout.component.html@@ -60,16 +60,19 @@            "            [src]="logoUrl"            [alt]="CONST.LOGO_ALT"            loading="eager"            decoding="async"            appDisableRightClick            appMobileOptimization            [originalUrl]="logoUrl"+            [width]="290"+            [height]="32"+            [format]="'webp'"          />        }      }    </div>    <nav class="header-layout__nav" aria-label="Header menu">      @for (item of menuItems; track item) {        @if (item.isWelcomeVideo) {src/app/shared/directives/mobile-optimization.directive.ts@@ -175,20 +175,22 @@ export class MobileOptimizationDirective    // Build srcset for responsive images - only add 2x if dimensions are small enough    let srcset = optimized;    try {      // Only generate 2x variant for high-DPI if:      // 1. Original width is known AND      // 2. Original is at least 2x the requested size AND      // 3. Requested size is reasonable (avoid excessive upscaling)+      // Skip 2x for LCP/priority images and posters — oversized srcset wastes bytes.      const shouldGenerate2x =+        !this.priority() &&        this._origWidth &&        this._origWidth >= (widthToRequest ?? 0) * 2 &&-        (widthToRequest ?? 0) < 1000; // Arbitrary limit to avoid huge requests+        (widthToRequest ?? 0) < 640;      if (shouldGenerate2x && !this.isPoster()) {        // Only generate 2x for regular images, not for posters        const optimized2x = this.mobileOptimizationService.getOptimizedImageUrl(          sourceUrlString,          widthToRequest ? Math.round((widthToRequest ?? 0) * 2) : undefined,          heightToRequest ? Math.round((heightToRequest ?? 0) * 2) : undefined,          qualityNum !== undefined ? qualityNum : undefined,

Ad skeleton min-height

src/app/components/skeleton/profile-skeleton/profile-skeleton.component.scss@@ -150,10 +150,18 @@  @include breakpoints(mini-ipad) {    margin-top: 12px;  }  &__ad {    width: 100%;    margin-top: 8px;    margin-bottom: 20px;+    // Match loaded skyscraper card (min-height 500px) to avoid desktop CLS.+    min-height: 500px;++    app-skeleton {+      display: block;+      width: 100%;+      min-height: 500px;+    }  }}src/app/components/skeleton/right-sidebar-skeleton/right-sidebar-skeleton.component.scss@@ -65,10 +65,18 @@    app-skeleton {      display: block;    }  }  &__ad {    width: 100%;    margin-bottom: 20px;+    // Match loaded skyscraper card (min-height 500px) to avoid desktop CLS.+    min-height: 500px;++    app-skeleton {+      display: block;+      width: 100%;+      min-height: 500px;+    }  }}

styles.scss imported the full Bootstrap barrel. Navbar, offcanvas, spinners, list-group, and placeholders never ship as used UI.

The curated stack

src/assets/scss/_bootstrap-partials.scss@@ -0,0 +1,38 @@+// Bootstrap SCSS diet: only modules this app uses (drops navbar, list-group,+// spinners, offcanvas, placeholders). Keep in sync with class usage audits.+@import 'bootstrap/scss/functions';+@import 'bootstrap/scss/variables';+@import 'bootstrap/scss/variables-dark';+@import 'bootstrap/scss/maps';+@import 'bootstrap/scss/mixins';+@import 'bootstrap/scss/utilities';++@import 'bootstrap/scss/root';+@import 'bootstrap/scss/reboot';+@import 'bootstrap/scss/type';+@import 'bootstrap/scss/images';+@import 'bootstrap/scss/containers';+@import 'bootstrap/scss/grid';+@import 'bootstrap/scss/tables';+@import 'bootstrap/scss/forms';+@import 'bootstrap/scss/buttons';+@import 'bootstrap/scss/transitions';+@import 'bootstrap/scss/dropdown';+@import 'bootstrap/scss/button-group';+@import 'bootstrap/scss/nav';+@import 'bootstrap/scss/card';+@import 'bootstrap/scss/accordion';+@import 'bootstrap/scss/breadcrumb';+@import 'bootstrap/scss/pagination';+@import 'bootstrap/scss/badge';+@import 'bootstrap/scss/alert';+@import 'bootstrap/scss/progress';+@import 'bootstrap/scss/close';+@import 'bootstrap/scss/toasts';+@import 'bootstrap/scss/modal';+@import 'bootstrap/scss/tooltip';+@import 'bootstrap/scss/popover';+@import 'bootstrap/scss/carousel';++@import 'bootstrap/scss/helpers';+@import 'bootstrap/scss/utilities/api';src/styles.scss@@ -1,10 +1,9 @@@use 'sass:map';-@use 'bootstrap/scss/bootstrap';@use 'tools' as *;@use 'main';@use './app/components/skeleton/skeleton';@font-face {  font-family: 'Satoshi';  src: url('./assets/fonts/Satoshi-Regular.otf') format('opentype');  font-weight: 400;src/assets/scss/_icon-lite.scss@@ -4,17 +4,17 @@  src:  url('../fonts/icomoon-lite.eot?r4a7ko');  src:  url('../fonts/icomoon-lite.eot?r4a7ko#iefix') format('embedded-opentype'),    url('../fonts/icomoon-lite.woff2?r4a7ko') format('woff2'),    url('../fonts/icomoon-lite.ttf?r4a7ko') format('truetype'),    url('../fonts/icomoon-lite.woff?r4a7ko') format('woff'),    url('../fonts/icomoon-lite.svg?r4a7ko#icomoon-lite') format('svg');  font-weight: normal;  font-style: normal;-  font-display: block;+  font-display: swap;}[class^="icon-lite"], [class*=" icon-lite"] {  /* use !important to prevent issues with browser extensions that change fonts */  font-family: 'icomoon-lite' !important;  speak: never;  font-style: normal;  font-weight: normal;

angular.json and generated tenants

The generate-whitelabel hunk also appears on S3. Same block forces critical-CSS inlining on prod/qa.

angular.json@@ -70,16 +70,17 @@              {                "glob": "**/*",                "input": "node_modules/flag-icon-css/flags",                "output": "/flags"              }            ],            "styles": [              "node_modules/@ng-select/ng-select/themes/default.theme.css",+              "src/assets/scss/_bootstrap-partials.scss",              "src/styles.scss",              "src/assets/scss/_white-label-theme.scss",              {                "input": "node_modules/intl-tel-input/build/css/intlTelInput.css",                "bundleName": "lazy-intl-tel-input",                "inject": false              },              {angular.json@@ -369,16 +370,17 @@            ],            "styles": [              "node_modules/intl-tel-input/build/css/intlTelInput.css",              "node_modules/@angular/material/prebuilt-themes/indigo-pink.css",              "node_modules/@ng-select/ng-select/themes/default.theme.css",              "node_modules/ngx-bootstrap/datepicker/bs-datepicker.css",              "node_modules/ngx-owl-carousel-o/lib/styles/prebuilt-themes/owl.carousel.min.css",              "node_modules/ngx-owl-carousel-o/lib/styles/prebuilt-themes/owl.theme.default.min.css",+              "src/assets/scss/_bootstrap-partials.scss",              "src/styles.scss",              "node_modules/quill/dist/quill.snow.css",              "node_modules/ngx-toastr/toastr.css",              "node_modules/primeicons/primeicons.css",              "node_modules/quill-emoji/dist/quill-emoji.css",              "node_modules/@ctrl/ngx-emoji-mart/picker.css"            ],            "stylePreprocessorOptions": {scripts/generate-whitelabel.js@@ -688,25 +688,59 @@ function createProjectConfig(userName, baseProject, targetEnv = null) {  }    // Application builder writes browser/ and server/ under this parent path  if (newProject.architect && newProject.architect.build && newProject.architect.build.options) {    newProject.architect.build.options.outputPath = `dist/${userName}`;        // Add white-label-theme-entry.scss to styles array    if (newProject.architect.build.options.styles && Array.isArray(newProject.architect.build.options.styles)) {+      const styles = newProject.architect.build.options.styles;+      const bootstrapPartialsPath = 'src/assets/scss/_bootstrap-partials.scss';+      const mainStylesPath = 'src/styles.scss';+      if (!styles.includes(bootstrapPartialsPath)) {+        const mainIdx = styles.indexOf(mainStylesPath);+        if (mainIdx >= 0) {+          styles.splice(mainIdx, 0, bootstrapPartialsPath);+        } else {+          styles.unshift(bootstrapPartialsPath);+        }+        console.log(`✅ Added bootstrap-partials.scss to styles for ${userName}`);+      }      const whiteLabelThemePath = 'src/assets/scss/_white-label-theme.scss';      // Only add if it doesn't already exist-      if (!newProject.architect.build.options.styles.includes(whiteLabelThemePath)) {-        newProject.architect.build.options.styles.push(whiteLabelThemePath);+      if (!styles.includes(whiteLabelThemePath)) {+        styles.push(whiteLabelThemePath);        console.log(`✅ Added white-label-theme-entry.scss to styles for ${userName}`);      }    }  }+  // Keep critical-CSS inlining on prod/qa (media=print + onload is intentional, not a duplicate stylesheet).+  const buildConfigs = newProject.architect?.build?.configurations;+  if (buildConfigs) {+    for (const name of ['production', 'qa']) {+      const cfg = buildConfigs[name];+      if (!cfg) continue;+      if (cfg.optimization === true || cfg.optimization == null) {+        cfg.optimization = {+          scripts: true,+          styles: { minify: true, inlineCritical: true },+          fonts: true+        };+      } else if (typeof cfg.optimization === 'object') {+        cfg.optimization.styles = {+          minify: true,+          ...(cfg.optimization.styles || {}),+          inlineCritical: true+        };+      }+    }+  }+  retargetArchitectProjectReferences(newProject.architect, userName);    // Determine which environments to process  const envsToProcess = targetEnv ? [targetEnv] : CONFIG.environments;  // Update ALL build configurations with proper fileReplacements  if (newProject.architect && newProject.architect.build && newProject.architect.build.configurations) {    // First, update existing configurations (production, development, qa, staging, etc.)

Uploads stamped Cache-Control: public,max-age=31536000,immutable on the whole dist, including index.html and prerendered HTML. Browsers could keep a stale shell for a year.

rclone-upload-dist.sh

  • Two rclone copy passes: everything except HTML as immutable, then only index.html / **/index.html as max-age=0,must-revalidate.
  • buildspec and the three tenant upload scripts call this helper.
  • Concurrency stays conservative (transfers=2, checkers=4).
  • explore, splash, manifest, favicon, and sitemaps stay excluded.
deployment_script/rclone-upload-dist.sh@@ -0,0 +1,56 @@+#!/usr/bin/env bash+# Upload a browser dist folder to S3 with split Cache-Control:+# - HTML (index.html / prerendered **/index.html): no-cache / must-revalidate+# - Hashed assets and everything else: long-lived immutable+set -euo pipefail++OUTPUT_PATH="${1:?usage: rclone-upload-dist.sh <outputPath> <s3Bucket>}"+BUCKET="${2:?usage: rclone-upload-dist.sh <outputPath> <s3Bucket>}"++COMMON_EXCLUDES=(+  --exclude "explore/**"+  --exclude "assets/splashscreen/**"+  --exclude "assets/Json/manifest.json"+  --exclude "assets/Json/splashscreens.json"+  --exclude "assets/favicon/**"+  --exclude "favicon.ico"+  --exclude "sitemap.xml"+  --exclude "sitemap-*.xml"+)++LOG_FILE="${RCLONE_LOG_FILE:-rclone-${BUCKET}.log}"++TRANSFERS="${RCLONE_TRANSFERS:-2}"+CHECKERS="${RCLONE_CHECKERS:-4}"++echo "===== UPLOADING ASSETS (immutable) TO s3:$BUCKET ====="+rclone copy "$OUTPUT_PATH" ":s3:$BUCKET" \+  "${COMMON_EXCLUDES[@]}" \+  --exclude "**/*.html" \+  --header-upload "Cache-Control:public,max-age=31536000,immutable" \+  --s3-env-auth \+  --checksum \+  --transfers="$TRANSFERS" \+  --checkers="$CHECKERS" \+  --retries=3 \+  --retries-sleep=5s \+  --log-level="${RCLONE_LOG_LEVEL:-INFO}" \+  --log-file="$LOG_FILE"++echo "===== UPLOADING HTML (must-revalidate) TO s3:$BUCKET ====="+rclone copy "$OUTPUT_PATH" ":s3:$BUCKET" \+  --include "index.html" \+  --include "**/index.html" \+  --exclude "*" \+  --header-upload "Cache-Control:public,max-age=0,must-revalidate" \+  --s3-env-auth \+  --checksum \+  --transfers="$TRANSFERS" \+  --checkers="$CHECKERS" \+  --retries=3 \+  --retries-sleep=5s \+  --log-level="${RCLONE_LOG_LEVEL:-INFO}" \+  --log-file="$LOG_FILE"++echo "===== RCLONE LOG ($BUCKET) ====="+cat "$LOG_FILE" || true

Callers

buildspec-s3.yml@@ -39,30 +39,21 @@ phases:          echo "===== $BUILD_ENV ENV - SKIPPING NPM AUDIT ====="        fi      - npm run $BUILD_ENV      # Replace API endpoint in shell script      - sed -i "s|{{API_ENDPOINT}}|$API_ENDPOINT|g" deployment_script/whitelabel_invalidate.sh-      # Upload static assets (non-HTML) with long-term cache-      - echo "===== UPLOADING STATIC ASSETS TO S3 (LONG CACHE) ====="+      # Upload: hashed assets immutable; HTML must-revalidate+      - echo "===== UPLOADING DIST TO S3 (SPLIT CACHE) ====="      - |-        rclone copy dist/TopFans/browser/ :s3:$ENV_S3 \-          --exclude "explore/**" \-          --header-upload "Cache-Control:public,max-age=31536000,immutable" \-          --s3-env-auth \-          --checksum \-          --log-level=INFO \-          --log-file=rclone-static.log--      # Show upload logs-      - echo "===== RCLONE STATIC ASSETS LOG ====="-      - cat rclone-static.log+        RCLONE_LOG_FILE=rclone-static.log \+          ./deployment_script/rclone-upload-dist.sh dist/TopFans/browser "$ENV_S3"      # Perform full CloudFront invalidation      - |        echo "===== INVALIDATING CLOUDFRONT: /* ====="        aws cloudfront create-invalidation \          --distribution-id "$ENV_CLOUDFRONT" \          --paths "/*" \          --query 'Invalidation.Id' \deployment_script/build_and_upload.sh@@ -28,34 +28,18 @@ for row in "${tenants[@]}"; do  echo "===== BUILDING FOR BUCKET: $bucket ====="  echo ">>> DEBUG: command=$command"    # Run build - will fail the entire script if this fails  eval "$command"  echo "===== UPLOADING WITH RCLONE TO $bucket ====="-  rclone copy "$outputPath" ":s3:$bucket" \-    --exclude "explore/**" \-    --exclude "assets/splashscreen/**" \-    --exclude "assets/Json/manifest.json" \-    --exclude "assets/Json/splashscreens.json" \-    --exclude "assets/favicon/**" \-    --exclude "favicon.ico" \-    --exclude "sitemap.xml" \-    --exclude "sitemap-*.xml" \-    --header-upload "Cache-Control:public,max-age=31536000,immutable" \-    --s3-env-auth \-    --checksum \-    --log-level=DEBUG \-    --log-file="rclone-$bucket.log"---  echo "===== RCLONE LOG OUTPUT ($bucket) ====="-  cat "rclone-$bucket.log"+  RCLONE_LOG_FILE="rclone-$bucket.log" RCLONE_LOG_LEVEL=DEBUG \+    ./deployment_script/rclone-upload-dist.sh "$outputPath" "$bucket"  echo "============================================="  userName=$(echo "$row" | jq -r '.userName // empty')  userId=$(echo "$row" | jq -r '.userId // empty')  environment=$(echo "$row" | jq -r '.environment // empty')  if [[ -z "$userId" || -z "$userName" || -z "$environment" ]]; then    echo "ERROR: missing userId/userName/environment for $bucket"    exit 1deployment_script/ondemand-build-and-upload.sh@@ -10,33 +10,18 @@ cd "$REPO_ROOT"node scripts/generate-whitelabel.js --env "$WL_ENV" --users "$WL_USERNAME" --user-id "$WL_USER_ID"npm run "prerender-all-${WL_USERNAME}-${WL_ENV}"output_path="${REPO_ROOT}/dist/${WL_USERNAME}/browser"[[ -d "$output_path" ]] || { echo "ERROR: missing build output at $output_path"; exit 1; }-rclone copy "$output_path" ":s3:${WL_S3_BUCKET}" \-  --exclude "explore/**" \-  --exclude "assets/splashscreen/**" \-  --exclude "assets/Json/manifest.json" \-  --exclude "assets/Json/splashscreens.json" \-  --exclude "assets/favicon/**" \-  --exclude "favicon.ico" \-  --exclude "sitemap.xml" \-  --exclude "sitemap-*.xml" \-  --header-upload "Cache-Control:public,max-age=31536000,immutable" \-  --s3-env-auth \-  --checksum \-  --log-level=INFO \-  --transfers=2 \-  --checkers=4 \-  --retries=3 \-  --retries-sleep=5s+RCLONE_LOG_FILE="rclone-${WL_S3_BUCKET}.log" \+  "$REPO_ROOT/deployment_script/rclone-upload-dist.sh" "$output_path" "$WL_S3_BUCKET"node scripts/request-wls-build.mjs \  --env="$WL_ENV" \  --user="$WL_USERNAME" \  --userId="$WL_USER_ID" \  --tasks=CONFIG,SPLASH,APP_ICON,MANIFEST,SITEMAPif [[ -n "${WL_CF_DIST:-}" ]]; thendeployment_script/parallel-build-and-upload.sh@@ -77,36 +77,21 @@ process_tenant() {    echo "❌ No output at $out_abs after ${WAIT_FOR_OUTPUT_SECONDS}s"    ls -la "$REPO_ROOT" || true    popd >/dev/null    return 21  fi  echo "Output ready:"  ls -la "$out_abs" || true-  # Upload with rclone (conservative concurrency)+  # Upload with rclone (assets immutable; HTML must-revalidate)  echo "===== UPLOADING: $bucket from $out_abs ====="  set +e-  rclone copy "$out_abs" ":s3:$bucket" \-    --exclude "explore/**" \-    --exclude "assets/splashscreen/**" \-    --exclude "assets/Json/manifest.json" \-    --exclude "assets/Json/splashscreens.json" \-    --exclude "assets/favicon/**" \-    --exclude "favicon.ico" \-    --exclude "sitemap.xml" \-    --exclude "sitemap-*.xml" \-    --header-upload "Cache-Control:public,max-age=31536000,immutable" \-    --s3-env-auth \-    --checksum \-    --log-level=INFO \-    --transfers=2 \-    --checkers=4 \-    --retries=3 \-    --retries-sleep=5s+  RCLONE_LOG_FILE="rclone-$bucket.log" \+    "$REPO_ROOT/deployment_script/rclone-upload-dist.sh" "$out_abs" "$bucket"  rc=$?  set -e  popd >/dev/null  if [[ $rc -ne 0 ]]; then    echo "❌ RCLONE FAILED for $bucket (rc=$rc)"    return "$rc"  fi

Inline critical CSS on white-label prod/qa

Same generate-whitelabel.js hunk as Bootstrap. Repeated because the two edits solve different problems.

scripts/generate-whitelabel.js@@ -688,25 +688,59 @@ function createProjectConfig(userName, baseProject, targetEnv = null) {  }    // Application builder writes browser/ and server/ under this parent path  if (newProject.architect && newProject.architect.build && newProject.architect.build.options) {    newProject.architect.build.options.outputPath = `dist/${userName}`;        // Add white-label-theme-entry.scss to styles array    if (newProject.architect.build.options.styles && Array.isArray(newProject.architect.build.options.styles)) {+      const styles = newProject.architect.build.options.styles;+      const bootstrapPartialsPath = 'src/assets/scss/_bootstrap-partials.scss';+      const mainStylesPath = 'src/styles.scss';+      if (!styles.includes(bootstrapPartialsPath)) {+        const mainIdx = styles.indexOf(mainStylesPath);+        if (mainIdx >= 0) {+          styles.splice(mainIdx, 0, bootstrapPartialsPath);+        } else {+          styles.unshift(bootstrapPartialsPath);+        }+        console.log(`✅ Added bootstrap-partials.scss to styles for ${userName}`);+      }      const whiteLabelThemePath = 'src/assets/scss/_white-label-theme.scss';      // Only add if it doesn't already exist-      if (!newProject.architect.build.options.styles.includes(whiteLabelThemePath)) {-        newProject.architect.build.options.styles.push(whiteLabelThemePath);+      if (!styles.includes(whiteLabelThemePath)) {+        styles.push(whiteLabelThemePath);        console.log(`✅ Added white-label-theme-entry.scss to styles for ${userName}`);      }    }  }+  // Keep critical-CSS inlining on prod/qa (media=print + onload is intentional, not a duplicate stylesheet).+  const buildConfigs = newProject.architect?.build?.configurations;+  if (buildConfigs) {+    for (const name of ['production', 'qa']) {+      const cfg = buildConfigs[name];+      if (!cfg) continue;+      if (cfg.optimization === true || cfg.optimization == null) {+        cfg.optimization = {+          scripts: true,+          styles: { minify: true, inlineCritical: true },+          fonts: true+        };+      } else if (typeof cfg.optimization === 'object') {+        cfg.optimization.styles = {+          minify: true,+          ...(cfg.optimization.styles || {}),+          inlineCritical: true+        };+      }+    }+  }+  retargetArchitectProjectReferences(newProject.architect, userName);    // Determine which environments to process  const envsToProcess = targetEnv ? [targetEnv] : CONFIG.environments;  // Update ALL build configurations with proper fileReplacements  if (newProject.architect && newProject.architect.build && newProject.architect.build.configurations) {    // First, update existing configurations (production, development, qa, staging, etc.)

The performance contract

  • Bootstrap is the curated partial, not the full barrel.
  • Google Translate loads from LocaleService, not index.html.
  • GTM/GA4 idle-load stays in gtag.service.ts.
  • Follow-up, not in this PR: source-map-explorer on the production bundle, Firebase modular imports, leftover Bootstrap JS.
  • polyfills.ts only adds a comment: keep zone.js plus @angular/localize.
knowledge/stack.md@@ -59,8 +59,15 @@ Clone an existing feature module (wallet, messages, monetize). Lazy route in `ap## Definition of done (UI slice)1. HTTP uses `ApiCacheService` / `ApiCallService` (no raw `HttpClient` in pages)2. Lazy route registered; path from `ROUTE_PATHS`3. Constants file for user-facing strings4. Matches [web-conventions.md](web-conventions.md)5. Talks to the existing backend via `environment.baseURL`; no assumed endpoints6. `tsc` and lint clean on touched files++## Performance notes++- Bootstrap SCSS is a curated partial stack in `src/assets/scss/_bootstrap-partials.scss` (listed in `angular.json` styles before `styles.scss`), not the full `bootstrap/scss/bootstrap` barrel.+- Google Translate `element.js` loads via `LocaleService.ensureGoogleTranslateLoaded()` (language switcher open or non-`en` restore), not from `index.html`.+- GTM/GA4 idle-load lives in `gtag.service.ts`. Duplicate container IDs are a tenant/`environment` config cleanup, not a second loader.+- Bundle audit (follow-up PRs): `ng build --configuration=production --stats-json` then `npx source-map-explorer dist/TopFans/browser/*.js` — prioritize Firebase modular imports and unused Bootstrap JS.src/polyfills.ts@@ -11,16 +11,20 @@ * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. * * Learn more in https://angular.io/guide/browser-support *//*************************************************************************************************** * BROWSER POLYFILLS+ *+ * Keep only zone.js + @angular/localize on the critical path. Do not add IE/legacy shims+ * or heavy polyfills here without an ADR (zoneless would also need an ADR). Optional+ * Node shims below are for browser libs that expect `global` / `process`. */import 'zone.js'; // Included with Angular CLI.import '@angular/localize/init';//  import 'intersection-observer';(window as any).global = window;

Watch on review

CSS
Any screen still using dropped Bootstrap modules: navbar, list-group, offcanvas, spinner-*, placeholders.
Translate
Non-English first load: element.js starts from LocaleService, so there can be a short untranslated flash even though the googtrans cookie is set early.
Timeouts
8s timeouts swallow slow merch / welcome-video / posts / ads. The user sees an empty section, not an error, unless the body has a message.
@defer
No welcome-video placeholder. SSR/hydration and below-the-fold first visit should be clicked through.
SEO
Organization JSON-LD and iPhone 17 splash images are gone from index.html. Confirm that was intended.
Cache
After deploy, confirm index.html and prerendered routes send must-revalidate, not immutable.

Built from git range 17dd0e9a46...cb09e9d27b (pull/10977).