Skip to content

Commit cfc872e

Browse files
committed
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.
1 parent db545a6 commit cfc872e

File tree

2 files changed

+61
-15
lines changed

2 files changed

+61
-15
lines changed
Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,37 @@
1-
import { codeToHtml } from '@/lib/shiki';
1+
'use client';
2+
import { useEffect, useState } from 'react';
3+
import { codeToHtml, disposeHighlighter } from '@/lib/shiki';
24

3-
type CodeRenderer = {
5+
type CodeRendererProps = {
46
code: string;
57
lang: string;
68
};
79

8-
export default async function CodeRenderer({ code, lang }: CodeRenderer) {
9-
const html = await codeToHtml({
10-
code,
11-
lang,
12-
});
10+
export default function CodeRenderer({ code, lang }: CodeRendererProps) {
11+
const [html, setHtml] = useState<string | null>(null);
12+
13+
useEffect(() => {
14+
const fetchHtml = async () => {
15+
try {
16+
const generatedHtml = await codeToHtml({ code, lang });
17+
setHtml(generatedHtml);
18+
} catch (error) {
19+
console.error('Error generating HTML:', error);
20+
}
21+
};
22+
23+
fetchHtml();
24+
25+
return () => {
26+
// Avoid disposing if the highlighter might still be needed
27+
// Consider removing or commenting out this line if it's causing issues
28+
// disposeHighlighter();
29+
};
30+
}, [code, lang]);
1331

1432
return (
1533
<div>
16-
<div dangerouslySetInnerHTML={{ __html: html }} />
34+
<div dangerouslySetInnerHTML={{ __html: html || '' }} />
1735
</div>
1836
);
1937
}

lib/shiki.ts

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,48 @@
1-
import { bundledLanguages, createHighlighter } from 'shiki/bundle/web';
1+
import {
2+
bundledLanguages,
3+
createHighlighter,
4+
Highlighter,
5+
} from 'shiki/bundle/web';
26
import { noir } from './custom-theme';
37

8+
// This variable will hold the cached highlighter instance
9+
let highlighter: Highlighter | null = null;
10+
11+
const getHighlighter = async (): Promise<Highlighter> => {
12+
if (!highlighter) {
13+
// Create it only once
14+
highlighter = await createHighlighter({
15+
themes: [noir],
16+
langs: [...Object.keys(bundledLanguages)],
17+
});
18+
}
19+
return highlighter;
20+
};
21+
422
export const codeToHtml = async ({
523
code,
624
lang,
725
}: {
826
code: string;
927
lang: string;
10-
}) => {
11-
const highlighter = await createHighlighter({
12-
themes: [noir],
13-
langs: [...Object.keys(bundledLanguages)],
14-
});
28+
}): Promise<string> => {
29+
const highlighterInstance = await getHighlighter();
1530

16-
return highlighter.codeToHtml(code, {
31+
// Ensure highlighterInstance is not null
32+
if (!highlighterInstance) {
33+
throw new Error('Highlighter instance is null');
34+
}
35+
36+
return highlighterInstance.codeToHtml(code, {
1737
lang: lang,
1838
theme: 'noir',
1939
});
2040
};
41+
42+
// Function to dispose of the highlighter when done (e.g., server-side cleanup)
43+
export const disposeHighlighter = async (): Promise<void> => {
44+
if (highlighter) {
45+
highlighter.dispose();
46+
highlighter = null; // Reset the cached instance
47+
}
48+
};

0 commit comments

Comments
 (0)