Optimize performance: batch DB ops, N+1 fixes, Suspense streaming, and Jotai best practices

- Add composite indexes on userEpisodeWatches and userMovieWatches for hot queries
- Batch episode tracking: wrap season/batch watches in single transaction (~8 queries vs 8*N)
- Fix N+1 patterns in credits, recommendations, and filmography with batch prefetch+insert
- Stream TV season hydration via Suspense instead of blocking page render
- Optimize webhook logs (per-connection LIMIT 10) and system health queries
- Merge genre filter waterfalls into single Promise.all fetch
- Migrate deprecated Jotai loadable() to unwrap()
- Replace isolated createStore()+Provider with useHydrateAtoms on root store
- Remove unnecessary atomWithStorage SSR guards (handled by Jotai internally)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 17:33:27 -05:00
co-authored by Claude Opus 4.6
parent ed825f3a78
commit f36ca0cbf8
23 changed files with 839 additions and 571 deletions
@@ -0,0 +1,14 @@
import { ensureTvHydrated } from "@/lib/services/metadata";
import { TitleSeasons } from "./title-seasons";
export async function AsyncTitleSeasons({
titleId,
tmdbId,
}: {
titleId: string;
tmdbId: number;
}) {
const seasons = await ensureTvHydrated(titleId, tmdbId);
if (seasons.length === 0) return null;
return <TitleSeasons seasons={seasons} />;
}
@@ -2,7 +2,7 @@
import type { Hotkey } from "@tanstack/react-hotkeys";
import { useHotkey } from "@tanstack/react-hotkeys";
import { getDefaultStore, useAtomValue } from "jotai";
import { useAtomValue } from "jotai";
import { useRouter } from "next/navigation";
import { commandPaletteOpenAtom } from "@/lib/atoms/command-palette";
import { titleTypeAtom, userStatusAtom } from "@/lib/atoms/title";
@@ -15,9 +15,7 @@ export function TitleKeyboardShortcuts() {
const { handleStatusChange, handleRating, handleWatchMovie } =
useTitleActions();
const commandPaletteOpen = useAtomValue(commandPaletteOpenAtom, {
store: getDefaultStore(),
});
const commandPaletteOpen = useAtomValue(commandPaletteOpenAtom);
const enabled = !commandPaletteOpen;
// W: toggle watchlist (add if not in library, remove if in library)
@@ -1,7 +1,6 @@
"use client";
import { createStore, Provider } from "jotai";
import { useState } from "react";
import { useHydrateAtoms } from "jotai/utils";
import {
episodeWatchesAtom,
seasonsAtom,
@@ -32,17 +31,15 @@ export function TitleProvider({
seasons: Season[];
children: React.ReactNode;
}) {
const [store] = useState(() => {
const s = createStore();
s.set(titleIdAtom, titleId);
s.set(titleTypeAtom, titleType);
s.set(titleNameAtom, titleName);
s.set(seasonsAtom, seasons);
s.set(userStatusAtom, initialStatus);
s.set(userRatingAtom, initialRating);
s.set(episodeWatchesAtom, initialEpisodeWatches);
return s;
});
useHydrateAtoms([
[titleIdAtom, titleId],
[titleTypeAtom, titleType],
[titleNameAtom, titleName],
[seasonsAtom, seasons],
[userStatusAtom, initialStatus],
[userRatingAtom, initialRating],
[episodeWatchesAtom, initialEpisodeWatches],
]);
return <Provider store={store}>{children}</Provider>;
return children;
}
@@ -7,10 +7,10 @@ import {
IconChevronUp,
IconDeviceTvOld,
} from "@tabler/icons-react";
import { useAtomValue } from "jotai";
import { useAtomValue, useSetAtom } from "jotai";
import { AnimatePresence, motion } from "motion/react";
import Image from "next/image";
import { useState } from "react";
import { useEffect, useState } from "react";
import {
AlertDialog,
AlertDialogAction,
@@ -29,9 +29,23 @@ import {
userStatusAtom,
watchingEpAtom,
} from "@/lib/atoms/title";
import type { Season } from "@/lib/types/title";
import { useTitleActions } from "./use-title-actions";
export function TitleSeasons() {
export function TitleSeasons({
seasons: streamedSeasons,
}: {
seasons?: Season[];
} = {}) {
const setSeasons = useSetAtom(seasonsAtom);
// When seasons are streamed via Suspense, sync them into the Jotai store
useEffect(() => {
if (streamedSeasons && streamedSeasons.length > 0) {
setSeasons(streamedSeasons);
}
}, [streamedSeasons, setSeasons]);
const seasons = useAtomValue(seasonsAtom);
const episodeWatches = useAtomValue(episodeWatchesAtom);
const userStatus = useAtomValue(userStatusAtom);
+14 -3
View File
@@ -2,7 +2,10 @@ import { eq } from "drizzle-orm";
import type { Metadata } from "next";
import { notFound, redirect } from "next/navigation";
import { Suspense } from "react";
import { RecommendationsSkeleton } from "@/components/skeletons";
import {
RecommendationsSkeleton,
SeasonsSkeleton,
} from "@/components/skeletons";
import { getSession } from "@/lib/auth/session";
import { db } from "@/lib/db/client";
import { titles } from "@/lib/db/schema";
@@ -10,6 +13,7 @@ import { getTitleWithChildren, importTitle } from "@/lib/services/metadata";
import { getUserTitleInfo } from "@/lib/services/tracking";
import { tmdbImageUrl } from "@/lib/tmdb/image";
import { getTitleThemeStyle } from "@/lib/utils/title-theme";
import { AsyncTitleSeasons } from "./_components/async-title-seasons";
import { TitleActions } from "./_components/title-actions";
import { TitleAvailability } from "./_components/title-availability";
import { TitleCast } from "./_components/title-cast";
@@ -71,7 +75,7 @@ export default async function TitleDetailPage({
]);
if (!result) notFound();
const { title, seasons, availability, cast } = result;
const { title, seasons, needsHydration, availability, cast } = result;
const themeStyle = getTitleThemeStyle(title.colorPalette);
@@ -94,7 +98,14 @@ export default async function TitleDetailPage({
<TitleAvailability availability={availability} />
</TitleHero>
{title.type === "tv" && seasons.length > 0 && <TitleSeasons />}
{title.type === "tv" && needsHydration && (
<Suspense fallback={<SeasonsSkeleton />}>
<AsyncTitleSeasons titleId={title.id} tmdbId={title.tmdbId} />
</Suspense>
)}
{title.type === "tv" && !needsHydration && seasons.length > 0 && (
<TitleSeasons />
)}
<TitleCast cast={cast} titleType={title.type} />