From b1ac11e1d78a30fe3280cade4a8164170e53dd95 Mon Sep 17 00:00:00 2001 From: JustMe Date: Fri, 16 Oct 2026 06:00:17 +0800 Subject: [PATCH] perf(web): shorten the cold start when opening a saved space Activate the account with parallel requests, run the full vault sync in the background, skip the duplicate space list request, and send the rejoin together with the first screen of data. [CI] --- web/src/core-bridge/worker/account.ts | 18 +++++++++++------- web/src/core-bridge/worker/channelTransport.ts | 8 +++++--- web/src/store/channels.ts | 4 +++- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/web/src/core-bridge/worker/account.ts b/web/src/core-bridge/worker/account.ts index a8a97bb..7f53435 100644 --- a/web/src/core-bridge/worker/account.ts +++ b/web/src/core-bridge/worker/account.ts @@ -106,14 +106,17 @@ async function syncVault(): Promise { async function activate(identity: { username: string; token: string }, key: CryptoKey, remote: VaultBody, remember = true, cachedPersonal?: PersonalProfile): Promise { const profile = validateVault(await openJson(remote.vault, key)); - const deviceSigning = await ensureDeviceSigning(identity.username, identity.token, profile.keys.signing); + // 设备签名密钥与个人资料互不依赖,同时请求,冷启动少等一个往返 + const [deviceSigning, personal] = await Promise.all([ + ensureDeviceSigning(identity.username, identity.token, profile.keys.signing), + loadPersonal(identity.token).catch((error: unknown) => { + if (!(error instanceof Error && error.message === '网络连接失败')) throw error; + return cachedPersonal; + }), + ]); clearResourceSessions(); setResourceScope(identity.username); const session: AccountSession = { ...identity, key, profile, revision: remote.revision, remember, controller: new AbortController(), dirty: false, deviceSigning }; - try { session.personal = await loadPersonal(identity.token); } - catch (error) { - if (!(error instanceof Error && error.message === '网络连接失败')) throw error; - session.personal = cachedPersonal; - } + session.personal = personal; account = session; setAccountIdentity(identity, profile.keys.publicKey, session.personal); await applyProfile(mergeVault(profile, await listAllResources(), await listResourceDeletes())); setResourceChangeListener(scheduleSync); @@ -135,7 +138,8 @@ export function restoreAccount(): Promise { try { const session = await activate(saved, saved.key, remote, true, saved.personal); setAccountIdentity(session, session.profile.keys.publicKey, session.personal); - await syncVault().catch(() => {}); + // 保险库已在 activate 里并入本机资源,完整同步(约 4 次往返)放到后台,不挡住打开页面 + void syncVault().catch(() => {}); } catch (error) { await logoutAccount(); throw error; } })().catch((error: unknown) => { restoration = undefined; throw error; }); return restoration; diff --git a/web/src/core-bridge/worker/channelTransport.ts b/web/src/core-bridge/worker/channelTransport.ts index 04416f5..85e2a4d 100644 --- a/web/src/core-bridge/worker/channelTransport.ts +++ b/web/src/core-bridge/worker/channelTransport.ts @@ -21,8 +21,10 @@ export async function connectChannel(id: string): Promise { if (resource.kind !== 'channel') throw new Error('资源类型不匹配'); // Device sessions can be revoked independently of account membership; rejoin idempotently. const joinedAt = freshlyJoined.get(id); freshlyJoined.delete(id); - if (joinedAt === undefined || Date.now() - joinedAt > 60_000) - await apiFetch(`/channels/${id}/join`, { method: 'POST', headers: session.authHeaders(session.can('owner') ? 'owner' : 'read'), signal }); + const join = () => apiFetch(`/channels/${id}/join`, { method: 'POST', headers: session.authHeaders(session.can('owner') ? 'owner' : 'read'), signal }); + // 已保存的成员重连只是确认身份:与首屏数据同时发出,失败就由下面的重连循环再来一次。首次加入(needsJoin)仍然先 join + const joining = joinedAt !== undefined && Date.now() - joinedAt <= 60_000 ? undefined + : resource.needsJoin ? await join().then(() => undefined) : join().then(() => undefined); if (resource.needsJoin) { if (session.can('owner')) await channelMetadata(resource, session); resource.needsJoin = false; @@ -49,7 +51,7 @@ export async function connectChannel(id: string): Promise { while (!signal.aborted) { try { if (!initialized) { - [stream.cursor] = await Promise.all([syncChannel(resource, session, signal), channelMetadata(resource, session)]); + [stream.cursor] = await Promise.all([syncChannel(resource, session, signal), channelMetadata(resource, session), joining]); if (signal.aborted) break; await flushOutbox(resource, session); initialized = true; diff --git a/web/src/store/channels.ts b/web/src/store/channels.ts index 71218cb..50d0951 100644 --- a/web/src/store/channels.ts +++ b/web/src/store/channels.ts @@ -24,7 +24,7 @@ interface ChannelsState { const upsert = (list: ChannelInfo[], c: ChannelInfo) => [c, ...list.filter((x) => x.id !== c.id)]; let hydration: Promise | undefined; -export const useChannels = create()((set) => ({ +export const useChannels = create()((set, get) => ({ list: [], hydrated: false, pendingLink: null, @@ -41,6 +41,8 @@ export const useChannels = create()((set) => ({ return c; }, hydrate: () => { + // 账号刷新已经列过一次,别再请求一遍 + if (get().hydrated) return Promise.resolve(); hydration ??= api.listSaved().then((list) => set({ list, hydrated: true }), () => set({ hydrated: true })); return hydration; },