perf(frontend): eliminate author profile waterfall — use thought.author directly

This commit is contained in:
2026-05-15 19:47:06 +02:00
parent 9ecbde019d
commit dadfe04934
10 changed files with 8581 additions and 114 deletions

View File

@@ -5,9 +5,7 @@ import {
getFriends,
getMe,
getTopFriends,
getUserProfile,
Me,
User,
} from "@/lib/api";
import { PostThoughtForm } from "@/components/post-thought-form";
import { Button } from "@/components/ui/button";
@@ -62,17 +60,6 @@ async function FeedPage({
const { items: allThoughts, totalPages } = feedData!;
const thoughtThreads = buildThoughtThreads(allThoughts);
const authors = [...new Set(allThoughts.map((t) => t.author.username))];
const userProfiles = await Promise.all(
authors.map((username) => getUserProfile(username, token).catch(() => null))
);
const authorDetails = new Map<string, { avatarUrl?: string | null }>(
userProfiles
.filter((u): u is User => !!u)
.map((user) => [user.username, { avatarUrl: user.avatarUrl }])
);
const friends = (await getFriends(token)).users.map((user) => user.username);
const topFriendsData = me
? await getTopFriends(me.username, token).catch(() => ({ topFriends: [] }))
@@ -111,7 +98,6 @@ async function FeedPage({
<ThoughtThread
key={thought.id}
thought={thought}
authorDetails={authorDetails}
currentUser={me}
/>
))}

View File

@@ -1,6 +1,6 @@
import type { Metadata } from "next";
import { cookies } from "next/headers";
import { getMe, search, lookupRemoteActor, User } from "@/lib/api";
import { getMe, search, lookupRemoteActor } from "@/lib/api";
export async function generateMetadata({
searchParams,
@@ -51,13 +51,6 @@ export default async function SearchPage({ searchParams }: SearchPageProps) {
token ? getMe(token).catch(() => null) : null,
]);
const authorDetails = new Map<string, { avatarUrl?: string | null }>();
if (results) {
results.users.forEach((user: User) => {
authorDetails.set(user.username, { avatarUrl: user.avatarUrl });
});
}
return (
<div className="container mx-auto max-w-2xl p-4 sm:p-6">
<header className="my-6">
@@ -91,7 +84,6 @@ export default async function SearchPage({ searchParams }: SearchPageProps) {
<TabsContent value="thoughts">
<ThoughtList
thoughts={results.thoughts}
authorDetails={authorDetails}
currentUser={me}
/>
</TabsContent>

View File

@@ -1,7 +1,7 @@
// app/tags/[tagName]/page.tsx
import type { Metadata } from "next";
import { cookies } from "next/headers";
import { getThoughtsByTag, getUserProfile, getMe, Me, User } from "@/lib/api";
import { getThoughtsByTag, getMe, Me } from "@/lib/api";
export async function generateMetadata({
params,
@@ -49,16 +49,6 @@ export default async function TagPage({ params }: TagPageProps) {
const thoughtThreads = buildThoughtThreads(allThoughts);
const me = meResult.status === "fulfilled" ? (meResult.value as Me) : null;
const authors = [...new Set(allThoughts.map((t) => t.author.username))];
const userProfiles = await Promise.all(
authors.map((username) => getUserProfile(username, token).catch(() => null))
);
const authorDetails = new Map<string, { avatarUrl?: string | null }>(
userProfiles
.filter((u): u is User => !!u)
.map((user) => [user.username, { avatarUrl: user.avatarUrl }])
);
return (
<div className="container mx-auto max-w-2xl p-4 sm:p-6">
<header className="my-6">
@@ -72,7 +62,6 @@ export default async function TagPage({ params }: TagPageProps) {
<ThoughtThread
key={thought.id}
thought={thought}
authorDetails={authorDetails}
currentUser={me}
/>
))}

View File

@@ -3,10 +3,8 @@ import { cookies } from "next/headers";
import {
getThoughtById,
getThoughtThread,
getUserProfile,
getMe,
Me,
User,
ThoughtThread as ThoughtThreadType,
} from "@/lib/api";
import { ThoughtThread } from "@/components/thought-thread";
@@ -52,14 +50,6 @@ export async function generateMetadata({
};
}
function collectAuthors(thread: ThoughtThreadType): string[] {
const authors = new Set<string>([thread.author.username]);
for (const reply of thread.replies) {
collectAuthors(reply).forEach((author) => authors.add(author));
}
return Array.from(authors);
}
export default async function ThoughtPage({ params }: ThoughtPageProps) {
const { thoughtId } = await params;
const token = (await cookies()).get("auth_token")?.value ?? null;
@@ -76,20 +66,6 @@ export default async function ThoughtPage({ params }: ThoughtPageProps) {
const thread = threadResult.value;
const me = meResult.status === "fulfilled" ? (meResult.value as Me) : null;
// Fetch details for all authors in the thread efficiently
const authorUsernames = collectAuthors(thread);
const userProfiles = await Promise.all(
authorUsernames.map((username) =>
getUserProfile(username, token).catch(() => null)
)
);
const authorDetails = new Map<string, { avatarUrl?: string | null }>(
userProfiles
.filter((u): u is User => !!u)
.map((user) => [user.username, { avatarUrl: user.avatarUrl }])
);
return (
<div className="container mx-auto max-w-2xl p-4 sm:p-6">
<header className="my-6">
@@ -98,7 +74,6 @@ export default async function ThoughtPage({ params }: ThoughtPageProps) {
<main>
<ThoughtThread
thought={thread}
authorDetails={authorDetails}
currentUser={me}
/>
</main>

View File

@@ -126,9 +126,6 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
const fediverseHandle =
user.local && apiDomain ? `@${user.username}@${apiDomain}` : null;
const authorDetails = new Map<string, { avatarUrl?: string | null }>();
authorDetails.set(user.username, { avatarUrl: user.avatarUrl });
// Show who the profile owner follows (uses the already-fetched followingResult).
const friends =
followingResult.status === "fulfilled"
@@ -277,7 +274,6 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
<ThoughtThread
key={thought.id}
thought={thought}
authorDetails={authorDetails}
currentUser={me}
/>
))}