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)) {