Ionosphere.tv
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

perf: lazy-load track transcript — cuts initial fetch from 12MB to 2MB

Track page now fetches without transcript by default (saves ~10MB).
Transcript + facets load lazily when user switches to transcript tab.

getTrack default response excludes transcript field. Use
?include=transcript for full data. Cache-Control headers added.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+33 -9
+9
apps/ionosphere-appview/src/routes.ts
··· 675 675 }); 676 676 } 677 677 678 + // Default: exclude transcript (9MB+ facets) — fetched lazily via ?include=transcript 679 + const include = c.req.query("include"); 680 + if (include !== "transcript") { 681 + c.header("Cache-Control", "public, max-age=3600, stale-while-revalidate=86400"); 682 + const { transcript, ...rest } = data; 683 + return c.json(rest); 684 + } 685 + 686 + c.header("Cache-Control", "public, max-age=3600, stale-while-revalidate=86400"); 678 687 return c.json(data); 679 688 }); 680 689
+24 -9
apps/ionosphere/src/app/tracks/[stream]/TrackViewContent.tsx
··· 123 123 124 124 // --- Main Inner (inside engine provider) --- 125 125 126 - function TrackViewInner({ track }: { track: TrackData }) { 126 + function TrackViewInner({ track, stream }: { track: TrackData; stream: string }) { 127 127 const [activeTab, setActiveTab] = useState<"talks" | "transcript">("talks"); 128 - const hasTranscript = !!(track.transcript?.facets?.length); 128 + const [transcript, setTranscript] = useState(track.transcript ?? null); 129 + const [loadingTranscript, setLoadingTranscript] = useState(false); 130 + const hasTranscript = !!(transcript?.facets?.length); 131 + 132 + // Lazy-load transcript when tab is first activated 133 + useEffect(() => { 134 + if (activeTab !== "transcript" || transcript || loadingTranscript) return; 135 + setLoadingTranscript(true); 136 + fetch(`${API_BASE}/xrpc/tv.ionosphere.getTrack?stream=${encodeURIComponent(stream)}&include=transcript`) 137 + .then(res => res.json()) 138 + .then(data => { if (data.transcript) setTranscript(data.transcript); }) 139 + .catch(() => {}) 140 + .finally(() => setLoadingTranscript(false)); 141 + }, [activeTab, transcript, loadingTranscript, stream]); 129 142 const [speakerPopover, setSpeakerPopover] = useState<{ speakerId: string; position: { x: number; y: number } } | null>(null); 130 143 131 144 // Zoom state ··· 356 369 </button> 357 370 <button 358 371 onClick={() => setActiveTab("transcript")} 359 - disabled={!hasTranscript} 360 372 className={`pb-2 text-sm font-medium border-b-2 transition-colors ${ 361 373 activeTab === "transcript" 362 374 ? "border-neutral-300 text-neutral-100" 363 375 : "border-transparent text-neutral-500 hover:text-neutral-300" 364 - } ${!hasTranscript ? "opacity-30 cursor-not-allowed" : ""}`} 376 + }`} 365 377 > 366 378 Transcript 367 379 </button> ··· 377 389 <TalkList /> 378 390 </div> 379 391 )} 380 - {activeTab === "transcript" && hasTranscript && ( 381 - <WindowedTranscriptView document={track.transcript!} /> 392 + {activeTab === "transcript" && ( 393 + loadingTranscript 394 + ? <div className="flex items-center justify-center h-32 text-neutral-500">Loading transcript...</div> 395 + : hasTranscript 396 + ? <WindowedTranscriptView document={transcript!} /> 397 + : <div className="flex items-center justify-center h-32 text-neutral-500">No transcript available</div> 382 398 )} 383 399 </div> 384 400 </div> ··· 391 407 const [track, setTrack] = useState<TrackData | null>(null); 392 408 393 409 useEffect(() => { 394 - // Fetch the heavy data (diarization, transcript, words) client-side 410 + // Fetch track data without transcript (saves ~10MB) — transcript loads lazily on tab switch 395 411 fetch(`${API_BASE}/xrpc/tv.ionosphere.getTrack?stream=${encodeURIComponent(stream)}`) 396 412 .then(res => res.json()) 397 413 .then(data => setTrack(data)) 398 414 .catch(() => { 399 - // Fallback: use meta without heavy data 400 415 setTrack({ ...trackMeta, diarization: [], words: [] }); 401 416 }); 402 417 }, [stream, trackMeta]); ··· 411 426 412 427 return ( 413 428 <TimestampProvider> 414 - <TrackViewInner track={track} /> 429 + <TrackViewInner track={track} stream={stream} /> 415 430 </TimestampProvider> 416 431 ); 417 432 }