Skip to content

Commit 7ec6c43

Browse files
author
Fortinbra
committed
feat: initial page load performance optimizations (Feature 117)
- nginx: immutable 1-year cache for _framework/ (prod + demo) - API: Brotli+Gzip response compression (CompressionLevel.Fastest) - API: inline config JSON injection in index.html (eliminates /api/v1/config fetch) - Client: IL trimming (PublishTrimmed, TrimMode=partial) - 50% payload reduction - Client: HybridGlobalization - 70% ICU data savings - Client: PWA service worker for framework asset caching on return visits - Client: skeleton loading screen replacing spinner (header+sidebar+content) - Client: parallelize Calendar page 5 API calls via Task.WhenAll - Tests: fix 345 pre-existing bUnit failures (missing service registrations) - Measured payload: 3.32 MB Brotli (down from 6.68 MB untrimmed)
1 parent 0a527f3 commit 7ec6c43

25 files changed

+795
-82
lines changed

docs/117-initial-page-load-performance.md

Lines changed: 54 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Feature 117: Initial Page Load Performance
2-
> **Status:** Planning
2+
> **Status:** Done
33
44
## Overview
55

@@ -283,45 +283,69 @@ This is marginal but essentially free to implement.
283283
## Implementation Plan
284284

285285
### Phase 1: Quick Wins — Caching & Compression
286-
1. Fix nginx `_framework/` cache headers (immutable, 1-year)
287-
2. Add `UseResponseCompression` with Brotli to API
288-
3. Preload `api/v1/config` via `<link rel="preload">` in `index.html`
289-
4. Embed client config as inline JSON in `index.html` (eliminate config fetch)
286+
1. [x] Fix nginx `_framework/` cache headers (immutable, 1-year)
287+
2. [x] Add `UseResponseCompression` with Brotli to API
288+
3. [x] Preload `api/v1/config` via `<link rel="preload">` in `index.html`
289+
4. [x] Embed client config as inline JSON in `index.html` (eliminate config fetch)
290290

291291
### Phase 2: Payload Reduction
292-
5. Enable IL trimming (`PublishTrimmed`, `TrimMode=partial`) for Release builds
293-
6. Evaluate and enable hybrid globalization or invariant mode
294-
7. Identify and configure lazy-loaded assemblies for non-landing-page features
295-
8. Verify no trimming/AOT regressions via E2E tests
292+
5. [x] Enable IL trimming (`PublishTrimmed`, `TrimMode=partial`) for Release builds
293+
6. [x] Enable hybrid globalization (`HybridGlobalization=true`)
294+
7. [ ] ~~Identify and configure lazy-loaded assemblies~~ — Deferred: requires splitting Client into separate Razor Class Libraries (all pages compile into one DLL; Contracts/Shared are lightweight)
295+
8. [x] Verify no trimming regressions — Release publish succeeds; all 5,119 unit/integration tests pass
296296

297297
### Phase 3: Service Worker & Offline Cache
298-
9. Add Blazor PWA service worker for framework caching
299-
10. Configure service worker cache versioning strategy
300-
11. Test offline-first behavior for cached framework files
298+
9. [x] Add Blazor PWA service worker for framework caching
299+
10. [x] Configure service worker cache versioning strategy
300+
11. [x] Offline-first verified — service worker caches all `_framework/` assets via manifest; API/index.html excluded from cache
301301

302302
### Phase 4: Auth Flow Optimization
303-
12. Verify silent token renewal is working (hidden iframe)
304-
13. Add skeleton screens for post-auth page loading
305-
14. Prefetch landing page data during auth resolution (return visits)
306-
15. Explore early JS-based auth redirect (before WASM loads)
303+
12. [x] Verify silent token renewal is working — Already implemented via `TokenRefreshHandler` (automatic 401 → token refresh → retry)
304+
13. [x] Add skeleton screens for post-auth page loading
305+
14. [x] Prefetch landing page data during auth resolution — Parallelized Calendar's 5 sequential API calls (`LoadAccounts`, `LoadCategories`, `LoadCalendarData`, `LoadPastDueItems`, `LoadBudgetSummary`) via `Task.WhenAll`
306+
15. [ ] ~~Explore early JS-based auth redirect~~ — Explored, deferred: sessionStorage is per-tab (empty on new tabs); constructing OIDC authorize URL with PKCE in plain JS duplicates fragile logic; service worker already eliminates WASM download on return visits
307307

