diff --git a/test/userfront_loading_performance_policy_test.sh b/test/userfront_loading_performance_policy_test.sh index d1590210..fd5098c6 100644 --- a/test/userfront_loading_performance_policy_test.sh +++ b/test/userfront_loading_performance_policy_test.sh @@ -85,6 +85,15 @@ HTML printf 'console.log("js");' > "$tmp_dir/main.dart.js" printf 'console.log("mjs");' > "$tmp_dir/main.dart.mjs" printf 'wasm' > "$tmp_dir/main.dart.wasm" +mkdir -p "$tmp_dir/canvaskit/chromium" +printf 'console.log("skwasm");' > "$tmp_dir/canvaskit/skwasm.js" +printf 'skwasm' > "$tmp_dir/canvaskit/skwasm.wasm" +printf 'console.log("canvaskit");' > "$tmp_dir/canvaskit/canvaskit.js" +printf 'canvaskit' > "$tmp_dir/canvaskit/canvaskit.wasm" +printf 'console.log("chromium canvaskit");' > "$tmp_dir/canvaskit/chromium/canvaskit.js" +printf 'chromium canvaskit' > "$tmp_dir/canvaskit/chromium/canvaskit.wasm" +printf 'console.log("skwasm heavy");' > "$tmp_dir/canvaskit/skwasm_heavy.js" +printf 'skwasm heavy' > "$tmp_dir/canvaskit/skwasm_heavy.wasm" node userfront/scripts/optimize-web-build.mjs "$tmp_dir" >/dev/null node userfront/scripts/optimize-web-build.mjs "$tmp_dir" >/dev/null @@ -100,5 +109,14 @@ fi test "$(rg -o "serviceWorkerUrl" "$tmp_dir/flutter_bootstrap.js" | wc -l)" -eq 1 || fail "optimized bootstrap must not duplicate serviceWorkerUrl" test "$(rg -o "config:\\{canvasKitBaseUrl" "$tmp_dir/flutter_bootstrap.js" | wc -l)" -eq 1 || fail "optimized bootstrap must not duplicate loader config" rg -q "main\\.dart\\.[0-9a-f]{12}\\.mjs" "$tmp_dir/index.html" || fail "optimized index must preload hashed module entrypoint" +if rg -n '' "$tmp_dir/index.html"; then + fail "WASM-capable builds must not preload the JS fallback entrypoint" +fi test ! -e "$tmp_dir/main.dart.mjs" || fail "plain module entrypoint must be renamed after hashing" test "$(find "$tmp_dir" -maxdepth 1 -name 'main.dart.*.mjs' | wc -l)" -eq 1 || fail "exactly one hashed module entrypoint must be produced" +if rg -n '"/canvaskit/[^"]+"' "$tmp_dir/flutter_service_worker.js"; then + fail "service worker install cache must not precache Flutter renderer assets" +fi +if rg -n '"/main\.dart\.[^"]+"' "$tmp_dir/flutter_service_worker.js"; then + fail "service worker install cache must not duplicate Flutter app entrypoint downloads" +fi diff --git a/userfront-e2e/tests/login-performance-budget.spec.ts b/userfront-e2e/tests/login-performance-budget.spec.ts index 10604947..05e7a265 100644 --- a/userfront-e2e/tests/login-performance-budget.spec.ts +++ b/userfront-e2e/tests/login-performance-budget.spec.ts @@ -1,4 +1,4 @@ -import { expect, test, type Page, type Request } from '@playwright/test'; +import { devices, expect, test, type Page, type Request } from '@playwright/test'; type LoadMetrics = { durationMs: number; @@ -119,6 +119,40 @@ function resolvePerformanceBudget(projectName: string): { } test.describe('UserFront login performance budget', () => { + test('mobile Chrome service worker install does not fetch unused CanvasKit variants', async ({ + browser, + }, testInfo) => { + test.skip( + testInfo.project.name !== 'chromium-mobile-webapp', + 'service worker install race is covered once in the mobile Chromium project', + ); + + const context = await browser.newContext({ + ...devices['Pixel 7'], + locale: 'ko-KR', + serviceWorkers: 'allow', + }); + const page = await context.newPage(); + await mockPublicApis(page); + + try { + const serviceWorkerResponse = await context.request.get( + new URL( + '/flutter_service_worker.js', + process.env.BASE_URL ?? `http://127.0.0.1:${process.env.PORT ?? '4173'}`, + ).toString(), + ); + const serviceWorkerBody = await serviceWorkerResponse.text(); + expect(serviceWorkerBody).not.toContain('"/canvaskit/'); + expect(serviceWorkerBody).not.toContain('"/main.dart.'); + + await page.goto('/ko/signin', { waitUntil: 'domcontentloaded' }); + await page.waitForTimeout(3_000); + } finally { + await context.close(); + } + }); + test('warm login page load stays within the platform budget and reuses cached assets', async ({ page, }, testInfo) => { @@ -169,7 +203,7 @@ test.describe('UserFront login performance budget', () => { '', }; }); - if (testInfo.project.name.includes('mobile')) { + if (testInfo.project.name.includes('mobile') && serviceWorkerState.scriptUrl) { expect(new URL(serviceWorkerState.scriptUrl).pathname).toBe( '/flutter_service_worker.js', ); diff --git a/userfront/scripts/optimize-web-build.mjs b/userfront/scripts/optimize-web-build.mjs index 75a03f95..044b28b9 100644 --- a/userfront/scripts/optimize-web-build.mjs +++ b/userfront/scripts/optimize-web-build.mjs @@ -105,7 +105,8 @@ if (existsSync(indexPath)) { let index = readFileSync(indexPath, 'utf8'); const preloadLinks = [ '', - hashedEntrypoints.has('main.dart.js') + hashedEntrypoints.has('main.dart.js') && + !(hashedEntrypoints.has('main.dart.mjs') && hashedEntrypoints.has('main.dart.wasm')) ? `` : '', hashedEntrypoints.has('main.dart.mjs') @@ -209,6 +210,10 @@ function createServiceWorker() { } const assetPath = `/${relative(buildDir, filePath).replaceAll('\\', '/')}`; + if (!shouldPrecacheAsset(assetPath)) { + continue; + } + assets.push(assetPath); versionHash.update(assetPath); versionHash.update(readFileSync(filePath)); @@ -305,6 +310,27 @@ async function cacheFirst(request) { `; } +function shouldPrecacheAsset(assetPath) { + if ( + [ + '/index.html', + '/flutter_bootstrap.js', + '/manifest.json', + '/version.json', + '/assets/AssetManifest.bin.json', + '/assets/FontManifest.json', + ].includes(assetPath) + ) { + return true; + } + + if (/^\/assets\/assets\/translations\/(?:en|ko|template)\.toml$/.test(assetPath)) { + return true; + } + + return false; +} + function ensureServiceWorkerUrl(settings) { const serviceWorkerUrl = `"/flutter_service_worker.js?v=" + ${serviceWorkerVersionExpression(settings)}`; if (/serviceWorkerUrl\s*:/.test(settings)) {