feat(optimizations): adds memoization for performance optimization

Add currentUserName to video API responses and session for display in comments. Also adds memoization for performance optimization and implements Page Visibility API to pause comment polling when tab is hidden.
This commit is contained in:
Yusuf İpek
2026-02-14 16:21:52 +03:00
parent 20005f1a15
commit 1c7ee70815
6 changed files with 105 additions and 42 deletions
@@ -70,6 +70,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
...video,
isAuthenticated: !!session?.user?.id,
currentUserId: session?.user?.id || null,
currentUserName: session?.user?.name || null,
});
return withCacheControl(response, 'private, no-cache');
+1
View File
@@ -76,6 +76,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
},
isAuthenticated: !!session?.user?.id,
currentUserId: session?.user?.id || null,
currentUserName: session?.user?.name || null,
canComment: access.hasAccess,
});
+10 -4
View File
@@ -10,6 +10,7 @@
"@base-ui/react": "^1.1.0",
"@prisma/adapter-pg": "^7.3.0",
"@prisma/client": "^7.3.0",
"@types/react-window": "^2.0.0",
"bcryptjs": "^3.0.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -25,7 +26,7 @@
"radix-ui": "^1.4.3",
"react": "19.2.3",
"react-dom": "19.2.3",
"shadcn": "^3.8.3",
"react-window": "^2.2.7",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"tw-animate-css": "^1.4.0",
@@ -41,6 +42,7 @@
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.1.6",
"shadcn": "^3.8.3",
"tailwindcss": "^4",
"typescript": "^5",
},
@@ -697,6 +699,8 @@
"@types/react-dom": ["@types/[email protected]", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@types/react-window": ["@types/[email protected]", "", { "dependencies": { "react-window": "*" } }, "sha512-E8hMDtImEpMk1SjswSvqoSmYvk7GEtyVaTa/GJV++FdDNuMVVEzpAClyJ0nqeKYBrMkGiyH6M1+rPLM0Nu1exQ=="],
"@types/statuses": ["@types/[email protected]", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="],
"@types/validate-npm-package-name": ["@types/[email protected]", "", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="],
@@ -1163,7 +1167,7 @@
"hermes-parser": ["[email protected]", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
"hono": ["[email protected].4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="],
"hono": ["[email protected].7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="],
"http-errors": ["[email protected]", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
@@ -1577,6 +1581,8 @@
"react-style-singleton": ["[email protected]", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
"react-window": ["[email protected]", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-SH5nvfUQwGHYyriDUAOt7wfPsfG9Qxd6OdzQxl5oQ4dsSsUicqQvjV7dR+NqZ4coY0fUn3w1jnC5PwzIUWEg5w=="],
"readdirp": ["[email protected]", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"recast": ["[email protected]", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="],
@@ -1863,10 +1869,10 @@
"@modelcontextprotocol/sdk/ajv": ["[email protected]", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"@modelcontextprotocol/sdk/hono": ["[email protected]", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="],
"@next/eslint-plugin-next/fast-glob": ["[email protected]", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="],
"@prisma/dev/hono": ["[email protected]", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="],
"@prisma/engines/@prisma/get-platform": ["@prisma/[email protected]", "", { "dependencies": { "@prisma/debug": "7.3.0" } }, "sha512-N7c6m4/I0Q6JYmWKP2RCD/sM9eWiyCPY98g5c0uEktObNSZnugW2U/PO+pwL0UaqzxqTXt7gTsYsb0FnMnJNbg=="],
"@prisma/fetch-engine/@prisma/get-platform": ["@prisma/[email protected]", "", { "dependencies": { "@prisma/debug": "7.3.0" } }, "sha512-N7c6m4/I0Q6JYmWKP2RCD/sM9eWiyCPY98g5c0uEktObNSZnugW2U/PO+pwL0UaqzxqTXt7gTsYsb0FnMnJNbg=="],
+89 -38
View File
@@ -1,6 +1,7 @@
'use client';
import { useState, useRef, useCallback, useEffect } from 'react';
import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
import { List } from 'react-window';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { toast } from 'sonner';
@@ -127,6 +128,7 @@ interface VideoData {
versions: (Version & { comments: Comment[] })[];
isAuthenticated: boolean;
currentUserId: string | null;
currentUserName: string | null;
canComment?: boolean;
}
@@ -241,6 +243,16 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
const projectId = propProjectId || video?.projectId;
// Cursor idle detection: hide overlay when cursor idle for 3s while playing
// Memoize version selection handler to prevent recreating on each render
const handleVersionSelect = useCallback((versionId: string) => {
setActiveVersionId(versionId);
}, []);
// Memoize toggle show resolved handler
const handleToggleShowResolved = useCallback(() => {
setShowResolved(prev => !prev);
}, []);
const handleVideoMouseMove = useCallback(() => {
setCursorIdle(false);
if (cursorIdleTimerRef.current) clearTimeout(cursorIdleTimerRef.current);
@@ -260,8 +272,9 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
};
}, []);
// Determine current user ID for permission checks
// Determine current user info for permission checks and comment display
const currentUserId = video?.currentUserId || null;
const currentUserName = video?.currentUserName || null;
const apiBasePath = mode === 'dashboard'
? `/api/projects/${propProjectId}/videos/${videoId}`
@@ -295,12 +308,52 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
fetchVideo();
}, [apiBasePath, mode]);
const activeVersion = video?.versions?.find((v) => v.id === activeVersionId) ||
video?.versions?.find((v) => v.isActive) ||
video?.versions?.[0];
const comments = activeVersion?.comments || [];
const filteredComments = comments.filter((c) => showResolved || !c.isResolved);
const duration = videoDuration || activeVersion?.duration || 0;
// Memoize active version lookup to avoid recalculating on every render
const activeVersion = useMemo(() => {
return video?.versions?.find((v) => v.id === activeVersionId) ||
video?.versions?.find((v) => v.isActive) ||
video?.versions?.[0];
}, [video?.versions, activeVersionId]);
// Memoize comments array
const comments = useMemo(() => {
return activeVersion?.comments || [];
}, [activeVersion]);
// Memoize filtered comments to avoid filtering on every render
const filteredComments = useMemo(() => {
return comments.filter((c) => showResolved || !c.isResolved);
}, [comments, showResolved]);
// Memoize sorted comments to avoid sorting on every render
const sortedComments = useMemo(() => {
return [...filteredComments].sort((a, b) => a.timestamp - b.timestamp);
}, [filteredComments]);
// Memoize duration computation
const duration = useMemo(() => {
return videoDuration || activeVersion?.duration || 0;
}, [videoDuration, activeVersion?.duration]);
// Memoize embed URL calculation to avoid recalculating on every render
const embedUrl = useMemo(() => {
if (!activeVersion) return '';
if (activeVersion.providerId === 'youtube') {
return `https://www.youtube.com/embed/${activeVersion.videoId}?enablejsapi=1&rel=0&modestbranding=1&controls=0&showinfo=0&iv_load_policy=3&disablekb=1`;
}
if (activeVersion.providerId === 'vimeo') {
return `https://player.vimeo.com/video/${activeVersion.videoId}`;
}
try {
const url = new URL(activeVersion.originalUrl);
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
return '';
}
return activeVersion.originalUrl;
} catch {
return '';
}
}, [activeVersion]);
useEffect(() => {
if (!projectId) return;
@@ -734,7 +787,7 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
voiceDuration: voiceData?.duration ?? null,
isResolved: false,
createdAt: new Date().toISOString(),
author: isGuest ? null : { id: 'current-user', name: null, image: null },
author: isGuest ? null : { id: 'current-user', name: currentUserName, image: null },
guestName: isGuest ? guestName : null,
tag: availableTags.find(t => t.id === selectedTagId) || null,
replies: [],
@@ -1085,7 +1138,7 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
voiceUrl: voiceData?.url ?? null,
voiceDuration: voiceData?.duration ?? null,
createdAt: new Date().toISOString(),
author: isGuest ? null : { id: 'current-user', name: null, image: null },
author: isGuest ? null : { id: 'current-user', name: currentUserName, image: null },
guestName: isGuest ? guestName : null,
tag: null,
};
@@ -1352,11 +1405,16 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
}
}, [activeVersionId, video]);
// Comment polling with Page Visibility API to pause when tab is hidden
useEffect(() => {
if (!activeVersion) return;
const interval = setInterval(async () => {
let intervalId: ReturnType<typeof setInterval> | null = null;
let isPageVisible = true;
const poll = async () => {
try {
if (isMutatingRef.current) return;
if (isMutatingRef.current || !isPageVisible) return;
const res = await fetch(apiBasePath, { cache: 'no-store' });
if (res.ok) {
@@ -1366,8 +1424,22 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
}
}
} catch { /* silent */ }
}, 10000);
return () => clearInterval(interval);
};
// Start polling
intervalId = setInterval(poll, 10000);
// Handle page visibility change
const handleVisibilityChange = () => {
isPageVisible = document.visibilityState === 'visible';
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
if (intervalId) clearInterval(intervalId);
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [activeVersion, apiBasePath]);
const handleNewVersionUrlChange = (url: string) => {
@@ -1472,23 +1544,6 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
}
};
const getEmbedUrl = (version: Version) => {
if (version.providerId === 'youtube') {
return `https://www.youtube.com/embed/${version.videoId}?enablejsapi=1&rel=0&modestbranding=1&controls=0&showinfo=0&iv_load_policy=3&disablekb=1`;
}
if (version.providerId === 'vimeo') {
return `https://player.vimeo.com/video/${version.videoId}`;
}
try {
const url = new URL(version.originalUrl);
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
return '';
}
return version.originalUrl;
} catch {
return '';
}
};
const containerHeight = 'h-screen';
const backHref = mode === 'dashboard'
@@ -1630,8 +1685,6 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
);
}
const embedUrl = getEmbedUrl(activeVersion);
return (
<div
className={cn(containerHeight, 'flex flex-col bg-background overflow-hidden')}
@@ -1671,7 +1724,7 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
{video.versions.map((version) => (
<DropdownMenuItem
key={version.id}
onClick={() => setActiveVersionId(version.id)}
onClick={() => handleVersionSelect(version.id)}
>
<Badge
variant={version.id === activeVersionId ? 'default' : 'secondary'}
@@ -1988,7 +2041,7 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
<span className="font-medium">Comments</span>
<Badge variant="secondary">{comments.length}</Badge>
</div>
<Button variant="ghost" size="sm" onClick={() => setShowResolved(!showResolved)}>
<Button variant="ghost" size="sm" onClick={handleToggleShowResolved}>
{showResolved ? 'Hide' : 'Show'} Resolved
</Button>
</div>
@@ -2001,9 +2054,7 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi
<p className="text-sm">Be the first to leave feedback!</p>
</div>
) : (
filteredComments
.sort((a, b) => a.timestamp - b.timestamp)
.map((comment) => {
sortedComments.map((comment) => {
const authorName =
comment.author?.name || comment.guestName || 'Anonymous';
const isEditing = editingCommentId === comment.id;
+2
View File
@@ -62,12 +62,14 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
async session({ session, token }) {
if (token.sub && session.user) {
session.user.id = token.sub;
session.user.name = token.name || null;
}
return session;
},
async jwt({ token, user }) {
if (user) {
token.sub = user.id;
token.name = user.name;
}
return token;
},
+2
View File
@@ -23,6 +23,7 @@
"@base-ui/react": "^1.1.0",
"@prisma/adapter-pg": "^7.3.0",
"@prisma/client": "^7.3.0",
"@types/react-window": "^2.0.0",
"bcryptjs": "^3.0.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -38,6 +39,7 @@
"radix-ui": "^1.4.3",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-window": "^2.2.7",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"tw-animate-css": "^1.4.0",