mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-19 04:14:46 +08:00
fix: optimize animations to prevent browser freeze with many research steps (#630)
Fixes #570 where browser freezes when research plan has 8+ steps. Performance optimizations: - Add animation throttling: only animate first 10 activity items - Reduce animation durations (0.4s → 0.3s for activities, 0.2s → 0.15s for results) - Remove scale animations (GPU-intensive) from search results - Limit displayed results (20 pages, 10 images max) - Add conditional animations based on item index - Cap animation delays to prevent excessive staggering - Add React.memo to ActivityMessage and ActivityListItem components These changes significantly improve performance when rendering multiple research steps while maintaining visual appeal for smaller lists.
This commit is contained in:
@@ -7,7 +7,7 @@ import { LRUCache } from "lru-cache";
|
|||||||
import { BookOpenText, FileText, PencilRuler, Search } from "lucide-react";
|
import { BookOpenText, FileText, PencilRuler, Search } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||||
import { docco } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
import { docco } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||||
import { dark } from "react-syntax-highlighter/dist/esm/styles/prism";
|
import { dark } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||||
@@ -31,6 +31,10 @@ import { useMessage, useStore } from "~/core/store";
|
|||||||
import { parseJSON } from "~/core/utils";
|
import { parseJSON } from "~/core/utils";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
// Performance optimization constants
|
||||||
|
const MAX_ANIMATED_ITEMS = 10; // Only animate first 10 items
|
||||||
|
const ANIMATION_DELAY_MULTIPLIER = 0.05; // Reduced delay between animations
|
||||||
|
|
||||||
export function ResearchActivitiesBlock({
|
export function ResearchActivitiesBlock({
|
||||||
className,
|
className,
|
||||||
researchId,
|
researchId,
|
||||||
@@ -42,27 +46,36 @@ export function ResearchActivitiesBlock({
|
|||||||
state.researchActivityIds.get(researchId),
|
state.researchActivityIds.get(researchId),
|
||||||
)!;
|
)!;
|
||||||
const ongoing = useStore((state) => state.ongoingResearchId === researchId);
|
const ongoing = useStore((state) => state.ongoingResearchId === researchId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ul className={cn("flex flex-col py-4", className)}>
|
<ul className={cn("flex flex-col py-4", className)}>
|
||||||
{activityIds.map(
|
{activityIds.map(
|
||||||
(activityId, i) =>
|
(activityId, i) => {
|
||||||
i !== 0 && (
|
if (i === 0) return null;
|
||||||
|
|
||||||
|
// Performance optimization: limit animations for large lists
|
||||||
|
const shouldAnimate = i < MAX_ANIMATED_ITEMS;
|
||||||
|
const animationDelay = shouldAnimate ? Math.min(i * ANIMATION_DELAY_MULTIPLIER, 0.5) : 0;
|
||||||
|
|
||||||
|
return (
|
||||||
<motion.li
|
<motion.li
|
||||||
key={activityId}
|
key={activityId}
|
||||||
style={{ transition: "all 0.4s ease-out" }}
|
style={{ transition: shouldAnimate ? "all 0.3s ease-out" : "none" }}
|
||||||
initial={{ opacity: 0, y: 24 }}
|
initial={shouldAnimate ? { opacity: 0, y: 24 } : { opacity: 1, y: 0 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{
|
transition={shouldAnimate ? {
|
||||||
duration: 0.4,
|
duration: 0.3, // Reduced from 0.4
|
||||||
|
delay: animationDelay,
|
||||||
ease: "easeOut",
|
ease: "easeOut",
|
||||||
}}
|
} : undefined}
|
||||||
>
|
>
|
||||||
<ActivityMessage messageId={activityId} />
|
<ActivityMessage messageId={activityId} />
|
||||||
<ActivityListItem messageId={activityId} />
|
<ActivityListItem messageId={activityId} />
|
||||||
{i !== activityIds.length - 1 && <hr className="my-8" />}
|
{i !== activityIds.length - 1 && <hr className="my-8" />}
|
||||||
</motion.li>
|
</motion.li>
|
||||||
),
|
);
|
||||||
|
},
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
{ongoing && <LoadingAnimation className="mx-4 my-12" />}
|
{ongoing && <LoadingAnimation className="mx-4 my-12" />}
|
||||||
@@ -70,7 +83,7 @@ export function ResearchActivitiesBlock({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ActivityMessage({ messageId }: { messageId: string }) {
|
const ActivityMessage = React.memo(({ messageId }: { messageId: string }) => {
|
||||||
const message = useMessage(messageId);
|
const message = useMessage(messageId);
|
||||||
if (message?.agent && message.content) {
|
if (message?.agent && message.content) {
|
||||||
if (message.agent !== "reporter" && message.agent !== "planner") {
|
if (message.agent !== "reporter" && message.agent !== "planner") {
|
||||||
@@ -84,9 +97,10 @@ function ActivityMessage({ messageId }: { messageId: string }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
});
|
||||||
|
ActivityMessage.displayName = "ActivityMessage";
|
||||||
|
|
||||||
function ActivityListItem({ messageId }: { messageId: string }) {
|
const ActivityListItem = React.memo(({ messageId }: { messageId: string }) => {
|
||||||
const message = useMessage(messageId);
|
const message = useMessage(messageId);
|
||||||
if (message) {
|
if (message) {
|
||||||
if (!message.isStreaming && message.toolCalls?.length) {
|
if (!message.isStreaming && message.toolCalls?.length) {
|
||||||
@@ -109,7 +123,8 @@ function ActivityListItem({ messageId }: { messageId: string }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
});
|
||||||
|
ActivityListItem.displayName = "ActivityListItem";
|
||||||
|
|
||||||
const __pageCache = new LRUCache<string, string>({ max: 100 });
|
const __pageCache = new LRUCache<string, string>({ max: 100 });
|
||||||
type SearchResult =
|
type SearchResult =
|
||||||
@@ -187,38 +202,46 @@ function WebSearchToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
|||||||
))}
|
))}
|
||||||
{pageResults
|
{pageResults
|
||||||
.filter((result) => result.type === "page")
|
.filter((result) => result.type === "page")
|
||||||
.map((searchResult, i) => (
|
.slice(0, 20) // Limit displayed results for performance
|
||||||
<motion.li
|
.map((searchResult, i) => {
|
||||||
key={`search-result-${i}`}
|
const shouldAnimate = i < 6; // Only animate first 6 results
|
||||||
className="text-muted-foreground bg-accent flex max-w-40 gap-2 rounded-md px-2 py-1 text-sm"
|
return (
|
||||||
initial={{ opacity: 0, y: 10, scale: 0.66 }}
|
<motion.li
|
||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
key={`search-result-${i}`}
|
||||||
transition={{
|
className="text-muted-foreground bg-accent flex max-w-40 gap-2 rounded-md px-2 py-1 text-sm"
|
||||||
duration: 0.2,
|
initial={shouldAnimate ? { opacity: 0, y: 10 } : { opacity: 1, y: 0 }}
|
||||||
delay: i * 0.1,
|
animate={{ opacity: 1, y: 0 }}
|
||||||
ease: "easeOut",
|
transition={shouldAnimate ? {
|
||||||
}}
|
duration: 0.15, // Reduced from 0.2
|
||||||
>
|
delay: Math.min(i * 0.05, 0.3), // Cap delay at 0.3s
|
||||||
<FavIcon
|
ease: "easeOut",
|
||||||
className="mt-1"
|
} : undefined}
|
||||||
url={searchResult.url}
|
>
|
||||||
title={searchResult.title}
|
<FavIcon
|
||||||
/>
|
className="mt-1"
|
||||||
<a href={searchResult.url} target="_blank">
|
url={searchResult.url}
|
||||||
{searchResult.title}
|
title={searchResult.title}
|
||||||
</a>
|
/>
|
||||||
</motion.li>
|
<a href={searchResult.url} target="_blank">
|
||||||
))}
|
{searchResult.title}
|
||||||
{imageResults.map((searchResult, i) => (
|
</a>
|
||||||
<motion.li
|
</motion.li>
|
||||||
key={`search-result-${i}`}
|
);
|
||||||
initial={{ opacity: 0, y: 10, scale: 0.66 }}
|
})}
|
||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
{imageResults
|
||||||
transition={{
|
.slice(0, 10) // Limit displayed images for performance
|
||||||
duration: 0.2,
|
.map((searchResult, i) => {
|
||||||
delay: i * 0.1,
|
const shouldAnimate = i < 4; // Only animate first 4 images
|
||||||
ease: "easeOut",
|
return (
|
||||||
}}
|
<motion.li
|
||||||
|
key={`search-result-${i}`}
|
||||||
|
initial={shouldAnimate ? { opacity: 0, y: 10 } : { opacity: 1, y: 0 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={shouldAnimate ? {
|
||||||
|
duration: 0.15,
|
||||||
|
delay: Math.min(i * 0.05, 0.2),
|
||||||
|
ease: "easeOut",
|
||||||
|
} : undefined}
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
className="flex flex-col gap-2 overflow-hidden rounded-md opacity-75 transition-opacity duration-300 hover:opacity-100"
|
className="flex flex-col gap-2 overflow-hidden rounded-md opacity-75 transition-opacity duration-300 hover:opacity-100"
|
||||||
@@ -234,7 +257,8 @@ function WebSearchToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
|||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</motion.li>
|
</motion.li>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -263,10 +287,10 @@ function CrawlToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
|||||||
<ul className="mt-2 flex flex-wrap gap-4">
|
<ul className="mt-2 flex flex-wrap gap-4">
|
||||||
<motion.li
|
<motion.li
|
||||||
className="text-muted-foreground bg-accent flex h-40 w-40 gap-2 rounded-md px-2 py-1 text-sm"
|
className="text-muted-foreground bg-accent flex h-40 w-40 gap-2 rounded-md px-2 py-1 text-sm"
|
||||||
initial={{ opacity: 0, y: 10, scale: 0.66 }}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{
|
transition={{
|
||||||
duration: 0.2,
|
duration: 0.15, // Reduced for better performance
|
||||||
ease: "easeOut",
|
ease: "easeOut",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -320,22 +344,25 @@ function RetrieverToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
|||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
{documents?.map((doc, i) => (
|
{documents?.map((doc, i) => {
|
||||||
<motion.li
|
const shouldAnimate = i < 4; // Only animate first 4 documents
|
||||||
key={`search-result-${i}`}
|
return (
|
||||||
className="text-muted-foreground bg-accent flex max-w-40 gap-2 rounded-md px-2 py-1 text-sm"
|
<motion.li
|
||||||
initial={{ opacity: 0, y: 10, scale: 0.66 }}
|
key={`search-result-${i}`}
|
||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
className="text-muted-foreground bg-accent flex max-w-40 gap-2 rounded-md px-2 py-1 text-sm"
|
||||||
transition={{
|
initial={shouldAnimate ? { opacity: 0, y: 10 } : { opacity: 1, y: 0 }}
|
||||||
duration: 0.2,
|
animate={{ opacity: 1, y: 0 }}
|
||||||
delay: i * 0.1,
|
transition={shouldAnimate ? {
|
||||||
ease: "easeOut",
|
duration: 0.15,
|
||||||
}}
|
delay: Math.min(i * 0.05, 0.2),
|
||||||
>
|
ease: "easeOut",
|
||||||
<FileText size={32} />
|
} : undefined}
|
||||||
{doc.title} (chunk-{i},size-{doc.content.length})
|
>
|
||||||
</motion.li>
|
<FileText size={32} />
|
||||||
))}
|
{doc.title} (chunk-{i},size-{doc.content.length})
|
||||||
|
</motion.li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user