11import type { ReactNode } from "react" ;
2- import React , { useState , useEffect } from "react" ;
2+ import React , { useMemo } from "react" ;
33import { Mermaid } from "./Mermaid" ;
44import {
55 getShikiHighlighter ,
66 mapToShikiLang ,
77 SHIKI_THEME ,
88} from "@/utils/highlighting/shikiHighlighter" ;
99import { CopyButton } from "@/components/ui/CopyButton" ;
10+ import { useIntersectionHighlight } from "@/hooks/useIntersectionHighlight" ;
1011
1112interface CodeProps {
1213 node ?: unknown ;
@@ -58,21 +59,20 @@ function extractShikiLines(html: string): string[] {
5859}
5960
6061/**
61- * CodeBlock component with async Shiki highlighting
62- * Displays code with line numbers in a CSS grid
62+ * CodeBlock component with lazy async Shiki highlighting
63+ * Displays code with line numbers in a CSS grid.
64+ * Highlighting is deferred until the block enters the viewport.
6365 */
6466const CodeBlock : React . FC < CodeBlockProps > = ( { code, language } ) => {
65- const [ highlightedLines , setHighlightedLines ] = useState < string [ ] | null > ( null ) ;
66-
6767 // Split code into lines, removing trailing empty line
68- const plainLines = code
69- . split ( "\n" )
70- . filter ( ( line , idx , arr ) => idx < arr . length - 1 || line !== "" ) ;
71-
72- useEffect ( ( ) => {
73- let cancelled = false ;
68+ const plainLines = useMemo (
69+ ( ) => code . split ( "\n" ) . filter ( ( line , idx , arr ) => idx < arr . length - 1 || line !== "" ) ,
70+ [ code ]
71+ ) ;
7472
75- async function highlight ( ) {
73+ // Lazy highlight when code block becomes visible
74+ const { result : highlightedLines , ref } = useIntersectionHighlight (
75+ async ( ) => {
7676 try {
7777 const highlighter = await getShikiHighlighter ( ) ;
7878 const shikiLang = mapToShikiLang ( language ) ;
@@ -89,30 +89,24 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ code, language }) => {
8989 theme : SHIKI_THEME ,
9090 } ) ;
9191
92- if ( ! cancelled ) {
93- const lines = extractShikiLines ( html ) ;
94- // Remove trailing empty line if present
95- const filteredLines = lines . filter (
96- ( line , idx , arr ) => idx < arr . length - 1 || line . trim ( ) !== ""
97- ) ;
98- setHighlightedLines ( filteredLines . length > 0 ? filteredLines : null ) ;
99- }
92+ const lines = extractShikiLines ( html ) ;
93+ // Remove trailing empty line if present
94+ const filteredLines = lines . filter (
95+ ( line , idx , arr ) => idx < arr . length - 1 || line . trim ( ) !== ""
96+ ) ;
97+ return filteredLines . length > 0 ? filteredLines : null ;
10098 } catch ( error ) {
10199 console . warn ( `Failed to highlight code block (${ language } ):` , error ) ;
102- if ( ! cancelled ) setHighlightedLines ( null ) ;
100+ return null ;
103101 }
104- }
105-
106- void highlight ( ) ;
107- return ( ) => {
108- cancelled = true ;
109- } ;
110- } , [ code , language ] ) ;
102+ } ,
103+ [ code , language ]
104+ ) ;
111105
112106 const lines = highlightedLines ?? plainLines ;
113107
114108 return (
115- < div className = "code-block-wrapper" >
109+ < div ref = { ref } className = "code-block-wrapper" >
116110 < div className = "code-block-container" >
117111 { lines . map ( ( content , idx ) => (
118112 < React . Fragment key = { idx } >
0 commit comments