308308
### Phase 5: Measurement & Validation
309-
16. Add Lighthouse CI or manual Lighthouse audit to track Core Web Vitals (LCP, FID, CLS)
310-
17. Measure before/after for each phase on both fast connections and throttled (Slow 3G)
311-
18. Document final payload sizes and load times in this feature doc
309+
16. [ ] Add Lighthouse CI or manual Lighthouse audit to track Core Web Vitals (LCP, FID, CLS) — deferred to post-deploy
310+
17. [ ] Measure before/after on throttled connections — deferred to post-deploy
311+
18. [x] Document final payload sizes (see Measured Results below)
312312

313313
---
314314

315-
## Success Metrics
316-
317-
| Metric | Current (Estimated) | Target |
318-
|--------|---------------------|--------|
319-
| First Contentful Paint (cold) | 3–5s | < 2s |
320-
| Time to Interactive (cold) | 5–10s | < 3.5s |
321-
| Time to Interactive (cached/return) | 3–5s | < 1.5s |
322-
| WASM payload (compressed) | ~8–12 MB | < 5 MB |
323-
| Post-auth data render | 1–2s | < 500ms |
324-
| Lighthouse Performance score | ~40–60 | > 75 |
315+
## Measured Results (Release Publish, 2026-03-17)
316+
317+
### Payload Sizes
318+
319+
| Metric | Before (no trimming) | After (trimmed) | Improvement |
320+
|--------|---------------------|-----------------|-------------|
321+
| `_framework/` total (all files) | 37.92 MB (642 files) | 19.02 MB (195 files) | **50% reduction** |
322+
| Brotli transfer size | 6.68 MB | 3.32 MB | **50.3% reduction** |
323+
| Largest files (Brotli) || dotnet.native 954 KB, CoreLib 541 KB, Client.dll 365 KB ||
324+
| ICU data (HybridGlobalization) | ~2 MB single file | 3 shards totaling ~600 KB | **70% reduction** |
325+
326+
### Optimizations Applied
327+
328+
| Optimization | Impact |
329+
|-------------|--------|
330+
| IL Trimming (`TrimMode=partial`) | 50% payload reduction |
331+
| HybridGlobalization | ~1.4 MB ICU savings |
332+
| Response Compression (Brotli) | 60–80% API response reduction |
333+
| nginx immutable caching | Zero revalidation on return visits |
334+
| Service Worker | Zero network for `_framework/` on cached visits |
335+
| Inline config embedding | Eliminated `/api/v1/config` round-trip |
336+
| Calendar parallel loading | 5 API calls concurrent vs sequential |
337+
| Skeleton loading screen | Perceived instant first paint |
338+
339+
### Success Metrics
340+
341+
| Metric | Original Estimate | Target | Measured |
342+
|--------|-------------------|--------|----------|
343+
| WASM payload (Brotli) | ~8–12 MB | < 5 MB | **3.32 MB**|
344+
| First Contentful Paint (cold) | 3–5s | < 2s | Pending Lighthouse |
345+
| Time to Interactive (cold) | 5–10s | < 3.5s | Pending Lighthouse |
346+
| Time to Interactive (cached/return) | 3–5s | < 1.5s | Expected near-instant (SW) |
347+
| Post-auth data render | 1–2s | < 500ms | Expected ~max(5 calls) |
348+
| Lighthouse Performance score | ~40–60 | > 75 | Pending |
325349

326350
---
327351

@@ -330,7 +354,7 @@ This is marginal but essentially free to implement.
330354
- **IL Trimming** can break reflection-based code. `TrimMode=partial` is conservative but still requires testing. Run full E2E suite after enabling.
331355
- **Invariant Globalization** will break `ToString("C")` and similar culture-dependent formatting. Needs audit of all Blazor components.
332356
- **Service Worker** cache invalidation must be correct — a stale service worker serving an old framework version against a new API is a common PWA bug. Blazor's built-in service worker handles this via cache versioning.
333-
- **Embedded Config** in `index.html` means the API's fallback handler needs to dynamically inject config, or a build step must template it. The current `MapFallbackToFile("index.html")` serves a static file — this would need to change to a Razor page or middleware that injects the JSON.
357+
- **Embedded Config** in `index.html` — Implemented via `MapFallback` handler in `Program.cs` that reads `index.html`, injects config JSON as `<script id="app-config" type="application/json">`, and serves dynamically. A fetch interceptor in `index.html` transparently provides this to Blazor's `HttpClient`.
334358
- **Silent Token Renewal** requires Authentik to set appropriate CORS and cookie policies for the iframe-based flow. If Authentik doesn't cooperate, this falls back to the full redirect.
335359
- **Brotli on Raspberry Pi** — encoding is CPU-intensive. Use pre-compressed files rather than on-the-fly compression for static assets. For API responses, `BrotliCompressionLevel.Fastest` keeps CPU usage reasonable.
336360

0 commit comments

Comments
 (0)