Prefetch all the things!

This commit is contained in:
Jack Kingsman
2026-02-13 00:48:37 -08:00
parent 908a479fa6
commit 1c4d6c07a8
4 changed files with 43 additions and 14 deletions

View File

@@ -20,8 +20,14 @@
<script>
// Start critical data fetches before React loads — shaves ~1-2s off startup.
// React hooks consume the promises via window.__prefetch.
var j = function(r) { return r.json(); };
window.__prefetch = {
unreads: fetch('/api/read-state/unreads').then(function(r) { return r.json(); }),
config: fetch('/api/radio/config').then(j),
settings: fetch('/api/settings').then(j),
channels: fetch('/api/channels').then(j),
contacts: fetch('/api/contacts?limit=1000&offset=0').then(j),
unreads: fetch('/api/read-state/unreads').then(j),
undecryptedCount: fetch('/api/packets/undecrypted/count').then(j),
};
</script>
<script type="module" src="/src/main.tsx"></script>

View File

@@ -9,6 +9,7 @@ import {
Suspense,
} from 'react';
import { api } from './api';
import { takePrefetch } from './prefetch';
import { useWebSocket } from './useWebSocket';
import {
useRepeaterMode,
@@ -308,7 +309,7 @@ export function App() {
// Fetch radio config (not sent via WebSocket)
const fetchConfig = useCallback(async () => {
try {
const data = await api.getRadioConfig();
const data = await (takePrefetch('config') ?? api.getRadioConfig());
setConfig(data);
} catch (err) {
console.error('Failed to fetch config:', err);
@@ -318,7 +319,7 @@ export function App() {
// Fetch app settings
const fetchAppSettings = useCallback(async () => {
try {
const data = await api.getSettings();
const data = await (takePrefetch('settings') ?? api.getSettings());
setAppSettings(data);
// Initialize in-memory cache with server data
initLastMessageTimes(data.last_message_times ?? {});
@@ -330,7 +331,7 @@ export function App() {
// Fetch undecrypted packet count
const fetchUndecryptedCount = useCallback(async () => {
try {
const data = await api.getUndecryptedPacketCount();
const data = await (takePrefetch('undecryptedCount') ?? api.getUndecryptedPacketCount());
setUndecryptedCount(data.count);
} catch (err) {
console.error('Failed to fetch undecrypted count:', err);
@@ -340,7 +341,7 @@ export function App() {
// Fetch all contacts, paginating if >1000
const fetchAllContacts = useCallback(async (): Promise<Contact[]> => {
const pageSize = 1000;
const first = await api.getContacts(pageSize, 0);
const first = await (takePrefetch('contacts') ?? api.getContacts(pageSize, 0));
if (first.length < pageSize) return first;
let all = [...first];
let offset = pageSize;
@@ -360,7 +361,7 @@ export function App() {
fetchUndecryptedCount();
// Fetch contacts and channels via REST (parallel, faster than WS serial push)
api.getChannels().then(setChannels).catch(console.error);
(takePrefetch('channels') ?? api.getChannels()).then(setChannels).catch(console.error);
fetchAllContacts()
.then((data) => {
setContacts(data);

View File

@@ -7,12 +7,7 @@ import {
type ConversationTimes,
} from '../utils/conversationState';
import type { Channel, Contact, Conversation, Message, UnreadCounts } from '../types';
// Consume the prefetched unreads promise started in index.html (if available).
// This lets the fetch run while React JS is still downloading/parsing.
const prefetchedUnreads: Promise<UnreadCounts> | undefined = (
window as unknown as { __prefetch?: { unreads?: Promise<UnreadCounts> } }
).__prefetch?.unreads;
import { takePrefetch } from '../prefetch';
export interface UseUnreadCountsResult {
unreadCounts: Record<string, number>;
@@ -63,8 +58,9 @@ export function useUnreadCounts(
const contactsLen = contacts.length;
const prevLens = useRef({ channels: 0, contacts: 0 });
useEffect(() => {
if (prefetchedUnreads) {
prefetchedUnreads.then(applyUnreads).catch(() => fetchUnreads());
const prefetched = takePrefetch('unreads');
if (prefetched) {
prefetched.then(applyUnreads).catch(() => fetchUnreads());
} else {
fetchUnreads();
}

26
frontend/src/prefetch.ts Normal file
View File

@@ -0,0 +1,26 @@
/**
* Consume prefetched API promises started in index.html before React loaded.
*
* Each key is consumed at most once — the first caller gets the promise,
* subsequent callers get undefined and should fall back to a normal fetch.
*/
import type { AppSettings, Channel, Contact, RadioConfig, UnreadCounts } from './types';
interface PrefetchMap {
config?: Promise<RadioConfig>;
settings?: Promise<AppSettings>;
channels?: Promise<Channel[]>;
contacts?: Promise<Contact[]>;
unreads?: Promise<UnreadCounts>;
undecryptedCount?: Promise<{ count: number }>;
}
const store: PrefetchMap = (window as unknown as { __prefetch?: PrefetchMap }).__prefetch ?? {};
/** Take a prefetched promise (consumed once, then gone). */
export function takePrefetch<K extends keyof PrefetchMap>(key: K): PrefetchMap[K] {
const p = store[key];
delete store[key];
return p;
}