From cfc872eff97d7ff0a172ded386661bfbab0e1df7 Mon Sep 17 00:00:00 2001 From: Dev Kraken Date: Thu, 9 Jan 2025 19:25:19 +0400 Subject: [PATCH] fix(shiki): resolve disposal issue with highlighter instances This commit addresses issue #103 by implementing a singleton pattern for the Shiki highlighter instance. It ensures that instances are reused and properly disposed of, preventing memory leaks and performance degradation during hot reloads. --- components/website/code-renderer.tsx | 34 ++++++++++++++++------ lib/shiki.ts | 42 +++++++++++++++++++++++----- 2 files changed, 61 insertions(+), 15 deletions(-) diff --git a/components/website/code-renderer.tsx b/components/website/code-renderer.tsx index d45c7038..07cf94a6 100644 --- a/components/website/code-renderer.tsx +++ b/components/website/code-renderer.tsx @@ -1,19 +1,37 @@ -import { codeToHtml } from '@/lib/shiki'; +'use client'; +import { useEffect, useState } from 'react'; +import { codeToHtml, disposeHighlighter } from '@/lib/shiki'; -type CodeRenderer = { +type CodeRendererProps = { code: string; lang: string; }; -export default async function CodeRenderer({ code, lang }: CodeRenderer) { - const html = await codeToHtml({ - code, - lang, - }); +export default function CodeRenderer({ code, lang }: CodeRendererProps) { + const [html, setHtml] = useState(null); + + useEffect(() => { + const fetchHtml = async () => { + try { + const generatedHtml = await codeToHtml({ code, lang }); + setHtml(generatedHtml); + } catch (error) { + console.error('Error generating HTML:', error); + } + }; + + fetchHtml(); + + return () => { + // Avoid disposing if the highlighter might still be needed + // Consider removing or commenting out this line if it's causing issues + // disposeHighlighter(); + }; + }, [code, lang]); return (
-
+
); } diff --git a/lib/shiki.ts b/lib/shiki.ts index 98075a8a..a87fb3ed 100644 --- a/lib/shiki.ts +++ b/lib/shiki.ts @@ -1,20 +1,48 @@ -import { bundledLanguages, createHighlighter } from 'shiki/bundle/web'; +import { + bundledLanguages, + createHighlighter, + Highlighter, +} from 'shiki/bundle/web'; import { noir } from './custom-theme'; +// This variable will hold the cached highlighter instance +let highlighter: Highlighter | null = null; + +const getHighlighter = async (): Promise => { + if (!highlighter) { + // Create it only once + highlighter = await createHighlighter({ + themes: [noir], + langs: [...Object.keys(bundledLanguages)], + }); + } + return highlighter; +}; + export const codeToHtml = async ({ code, lang, }: { code: string; lang: string; -}) => { - const highlighter = await createHighlighter({ - themes: [noir], - langs: [...Object.keys(bundledLanguages)], - }); +}): Promise => { + const highlighterInstance = await getHighlighter(); - return highlighter.codeToHtml(code, { + // Ensure highlighterInstance is not null + if (!highlighterInstance) { + throw new Error('Highlighter instance is null'); + } + + return highlighterInstance.codeToHtml(code, { lang: lang, theme: 'noir', }); }; + +// Function to dispose of the highlighter when done (e.g., server-side cleanup) +export const disposeHighlighter = async (): Promise => { + if (highlighter) { + highlighter.dispose(); + highlighter = null; // Reset the cached instance + } +};