From 9408ee07e56db5bbb8319f8da862b549ab948bce Mon Sep 17 00:00:00 2001 From: Jonathan Sykes Date: Thu, 8 Oct 2026 10:08:35 +0800 Subject: [PATCH] Limit the WebKit response handoff to uncontrolled visits --- frontend/lazy.js | 2 +- frontend/lazy.test.js | 10 ++++++++-- perf/results/phase6-lazy-2026-10-08.json | 14 +++++++------- plans/phase6-investigation.md | 7 +++++++ 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/frontend/lazy.js b/frontend/lazy.js index 58412c8..fe83a12 100644 --- a/frontend/lazy.js +++ b/frontend/lazy.js @@ -125,7 +125,7 @@ // Own this response once. Classic-script and fetch preloads do not share // their cached bodies in WebKit; the later bootstrap consumes this promise. const agent=root.navigator.userAgent || ''; - api.captureApp=!!(manifest.appCache && root.crypto?.subtle && /AppleWebKit\//.test(agent) && !/(?:Chrome|Chromium|Edg|OPR)\//.test(agent)); + api.captureApp=!!(manifest.appCache && !root.navigator.serviceWorker?.controller && root.crypto?.subtle && /AppleWebKit\//.test(agent) && !/(?:Chrome|Chromium|Edg|OPR)\//.test(agent)); if(api.captureApp) { api.appResponse=(async()=>{ const key=url('/app.js'); diff --git a/frontend/lazy.test.js b/frontend/lazy.test.js index fddcfdb..1e7d88a 100644 --- a/frontend/lazy.test.js +++ b/frontend/lazy.test.js @@ -2,12 +2,12 @@ const { test } = require('node:test'); const assert = require('node:assert/strict'); const { readFileSync } = require('node:fs'); const vm = require('node:vm'); -function fixture(current, held = [], appCache = false, userAgent = 'AppleWebKit/605.1 Safari/605.1') { +function fixture(current, held = [], appCache = false, userAgent = 'AppleWebKit/605.1 Safari/605.1', controlled = false) { const inserted = [], listeners = {}, writes = [], fetched = []; const manifest = { buildTag: 'own', appCache, groups: { core: { files: [] }, 'layout:classic': { files: ['/classic.css'] }, 'layout:glass-stage': { files: ['/glass.css', '/one.js', '/two.js'] }, 'feature:test': { contract:1, files: ['/one.js', '/two.js'] } }, files: { '/app.js':{h:'a'}, '/classic.css': {h:'c'}, '/glass.css':{h:'g'}, '/one.js':{h:'1'}, '/two.js':{h:'2'} } }; const doc = { readyState:'loading', documentElement: { dataset:{} }, getElementById: () => ({ textContent:JSON.stringify(manifest) }), querySelectorAll: () => [], createElement: tag => ({ tagName:tag.toUpperCase() }), write: value => writes.push(value), addEventListener: (t,f) => listeners[t]=f }; doc.head = { append: node => { inserted.push(node); queueMicrotask(() => node.onload?.()); } }; - const root = { document:doc, localStorage:{ getItem:() => '{"settings":{"layout":"glass-stage"}}' }, console, Promise, URL, crypto:{subtle:{}}, fetch:async key=>{fetched.push(key);return new Response('app');}, setTimeout, clearTimeout, navigator:{userAgent}, addEventListener(){} }; + const root = { document:doc, localStorage:{ getItem:() => '{"settings":{"layout":"glass-stage"}}' }, console, Promise, URL, crypto:{subtle:{}}, fetch:async key=>{fetched.push(key);return new Response('app');}, setTimeout, clearTimeout, navigator:{userAgent,serviceWorker:{controller:controlled?{}:null,addEventListener(){}}}, addEventListener(){} }; if (current) root.caches = { open: async () => ({ match: async key => key === '/__ytp_asset_state' ? new Response(JSON.stringify({current})) : held.includes(key) ? new Response('', {headers:{'X-Asset-Hash':key.split('=')[1]}}) : undefined }) }; root.window=root; root.globalThis=root; vm.runInNewContext(readFileSync(require.resolve('./lazy.js'),'utf8'), root); @@ -108,3 +108,9 @@ test('iOS Chromium-branded browsers still use their WebKit response capture',asy const f=fixture(null,[],true,'AppleWebKit/605.1 CriOS/140.0 Mobile Safari/605.1');await f.root.Lazy.appResponse; assert.equal(f.root.Lazy.captureApp,true);assert.deepEqual(f.fetched,['/app.js?v=a']); }); + +test('controlled WebKit visits use worker-cached native app scripts without a response handoff',async()=>{ + const f=fixture(null,[],true,'AppleWebKit/605.1 Safari/605.1',true); + assert.equal(f.root.Lazy.captureApp,false);assert.equal(f.root.Lazy.appResponse,undefined);assert.deepEqual(f.fetched,[]); + assert.equal(f.inserted[0].as,'script');assert.equal(f.inserted[0].href,'/app.js?v=a'); +}); diff --git a/perf/results/phase6-lazy-2026-10-08.json b/perf/results/phase6-lazy-2026-10-08.json index 5b3d1ef..8a0c5b7 100644 --- a/perf/results/phase6-lazy-2026-10-08.json +++ b/perf/results/phase6-lazy-2026-10-08.json @@ -2,7 +2,7 @@ "results": [ { "browser": "chromium", - "sourceBuildTag": "22eb7414f94c", + "sourceBuildTag": "38131d1a6b5e", "classicSharedDefaults": true, "cssOrderPreserved": true, "settingsSearch": true, @@ -28,7 +28,7 @@ }, { "browser": "webkit", - "sourceBuildTag": "22eb7414f94c", + "sourceBuildTag": "38131d1a6b5e", "classicSharedDefaults": true, "cssOrderPreserved": true, "settingsSearch": true, @@ -56,7 +56,7 @@ "disconnected": true }, { - "message": "/127.0.0.1:33445/api/version.", + "message": "/127.0.0.1:43269/api/recommendations?fp=163577c02a53bc40.", "disconnected": true }, { @@ -64,7 +64,7 @@ "disconnected": true }, { - "message": "/127.0.0.1:33445/api/recommendations?fp=163577c02a53bc40.", + "message": "/127.0.0.1:43269/api/version.", "disconnected": true }, { @@ -72,7 +72,7 @@ "disconnected": true }, { - "message": "/127.0.0.1:33445/api/version.", + "message": "/127.0.0.1:43269/api/version.", "disconnected": true }, { @@ -80,7 +80,7 @@ "disconnected": true }, { - "message": "/127.0.0.1:33445/api/recommendations?fp=163577c02a53bc40.", + "message": "/127.0.0.1:43269/api/recommendations?fp=163577c02a53bc40.", "disconnected": true }, { @@ -88,7 +88,7 @@ "disconnected": true }, { - "message": "/127.0.0.1:33445/api/version.", + "message": "/127.0.0.1:43269/api/version.", "disconnected": true } ] diff --git a/plans/phase6-investigation.md b/plans/phase6-investigation.md index c089e04..4e789a1 100644 --- a/plans/phase6-investigation.md +++ b/plans/phase6-investigation.md @@ -106,3 +106,10 @@ CacheStorage or hashing work. WebKit's byte-verified owned response path is unchanged. The native fixture models the actual worker fallback decision instead of a second page-side decision. All 221 frontend tests and 184 server tests pass; both-engine lazy/offline/eviction/pinning/contracts/playback smoke passes. + +The WebKit warm prototype also paid repeated handoff/cache/hash work (warm +~98 ms versus Phase 5 83 ms). Capture now applies only to uncontrolled WebKit +visits. Controlled warm/offline/update pages use the worker's native exact-URL +script path, including its strengthened contract guard. A failing head-loader +test precedes this change; all 222 frontend and 184 server tests and both-engine +lazy smoke pass. Cold capture and both rollback flags are unchanged.