From fa2641bdedef25bab9598f72a3a2761cb6980738 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:25:01 +0000 Subject: [PATCH 1/6] Initial plan From ceb4265120cd0262d71c25ed01de93a6d27da14e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:30:06 +0000 Subject: [PATCH 2/6] perf: optimize ArticleDetailPage and ArticleActionBar rendering performance - Memoize sanitizedContent with useMemo (prevents DOMPurify on every render) - Wrap ArticleActionBar with React.memo (prevents re-renders during streaming) - Move annotationColorClass to module-level constant - Use articleRef pattern for handleLoadFullContent to avoid stale closures - Memoize handleShare with useCallback in ArticleActionBar - Memoize back navigation handler with useCallback Co-authored-by: chiga0 <24784430+chiga0@users.noreply.github.com> --- .../ArticleView/ArticleActionBar.tsx | 10 +++--- src/pages/ArticleDetailPage.tsx | 33 +++++++++++-------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/components/ArticleView/ArticleActionBar.tsx b/src/components/ArticleView/ArticleActionBar.tsx index 90e9548..a09d2a2 100644 --- a/src/components/ArticleView/ArticleActionBar.tsx +++ b/src/components/ArticleView/ArticleActionBar.tsx @@ -2,7 +2,7 @@ * ArticleActionBar - Fixed bottom banner with Favorite, Translate, AI Summary, Annotate, and Share buttons. */ -import { useState } from 'react'; +import { useState, useCallback, memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Heart, Languages, Sparkles, Loader2, Highlighter, Share2 } from 'lucide-react'; @@ -19,7 +19,7 @@ interface ArticleActionBarProps { onToggleAnnotate: () => void; } -export function ArticleActionBar({ +export const ArticleActionBar = memo(function ArticleActionBar({ isFavorite, isTranslating, isSummarizing, @@ -35,7 +35,7 @@ export function ArticleActionBar({ const [copied, setCopied] = useState(false); const [shareError, setShareError] = useState(false); - const handleShare = async () => { + const handleShare = useCallback(async () => { const url = articleLink || window.location.href; const title = articleTitle || document.title; if (navigator.share) { @@ -58,7 +58,7 @@ export function ArticleActionBar({ setTimeout(() => setShareError(false), 1500); } } - }; + }, [articleLink, articleTitle]); return (
); -} +}); diff --git a/src/pages/ArticleDetailPage.tsx b/src/pages/ArticleDetailPage.tsx index 1273e48..f50e497 100644 --- a/src/pages/ArticleDetailPage.tsx +++ b/src/pages/ArticleDetailPage.tsx @@ -49,6 +49,13 @@ function parseContentSegments(html: string): { html: string; text: string }[] { /** Maximum milliseconds to wait for an AI operation before auto-aborting. */ const AI_OPERATION_TIMEOUT_MS = 60_000; +const ANNOTATION_COLOR_CLASS: Record = { + yellow: 'bg-yellow-100 border-yellow-300 dark:bg-yellow-900/30 dark:border-yellow-700', + green: 'bg-green-100 border-green-300 dark:bg-green-900/30 dark:border-green-700', + blue: 'bg-blue-100 border-blue-300 dark:bg-blue-900/30 dark:border-blue-700', + pink: 'bg-pink-100 border-pink-300 dark:bg-pink-900/30 dark:border-pink-700', +}; + export function ArticleDetailPage() { const { article: loaderArticle, feed } = useLoaderData() as ArticleDetailLoaderData; const navigate = useNavigate(); @@ -66,6 +73,8 @@ export function ArticleDetailPage() { const [annotationNote, setAnnotationNote] = useState(''); const [annotationColor, setAnnotationColor] = useState('yellow'); const contentRef = useRef(null); + const articleRef = useRef(article); + articleRef.current = article; const [translations, setTranslations] = useState>({}); const [translatingIndex, setTranslatingIndex] = useState(-1); const [isTranslating, setIsTranslating] = useState(false); @@ -132,22 +141,23 @@ export function ArticleDetailPage() { // Manual retry: fetch full content from original URL const handleLoadFullContent = useCallback(async () => { - if (!article.link) return; + if (!articleRef.current.link) return; setIsLoadingFullContent(true); setFullContentError(null); try { - const updated = await fetchAndCacheFullContent(article); + const updated = await fetchAndCacheFullContent(articleRef.current); setArticle(updated); } catch { setFullContentError('Failed to load full article content'); } finally { setIsLoadingFullContent(false); } - }, [article]); + }, []); - const sanitizedContent = article.content - ? sanitizeHTML(article.content) - : article.summary || ''; + const sanitizedContent = useMemo( + () => (article.content ? sanitizeHTML(article.content) : article.summary || ''), + [article.content, article.summary], + ); const segments = useMemo(() => parseContentSegments(sanitizedContent), [sanitizedContent]); @@ -294,19 +304,14 @@ export function ArticleDetailPage() { setAnnotations((prev) => prev.filter((a) => a.id !== id)); }, []); - const annotationColorClass: Record = { - yellow: 'bg-yellow-100 border-yellow-300 dark:bg-yellow-900/30 dark:border-yellow-700', - green: 'bg-green-100 border-green-300 dark:bg-green-900/30 dark:border-green-700', - blue: 'bg-blue-100 border-blue-300 dark:bg-blue-900/30 dark:border-blue-700', - pink: 'bg-pink-100 border-pink-300 dark:bg-pink-900/30 dark:border-pink-700', - }; + const handleBack = useCallback(() => navigate(-1), [navigate]); return (
{/* Navigation */}