From 2610ac6d9e29ce1636124468f705e350b4d2980f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 9 Aug 2025 06:06:40 +0000 Subject: [PATCH] fix: prevent memory leaks in animated markdown components - Memoize context values in AppMarkdownStreaming to prevent unnecessary re-renders - Extract component functions in useMarkdownComponents using useCallback to prevent recreation - Add cleanup logic in TokenizedText to clear animated chunks when streaming finishes - Fix missing isStreamFinished dependency in TokenizedText useMemo - Memoize animation styles in animation-helpers to prevent object recreation These changes address critical memory leak sources that were causing 2GB+ memory usage growth. Co-Authored-By: nate@buster.so --- .../AppMarkdownStreaming.tsx | 24 +- .../AnimatedMarkdown/TokenizedText.tsx | 11 +- .../AnimatedMarkdown/animation-helpers.tsx | 16 +- .../useMarkdownComponents.tsx | 394 ++++++++++++------ 4 files changed, 306 insertions(+), 139 deletions(-) diff --git a/apps/web/src/components/ui/streaming/AppMarkdownStreaming/AppMarkdownStreaming.tsx b/apps/web/src/components/ui/streaming/AppMarkdownStreaming/AppMarkdownStreaming.tsx index 8f4e3e369..9dcc513ed 100644 --- a/apps/web/src/components/ui/streaming/AppMarkdownStreaming/AppMarkdownStreaming.tsx +++ b/apps/web/src/components/ui/streaming/AppMarkdownStreaming/AppMarkdownStreaming.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useContext } from 'react'; +import React, { useContext, useMemo } from 'react'; import { createContext } from 'react'; import { cn } from '@/lib/classMerge'; import { useMarkdownStreaming } from './useMarkdownStreaming'; @@ -31,16 +31,20 @@ const AppMarkdownStreaming = ({ isStreamFinished }); + const contextValue = useMemo( + () => ({ + animation, + animationDuration, + animationTimingFunction, + isStreamFinished, + isThrottleStreamingFinished: isFinished, + stripFormatting + }), + [animation, animationDuration, animationTimingFunction, isStreamFinished, isFinished, stripFormatting] + ); + return ( - +
{blockMatches.map((blockMatch, index) => { const Component = blockMatch.block.component; diff --git a/apps/web/src/components/ui/typography/AnimatedMarkdown/TokenizedText.tsx b/apps/web/src/components/ui/typography/AnimatedMarkdown/TokenizedText.tsx index f993a628d..e1a9c8678 100644 --- a/apps/web/src/components/ui/typography/AnimatedMarkdown/TokenizedText.tsx +++ b/apps/web/src/components/ui/typography/AnimatedMarkdown/TokenizedText.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useRef, useCallback } from 'react'; +import React, { useMemo, useRef, useCallback, useEffect } from 'react'; import { type MarkdownAnimation } from '../animation-common'; import { createAnimationStyle } from '../animation-common'; @@ -31,7 +31,7 @@ const TokenizedText: React.FC = React.memo( animationTimingFunction, isStreamFinished }), - [animation, animationDuration, animationTimingFunction] + [animation, animationDuration, animationTimingFunction, isStreamFinished] ); // Memoize the span creation function to avoid recreating it on every render @@ -99,6 +99,13 @@ const TokenizedText: React.FC = React.memo( return [baseText, ...animatedSpans]; }, [text, createSpan]); + useEffect(() => { + if (isStreamFinished) { + animatedChunksRef.current = []; + previousTextRef.current = ''; + } + }, [isStreamFinished]); + return <>{content}; } ); diff --git a/apps/web/src/components/ui/typography/AnimatedMarkdown/animation-helpers.tsx b/apps/web/src/components/ui/typography/AnimatedMarkdown/animation-helpers.tsx index 1462791b4..e1930314d 100644 --- a/apps/web/src/components/ui/typography/AnimatedMarkdown/animation-helpers.tsx +++ b/apps/web/src/components/ui/typography/AnimatedMarkdown/animation-helpers.tsx @@ -1,5 +1,5 @@ import type { AnimatedMarkdownProps } from './AnimatedMarkdown'; -import React from 'react'; +import React, { useMemo } from 'react'; import TokenizedText from './TokenizedText'; import { createAnimationStyle } from '../animation-common'; @@ -52,12 +52,17 @@ export const animateTokenizedText = ( // This else block might still wrap elements if they are not explicitly handled // by the ReactMarkdown components mapping (e.g. custom components not passing animateText to children) // For standard HTML elements, the `components` mapping should handle animation of children. + const animationStyle = useMemo( + () => createAnimationStyle(animationsProps), + [animationsProps.animation, animationsProps.animationDuration, animationsProps.animationTimingFunction, animationsProps.isStreamFinished] + ); + return ( @@ -65,12 +70,17 @@ export const animateTokenizedText = ( ); } + const animationStyle = useMemo( + () => createAnimationStyle(animationsProps), + [animationsProps.animation, animationsProps.animationDuration, animationsProps.animationTimingFunction, animationsProps.isStreamFinished] + ); + return ( diff --git a/apps/web/src/components/ui/typography/AnimatedMarkdown/useMarkdownComponents.tsx b/apps/web/src/components/ui/typography/AnimatedMarkdown/useMarkdownComponents.tsx index 02ba641fd..3f33cc2b8 100644 --- a/apps/web/src/components/ui/typography/AnimatedMarkdown/useMarkdownComponents.tsx +++ b/apps/web/src/components/ui/typography/AnimatedMarkdown/useMarkdownComponents.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useMemo, useCallback } from 'react'; import { type MarkdownAnimation, type MarkdownAnimationTimingFunction } from '../animation-common'; import { type Components } from 'react-markdown'; import { @@ -49,132 +49,278 @@ export const useMarkdownComponents = ({ }; }, [animation, animationDuration, animationTimingFunction, isStreamFinished, stripFormatting]); + const ParagraphWrapper = useCallback( + ({ children, className, style }: any) => ( + + {children} + + ), + [commonProps] + ); + + const H1Wrapper = useCallback( + ({ children, className, style }: any) => ( + + {children} + + ), + [commonProps] + ); + + const H2Wrapper = useCallback( + ({ children, className, style }: any) => ( + + {children} + + ), + [commonProps] + ); + + const H3Wrapper = useCallback( + ({ children, className, style }: any) => ( + + {children} + + ), + [commonProps] + ); + + const H4Wrapper = useCallback( + ({ children, className, style }: any) => ( + + {children} + + ), + [commonProps] + ); + + const H5Wrapper = useCallback( + ({ children, className, style }: any) => ( + + {children} + + ), + [commonProps] + ); + + const H6Wrapper = useCallback( + ({ children, className, style }: any) => ( + + {children} + + ), + [commonProps] + ); + + const BlockquoteWrapper = useCallback( + ({ children, className, style }: any) => ( + + {children} + + ), + [commonProps] + ); + + const StrongWrapper = useCallback( + ({ children, className, style }: any) => ( + + {children} + + ), + [commonProps] + ); + + const EmphasisWrapper = useCallback( + ({ children, className, style }: any) => ( + + {children} + + ), + [commonProps] + ); + + const DeleteWrapper = useCallback( + ({ children, className, style }: any) => ( + + {children} + + ), + [commonProps] + ); + + const LinkWrapper = useCallback( + ({ children, className, style, href }: any) => ( + + {children} + + ), + [commonProps] + ); + + const ImageWrapper = useCallback( + ({ className, style, src, alt }: any) => ( + + ), + [commonProps] + ); + + const HorizontalRuleWrapper = useCallback( + ({ className, style }: any) => ( + + ), + [commonProps] + ); + + const TableWrapper = useCallback( + ({ children, className, style }: any) => ( + + {children} + + ), + [commonProps] + ); + + const TableHeadWrapper = useCallback( + ({ children, className, style }: any) => ( + + {children} + + ), + [commonProps] + ); + + const TableBodyWrapper = useCallback( + ({ children, className, style }: any) => ( + + {children} + + ), + [commonProps] + ); + + const TableRowWrapper = useCallback( + ({ children, className, style }: any) => ( + + {children} + + ), + [commonProps] + ); + + const UnorderedListWrapper = useCallback( + ({ children, className, style }: any) => ( + + {children} + + ), + [commonProps] + ); + + const OrderedListWrapper = useCallback( + ({ children, className, style, ...rest }: any) => ( + + {children} + + ), + [commonProps] + ); + + const ListItemWrapper = useCallback( + ({ children, className, style }: any) => ( + + {children} + + ), + [commonProps] + ); + + const TableCellWrapper = useCallback( + ({ children, className, style }: any) => ( + + {children} + + ), + [] + ); + + const TableHeaderCellWrapper = useCallback( + ({ children, className, style }: any) => ( + + {children} + + ), + [] + ); + + const BreakWrapper = useCallback( + ({ className, style }: any) => , + [] + ); + + const CodeWrapper = useCallback( + ({ children, className, style, ...rest }: any) => ( + + {children} + + ), + [commonProps] + ); + const components: Components = useMemo(() => { return { - // Components with animations - p: ({ children, className, style }) => ( - - {children} - - ), - h1: ({ children, className, style }) => ( - - {children} - - ), - h2: ({ children, className, style }) => ( - - {children} - - ), - h3: ({ children, className, style }) => ( - - {children} - - ), - h4: ({ children, className, style }) => ( - - {children} - - ), - h5: ({ children, className, style }) => ( - - {children} - - ), - h6: ({ children, className, style }) => ( - - {children} - - ), - blockquote: ({ children, className, style }) => ( - - {children} - - ), - strong: ({ children, className, style }) => ( - - {children} - - ), - em: ({ children, className, style }) => ( - - {children} - - ), - del: ({ children, className, style }) => ( - - {children} - - ), - a: ({ children, className, style, href }) => ( - - {children} - - ), - img: ({ className, style, src, alt }) => ( - - ), - hr: ({ className, style }) => ( - - ), - table: ({ children, className, style }) => ( - - {children} - - ), - thead: ({ children, className, style }) => ( - - {children} - - ), - tbody: ({ children, className, style }) => ( - - {children} - - ), - tr: ({ children, className, style }) => ( - - {children} - - ), - - ul: ({ children, className, style }) => ( - - {children} - - ), - ol: ({ children, className, style, ...rest }) => ( - - {children} - - ), - li: ({ children, className, style }) => { - return ( - - {children} - - ); - }, - td: ({ children, className, style }) => ( - - {children} - - ), - th: ({ children, className, style }) => ( - - {children} - - ), - br: ({ className, style }) => , - code: ({ children, className, style, ...rest }) => ( - //we can assume that code is inline if it reach to this point - - {children} - - ) + p: ParagraphWrapper, + h1: H1Wrapper, + h2: H2Wrapper, + h3: H3Wrapper, + h4: H4Wrapper, + h5: H5Wrapper, + h6: H6Wrapper, + blockquote: BlockquoteWrapper, + strong: StrongWrapper, + em: EmphasisWrapper, + del: DeleteWrapper, + a: LinkWrapper, + img: ImageWrapper, + hr: HorizontalRuleWrapper, + table: TableWrapper, + thead: TableHeadWrapper, + tbody: TableBodyWrapper, + tr: TableRowWrapper, + ul: UnorderedListWrapper, + ol: OrderedListWrapper, + li: ListItemWrapper, + td: TableCellWrapper, + th: TableHeaderCellWrapper, + br: BreakWrapper, + code: CodeWrapper }; - }, [commonProps]); + }, [ + ParagraphWrapper, + H1Wrapper, + H2Wrapper, + H3Wrapper, + H4Wrapper, + H5Wrapper, + H6Wrapper, + BlockquoteWrapper, + StrongWrapper, + EmphasisWrapper, + DeleteWrapper, + LinkWrapper, + ImageWrapper, + HorizontalRuleWrapper, + TableWrapper, + TableHeadWrapper, + TableBodyWrapper, + TableRowWrapper, + UnorderedListWrapper, + OrderedListWrapper, + ListItemWrapper, + TableCellWrapper, + TableHeaderCellWrapper, + BreakWrapper, + CodeWrapper + ]); return { components, commonProps }; };