Skip to content

Commit 79c6579

Browse files
committed
perf(react): highlight each type once per thread
Of the ~20,000 types the Node.js docs highlight, 741 are different: `{string}` alone is on most pages. Each thread now keeps the markup of the types it highlighted, by highlighter, so a type it has seen is only embedded again, saving ~4% of the build's time and ~5% of its CPU. Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 5a15f4b commit 79c6579

3 files changed

Lines changed: 107 additions & 30 deletions

File tree

‎packages/react/src/jsx-ast/index.mjs‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
'use strict';
22

33
import { getHighlighter } from '@doc-kit/core/plugins/shiki/highlighter.mjs';
4-
import { createTypeAnnotationHandler } from '@doc-kit/core/plugins/type-annotations/highlighter.mjs';
54

65
import { AST_NODE_TYPES } from './constants.mjs';
76
import { generate, processChunk } from './generate.mjs';
@@ -44,8 +43,8 @@ export default {
4443
// Types are highlighted, with their links embedded, as static
4544
// markup (see `./plugins/static-markup.mjs`)
4645
handlers: {
47-
typeAnnotation: embedHighlightedTypes(
48-
createTypeAnnotationHandler(() => getHighlighter('jsx-ast'))
46+
typeAnnotation: embedHighlightedTypes(() =>
47+
getHighlighter('jsx-ast')
4948
),
5049
},
5150
},

‎packages/react/src/jsx-ast/plugins/__tests__/static-markup.test.mjs‎

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import assert from 'node:assert/strict';
22
import { describe, it } from 'node:test';
33

44
import { createHighlighter } from '@doc-kit/core/plugins/shiki/highlighter.mjs';
5-
import { createTypeAnnotationHandler } from '@doc-kit/core/plugins/type-annotations/highlighter.mjs';
65
import { toString } from 'hast-util-to-string';
76
import rehypeRaw from 'rehype-raw';
87
import { unified } from 'unified';
@@ -13,9 +12,7 @@ import rehypeStaticMarkup, {
1312

1413
const highlighter = await createHighlighter();
1514

16-
const embedTypes = embedHighlightedTypes(
17-
createTypeAnnotationHandler(() => highlighter)
18-
);
15+
const embedTypes = embedHighlightedTypes(() => highlighter);
1916

2017
// A minimal mdast-util-to-hast state: the handlers only use patch/applyData
2118
const state = { patch: () => {}, applyData: (_, result) => result };
@@ -134,6 +131,53 @@ describe('embedHighlightedTypes', () => {
134131
assert.equal(textContent(html), 'Promise<string>');
135132
});
136133

134+
it('highlights the same type once for each highlighter', async () => {
135+
let calls = 0;
136+
137+
// The highlighter, counting what its Shiki instance highlights
138+
const counting = {
139+
resolveLanguage: highlighter.resolveLanguage,
140+
get shiki() {
141+
const { shiki } = highlighter;
142+
143+
return {
144+
...shiki,
145+
codeToHast: (...args) => {
146+
calls++;
147+
148+
return shiki.codeToHast(...args);
149+
},
150+
};
151+
},
152+
};
153+
154+
const themed = await createHighlighter({
155+
themes: { light: 'github-light', dark: 'github-dark' },
156+
});
157+
158+
let current = counting;
159+
160+
const embed = embedHighlightedTypes(() => current);
161+
162+
const promise = () =>
163+
makeNode('Promise<string>', {
164+
typescript: true,
165+
links: [{ start: 0, end: 7, text: 'Promise', href: 'mdn.io/promise' }],
166+
});
167+
168+
const first = embed(state, promise());
169+
const second = embed(state, promise());
170+
171+
assert.equal(calls, 1);
172+
// Each type is a node of its own, with the same markup
173+
assert.notEqual(second, first);
174+
assert.equal(innerHTML(second), innerHTML(first));
175+
176+
current = themed;
177+
178+
assert.notEqual(innerHTML(embed(state, promise())), innerHTML(first));
179+
});
180+
137181
it('leaves a type that was not highlighted as it is', () => {
138182
const element = embedTypes(state, makeNode('Whatever', { links: [] }));
139183

‎packages/react/src/jsx-ast/plugins/static-markup.mjs‎

Lines changed: 57 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use strict';
22

3+
import { createTypeAnnotationHandler } from '@doc-kit/core/plugins/type-annotations/highlighter.mjs';
34
import { toJsxRuntime } from 'hast-util-to-jsx-runtime';
45
import { renderToString } from 'preact-render-to-string';
56
import { Fragment, jsx, jsxs } from 'preact/jsx-runtime';
@@ -38,21 +39,33 @@ const render = children =>
3839
);
3940

4041
/**
41-
* Turns the `<code>` of highlighted code into a JSX `<code>` holding its
42-
* markup as it is.
42+
* The attributes and the markup of the `<code>` of highlighted code.
4343
*
4444
* @param {import('hast').Element} code - The `<code>`
45+
* @returns {{ attributes: Record<string, unknown>, html: string }}
46+
*/
47+
const toMarkup = ({
48+
properties: { class: className, ...properties },
49+
children,
50+
}) => ({
51+
attributes: {
52+
className: [className].flat().join(' ') || undefined,
53+
...properties,
54+
},
55+
html: render(children),
56+
});
57+
58+
/**
59+
* A JSX `<code>` holding highlighted markup as it is.
60+
*
61+
* @param {ReturnType<typeof toMarkup>} markup
4562
* @param {boolean} [inline] - Whether it's a type's, rather than a block's
4663
*/
47-
const embedCode = (
48-
{ properties: { class: className, ...properties }, children },
49-
inline = false
50-
) =>
64+
const createCodeElement = ({ attributes, html }, inline = false) =>
5165
createJSXElement('code', {
5266
inline,
53-
className: [className].flat().join(' ') || undefined,
54-
...properties,
55-
dangerouslySetInnerHTML: { __html: render(children) },
67+
...attributes,
68+
dangerouslySetInnerHTML: { __html: html },
5669
});
5770

5871
/**
@@ -68,7 +81,7 @@ export const embedHighlightedBlocks = tree => {
6881

6982
if (node.tagName === 'pre' && code?.tagName === 'code') {
7083
if (isHighlighted(node)) {
71-
node.children[0] = embedCode(code);
84+
node.children[0] = createCodeElement(toMarkup(code));
7285
}
7386

7487
return SKIP;
@@ -79,25 +92,46 @@ export const embedHighlightedBlocks = tree => {
7992
};
8093

8194
/**
82-
* Wraps a `typeAnnotation` handler of `remark-rehype`, embedding the types it
83-
* highlights.
95+
* Creates the `typeAnnotation` handler of `remark-rehype` highlighting types
96+
* and embedding them.
97+
*
98+
* Every highlighted type is kept, by highlighter: the same few hundred types
99+
* are highlighted thousands of times, `{string}` alone on most pages.
84100
*
85-
* @param {(state: import('mdast-util-to-hast').State, node: import('mdast').Node) => import('hast').Element} handler
86-
* @returns {typeof handler}
101+
* @param {() => import('@doc-kit/core/plugins/shiki/highlighter.mjs').SyntaxHighlighter} getHighlighter - Gives the highlighter, once a type is highlighted
102+
* @returns {(state: import('mdast-util-to-hast').State, node: import('mdast').Node) => import('hast').ElementContent}
87103
*/
88-
export const embedHighlightedTypes = handler => (state, node) => {
89-
const result = handler(state, node);
104+
export const embedHighlightedTypes = getHighlighter => {
105+
const highlight = createTypeAnnotationHandler(getHighlighter);
106+
const highlighted = new WeakMap();
90107

91-
// A type that didn't parse, or links nowhere, isn't highlighted
92-
if (!isHighlighted(result)) {
93-
return result;
94-
}
108+
return (state, node) => {
109+
const highlighter = getHighlighter();
110+
111+
if (!highlighted.has(highlighter)) {
112+
highlighted.set(highlighter, new Map());
113+
}
114+
115+
const types = highlighted.get(highlighter);
116+
const key = JSON.stringify([node.value, node.data]);
117+
118+
if (!types.has(key)) {
119+
const result = highlight(state, node);
120+
121+
// A type that didn't parse, or links nowhere, isn't highlighted
122+
if (!isHighlighted(result)) {
123+
return result;
124+
}
125+
126+
types.set(key, toMarkup(result));
127+
}
95128

96-
const code = embedCode(result, true);
129+
const code = createCodeElement(types.get(key), true);
97130

98-
state.patch(node, code);
131+
state.patch(node, code);
99132

100-
return code;
133+
return code;
134+
};
101135
};
102136

103137
/**

0 commit comments

Comments
 (0)