perf(ui): deprioritise the dictionary download; drop the lobby prefetch
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 58s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m27s
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 58s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m27s
On a slow link (EDGE) the background dictionary download starved the game's own traffic — the board and the move preview stalled behind a 450KB fetch. Fetch the dawg at low priority (Priority Hints; ignored on iOS WebKit, harmless) so the browser schedules it behind the game's requests, and load it only on game open — the lobby no longer prefetches, which was its main competitor for the channel. Evict a cached blob the reader rejects (a stale or partial entry) so it self-heals instead of lingering and re-fetches. Raise the app-entry bundle budget to 110KB (the feature's small in-entry game/debug wiring; the heavy dict code stays lazy).
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
// same dictionary share one in-flight load.
|
||||
|
||||
import { Dawg } from './dawg';
|
||||
import { dictKey, idbGetDawg, idbPutDawg, requestPersist } from './store';
|
||||
import { dictKey, idbGetDawg, idbPutDawg, idbDelDawg, requestPersist } from './store';
|
||||
import { gateway } from '../gateway';
|
||||
import type { Variant } from '../model';
|
||||
|
||||
@@ -61,7 +61,9 @@ async function load(variant: Variant, version: string, key: string): Promise<Daw
|
||||
return reader;
|
||||
}
|
||||
} catch {
|
||||
/* fall through to the network */
|
||||
// A cached blob the reader rejected (a stale or partial entry): evict it so the next
|
||||
// load re-fetches instead of failing on it forever, then fall through to the network.
|
||||
void idbDelDawg(key);
|
||||
}
|
||||
|
||||
// Tier 2: the session-gated download — gated by the bad-connection breaker.
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
// Lobby-time warm-up of the local move-preview dictionaries: after the lobby
|
||||
// loads, fetch the DAWG of each of the player's unfinished games in the
|
||||
// background, so opening one previews moves locally with no wait. It mirrors
|
||||
// preloadGames — bounded, best-effort, dedup'd — and warms "your turn" games
|
||||
// first, since those are the ones the player is most likely to open and play.
|
||||
// Each distinct (variant, version) is fetched once.
|
||||
|
||||
import { getDawg, hasDawg } from './loader';
|
||||
import { dictKey } from './store';
|
||||
import { isMyTurn } from '../lobbysort';
|
||||
import type { GameView, Variant } from '../model';
|
||||
|
||||
// Cap on dictionaries warmed at once. A player rarely spans more than a couple of
|
||||
// (variant, version) pairs, so this is mostly a safety bound on a burst.
|
||||
const MAX_CONCURRENT = 3;
|
||||
// Dictionaries being warmed right now, so the lobby's repeated refreshes never
|
||||
// queue the same one twice.
|
||||
const inflight = new Set<string>();
|
||||
|
||||
/**
|
||||
* preloadDicts warms the local dictionary cache for the player's unfinished games,
|
||||
* your-turn games first (myId identifies the viewer). It is best-effort and
|
||||
* bounded; finished games and already-loaded or in-flight dictionaries are
|
||||
* skipped. It resolves once every queued dictionary has settled.
|
||||
*/
|
||||
export async function preloadDicts(games: GameView[], myId: string): Promise<void> {
|
||||
const ordered = [...games].sort((a, b) => Number(isMyTurn(b, myId)) - Number(isMyTurn(a, myId)));
|
||||
|
||||
const targets: Array<{ variant: Variant; version: string; key: string }> = [];
|
||||
const seen = new Set<string>();
|
||||
for (const g of ordered) {
|
||||
if (g.status === 'finished' || !g.dictVersion) continue;
|
||||
const key = dictKey(g.variant, g.dictVersion);
|
||||
if (seen.has(key) || inflight.has(key) || hasDawg(g.variant, g.dictVersion)) continue;
|
||||
seen.add(key);
|
||||
targets.push({ variant: g.variant, version: g.dictVersion, key });
|
||||
}
|
||||
if (targets.length === 0) return;
|
||||
|
||||
for (const t of targets) inflight.add(t.key);
|
||||
let next = 0;
|
||||
const worker = async (): Promise<void> => {
|
||||
while (next < targets.length) {
|
||||
const t = targets[next++];
|
||||
try {
|
||||
await getDawg(t.variant, t.version);
|
||||
} catch {
|
||||
/* best-effort: the game falls back to the network preview on open */
|
||||
} finally {
|
||||
inflight.delete(t.key);
|
||||
}
|
||||
}
|
||||
};
|
||||
await Promise.all(Array.from({ length: Math.min(MAX_CONCURRENT, targets.length) }, worker));
|
||||
}
|
||||
@@ -65,6 +65,24 @@ export async function idbPutDawg(key: string, data: ArrayBuffer): Promise<void>
|
||||
}
|
||||
}
|
||||
|
||||
/** idbDelDawg removes a cached blob — e.g. a stale or partial entry the reader rejected, so
|
||||
* the next load re-fetches instead of failing on it forever. Best-effort. */
|
||||
export async function idbDelDawg(key: string): Promise<void> {
|
||||
const db = openDb();
|
||||
if (!db) return;
|
||||
try {
|
||||
const d = await db;
|
||||
await new Promise<void>((resolve) => {
|
||||
const tx = d.transaction(STORE, 'readwrite');
|
||||
tx.objectStore(STORE).delete(key);
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => resolve();
|
||||
});
|
||||
} catch {
|
||||
/* best-effort */
|
||||
}
|
||||
}
|
||||
|
||||
let persistRequested = false;
|
||||
|
||||
/**
|
||||
|
||||
@@ -64,9 +64,13 @@ export function createTransport(baseUrl: string): GatewayClient {
|
||||
|
||||
async fetchDict(variant, version) {
|
||||
if (!token) throw new Error('fetchDict: no session');
|
||||
// Low priority so the browser schedules the (large) dictionary behind the game's own
|
||||
// requests — the move eval, state and live events — on a slow link (EDGE). Ignored
|
||||
// where the Priority Hints API is unsupported (iOS WebKit), which is harmless.
|
||||
const res = await fetch(`${origin}/dict/${encodeURIComponent(variant)}/${encodeURIComponent(version)}`, {
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
priority: 'low',
|
||||
} as RequestInit);
|
||||
if (!res.ok) throw new Error(`fetchDict ${variant}/${version}: HTTP ${res.status}`);
|
||||
return res.arrayBuffer();
|
||||
},
|
||||
|
||||
@@ -51,14 +51,7 @@
|
||||
// Warm the cache for the ongoing games so opening one from the lobby is instant. The list
|
||||
// just loaded, so the connection is up; the call is non-blocking and re-running it on each
|
||||
// lobby refresh cheaply warms any newly appeared game (already-cached ones are skipped).
|
||||
if (connection.online) {
|
||||
void preloadGames(games);
|
||||
// Warm the local move-preview dictionaries (your-turn first). The dict subsystem is
|
||||
// dynamically imported so it stays out of the initial app bundle — a progressive
|
||||
// enhancement; without it a game simply falls back to the network preview.
|
||||
const myId = app.session?.userId ?? '';
|
||||
void import('../lib/dict/prefetch').then((m) => m.preloadDicts(games, myId));
|
||||
}
|
||||
if (connection.online) void preloadGames(games);
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user