Skip to content

Commit 275dae7

Browse files
committed
Core: improve Shiki performance
1 parent 9ba1250 commit 275dae7

File tree

4 files changed

+80
-42
lines changed

4 files changed

+80
-42
lines changed

packages/core/src/highlight/config.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,11 @@ export function defineShikiConfig(config: ShikiConfig): ResolvedShikiConfig {
1515
createHighlighter() {
1616
if (created) return created;
1717

18-
const res = config.createHighlighter();
19-
if ('then' in res) {
20-
return res.then((v) => {
21-
created = v;
22-
return v;
23-
});
24-
} else {
25-
return (created = res);
18+
created = config.createHighlighter();
19+
if ('then' in created) {
20+
created = created.then((v) => (created = v));
2621
}
22+
return created;
2723
},
2824
};
2925
}

packages/core/src/highlight/core/client.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@ import type { MakeOptional } from '@/types';
77

88
const ShikiConfigContext = createContext<ResolvedShikiConfig | null>(null);
99

10-
export function useShikiConfig(defaultConfig?: ResolvedShikiConfig) {
11-
if (defaultConfig) return defaultConfig;
10+
export function useShikiConfigOptional() {
11+
return use(ShikiConfigContext);
12+
}
13+
14+
export function useShikiConfig(forced?: ResolvedShikiConfig) {
15+
if (forced) return forced;
1216
const ctx = use(ShikiConfigContext);
1317
if (!ctx) throw new Error(`missing <ShikiConfigProvider />`);
1418
return ctx;
@@ -26,16 +30,14 @@ export function ShikiConfigProvider({
2630

2731
const promises: Record<string, Promise<ReactNode>> = {};
2832

33+
export type UseShikiOptions = MakeOptional<CoreHighlightOptions, 'config'>;
34+
2935
/**
3036
* get highlighted results, should be used with React Suspense API.
3137
*
3238
* note: results are cached with (lang, code) as keys, if this is not the desired behaviour, pass a `deps` instead.
3339
*/
34-
export function useShiki(
35-
code: string,
36-
options: MakeOptional<CoreHighlightOptions, 'config'>,
37-
deps?: DependencyList,
38-
): ReactNode {
40+
export function useShiki(code: string, options: UseShikiOptions, deps?: DependencyList): ReactNode {
3941
const config = useShikiConfig(options.config);
4042
const key = useMemo(() => {
4143
return deps ? JSON.stringify(deps) : `${options.lang}:${code}`;

packages/core/src/highlight/core/index.ts

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import type {
2-
BundledHighlighterOptions,
32
BundledLanguage,
43
CodeOptionsMeta,
54
CodeOptionsThemes,
65
CodeToHastOptionsCommon,
76
BundledTheme,
7+
HighlighterCore,
8+
ThemeRegistrationAny,
9+
LanguageRegistration,
810
} from 'shiki';
911
import {
1012
type Components,
@@ -30,7 +32,7 @@ export async function highlightHast(
3032
): Promise<Root> {
3133
const { lang: initialLang, fallbackLanguage, config, ...rest } = options;
3234
let lang = initialLang;
33-
let themesToLoad: unknown[];
35+
let themesToLoad: (ThemeRegistrationAny | string)[];
3436

3537
if (!('theme' in rest) && !('themes' in rest)) {
3638
Object.assign(rest, config.defaultThemes);
@@ -45,14 +47,13 @@ export async function highlightHast(
4547
}
4648

4749
const highlighter = await config.createHighlighter();
48-
await highlighter.loadTheme(...(themesToLoad as never[]));
49-
50-
try {
51-
await highlighter.loadLanguage(lang as never);
52-
} catch {
53-
lang = fallbackLanguage ?? 'text';
54-
await highlighter.loadLanguage(lang as never);
55-
}
50+
await Promise.all([
51+
loadMissingTheme(highlighter, ...themesToLoad),
52+
loadMissingLanguage(highlighter, lang).catch(() => {
53+
lang = fallbackLanguage ?? 'text';
54+
if (fallbackLanguage) return loadMissingLanguage(highlighter, fallbackLanguage);
55+
}),
56+
]);
5657

5758
return highlighter.codeToHast(code, {
5859
lang,
@@ -61,6 +62,44 @@ export async function highlightHast(
6162
});
6263
}
6364

65+
async function loadMissingTheme(
66+
highlighter: HighlighterCore,
67+
...themes: (ThemeRegistrationAny | string)[]
68+
) {
69+
const { isSpecialTheme } = await import('shiki/core');
70+
71+
const missingThemes = themes.filter((theme) => {
72+
if (isSpecialTheme(theme)) return false;
73+
try {
74+
highlighter.getTheme(theme);
75+
return false;
76+
} catch {
77+
return true;
78+
}
79+
});
80+
81+
if (missingThemes.length > 0) await highlighter.loadTheme(...(missingThemes as never[]));
82+
}
83+
84+
async function loadMissingLanguage(
85+
highlighter: HighlighterCore,
86+
...langs: (LanguageRegistration | string)[]
87+
) {
88+
const { isSpecialLang } = await import('shiki/core');
89+
90+
const missingLangs = langs.filter((lang) => {
91+
if (isSpecialLang(lang)) return false;
92+
try {
93+
highlighter.getLanguage(lang);
94+
return false;
95+
} catch {
96+
return true;
97+
}
98+
});
99+
100+
if (missingLangs.length > 0) await highlighter.loadLanguage(...(missingLangs as never[]));
101+
}
102+
64103
export function hastToJsx(hast: Root, options?: Partial<ToJsxOptions>) {
65104
return toJsxRuntime(hast, {
66105
jsx,
@@ -79,13 +118,16 @@ export function hastToJsx(hast: Root, options?: Partial<ToJsxOptions>) {
79118
*/
80119
export async function getHighlighter(
81120
config: ResolvedShikiConfig,
82-
options: Omit<BundledHighlighterOptions<BundledLanguage, BundledTheme>, 'engine' | 'langAlias'>,
121+
options?: {
122+
langs?: (BundledLanguage | LanguageRegistration)[];
123+
themes?: (BundledTheme | ThemeRegistrationAny)[];
124+
},
83125
) {
84126
const highlighter = await config.createHighlighter();
85127

86128
await Promise.all([
87-
highlighter.loadLanguage(...(options.langs as never[])),
88-
highlighter.loadTheme(...(options.themes as never[])),
129+
options?.langs && loadMissingLanguage(highlighter, ...options.langs),
130+
options?.themes && loadMissingTheme(highlighter, ...options.themes),
89131
]);
90132

91133
return highlighter;

packages/core/src/highlight/full/config.ts

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
import { createHighlighter, createOnigurumaEngine } from 'shiki';
21
import { defineShikiConfig } from '../config';
32

4-
export const withJSEngine = defineShikiConfig({
5-
defaultThemes: {
6-
themes: {
7-
light: 'github-light',
8-
dark: 'github-dark',
9-
},
3+
const defaultThemes = {
4+
themes: {
5+
light: 'github-light',
6+
dark: 'github-dark',
107
},
8+
};
9+
10+
export const withJSEngine = defineShikiConfig({
11+
defaultThemes,
1112
async createHighlighter() {
13+
const { createHighlighter } = await import('shiki');
1214
const { createJavaScriptRegexEngine } = await import('shiki/engine/javascript');
1315

1416
return createHighlighter({
@@ -20,13 +22,9 @@ export const withJSEngine = defineShikiConfig({
2022
});
2123

2224
export const withWASMEngine = defineShikiConfig({
23-
defaultThemes: {
24-
themes: {
25-
light: 'github-light',
26-
dark: 'github-dark',
27-
},
28-
},
29-
createHighlighter() {
25+
defaultThemes,
26+
async createHighlighter() {
27+
const { createHighlighter, createOnigurumaEngine } = await import('shiki');
3028
return createHighlighter({
3129
langs: [],
3230
themes: [],

0 commit comments

Comments
 (0)