Warning
This is an internal project, and is not intended for public use. No support or stability guarantees are provided.
Purpose: Reduce page weight and improve hydration performance when sending complex data structures from the server to the client.
Core Strategy: Send plain text in the initial HTML for fast rendering and SEO, then deliver enhanced data (like syntax-highlighted code) as compressed, deferred props that are parsed only when needed. Keep crawler-relevant links in server-rendered HTML.
When crossing server to client boundaries, sometimes we need large and complex data structures to be sent from the server to the client. When a page's props become too large, it can compound to create a page that exceeds a practical crawl budget (commonly treated as around 2MB of HTML by many teams).
To optimize for crawlers, the initial HTML should contain semantically relevant information. In this project, code snippets are a useful example. At a basic level, you only need plain text to understand code. For human readers, syntax coloring can improve readability.
Large code snippets can balloon the overall size of the page, simply based on
the number of <span> elements used to add color to the code. When the client
needs access to that same semantic information, it has to be repeated at the
end of the HTML, serialized.
This complex structure also slows hydration, because the data must be parsed and processed before the page can become interactive.
When streaming back multiple large code blocks on a single page, uncompressed
props also introduce head-of-line blocking. Every code block above the viewport
must be fully parsed and hydrated before the last one can render. This is
especially noticeable when a user navigates directly to a #slug targeting the
last code block on the page - they have to wait for all preceding blocks to
process first.
We send the plain text variant of the code snippet in the initial HTML, safely escaped by React:
<pre><code class="language-javascript">
import { AlertDialog } from '@base-ui/react/alert-dialog';
import styles from './index.module.css';
export default function ExampleAlertDialog() {
return (
<AlertDialog.Root>
<AlertDialog.Trigger data-color="red" className={styles.Button}>
Discard draft
</AlertDialog.Trigger>
<AlertDialog.Portal>
<AlertDialog.Backdrop className={styles.Backdrop} />
<AlertDialog.Popup className={styles.Popup}>
<AlertDialog.Title className={styles.Title}>Discard draft?</AlertDialog.Title>
<AlertDialog.Description className={styles.Description}>
You can't undo this action.
</AlertDialog.Description>
<div className={styles.Actions}>
<AlertDialog.Close className={styles.Button}>Cancel</AlertDialog.Close>
<AlertDialog.Close data-color="red" className={styles.Button}>
Discard
</AlertDialog.Close>
</div>
</AlertDialog.Popup>
</AlertDialog.Portal>
</AlertDialog.Root>
);
}
</code></pre>
This HTML string is 1.33 KB.
Because this code lives inside a client component, we send the props in a serialized format, such as a stringified JSON object:
{
"language": "javascript",
"code": "import { AlertDialog } from '@base-ui/react/alert-dialog';\nimport styles from './index.module.css';\n\nexport default function ExampleAlertDialog() {\n return (\n <AlertDialog.Root>\n <AlertDialog.Trigger data-color=\"red\" className={styles.Button}>\n Discard draft\n </AlertDialog.Trigger>\n <AlertDialog.Portal>\n <AlertDialog.Backdrop className={styles.Backdrop} />\n <AlertDialog.Popup className={styles.Popup}>\n <AlertDialog.Title className={styles.Title}>Discard draft?</AlertDialog.Title>\n <AlertDialog.Description className={styles.Description}>\n You can't undo this action.\n </AlertDialog.Description>\n <div className={styles.Actions}>\n <AlertDialog.Close className={styles.Button}>Cancel</AlertDialog.Close>\n <AlertDialog.Close data-color=\"red\" className={styles.Button}>\n Discard\n </AlertDialog.Close>\n </div>\n </AlertDialog.Popup>\n </AlertDialog.Portal>\n </AlertDialog.Root>\n );\n}\n"
}
This JSON string is 1.08 KB.
The browser parses and paints this HTML very quickly. When JS downloads, the page hydrates immediately. This requires two copies of the plain text code in the HTML, doubling the cost for uncompressed HTML, but transfer-level compression substantially reduces the duplicate cost in practice.
After streaming plain text, we can still deliver full syntax highlighting. The highlighted code is represented as a HAST tree, a JSON-based AST where each span of color becomes a nested object:
{
"highlightedCode": {
"type": "root",
"data": { "totalLines": 27 },
"children": [{
"type": "element", "tagName": "span",
"properties": { "className": "frame", "dataLined": "" },
"children": [{
"type": "element", "tagName": "span",
"properties": { "className": "line", "dataLn": 1 },
"children": [{
"type": "element", "tagName": "span",
"properties": { "className": ["pl-k"] },
"children": [{
"type": "text",
"value": "import"
}]
},
{
"type": "text",
"value": " { "
},
{
"type": "element", "tagName": "span",
"properties": { "className": ["pl-smi"] },
"children": [{
"type": "text",
"value": "AlertDialog"
}]
},
[...]
]
}]
}]
}
}
This HAST serializes to 16.76 KB of JSON.
The HAST can then be rendered after hydration to enhance the plain text with syntax coloring:
<pre><code class="language-tsx" data-total-lines="27"><span class="frame" data-lined=""><span class="line" data-ln="1"><span class="pl-k">import</span> { <span class="pl-smi">AlertDialog</span> } <span class="pl-k">from</span> <span class="pl-s"><span class="pl-pds">'</span>@base-ui/react/alert-dialog<span class="pl-pds">'</span></span>;</span>
<span class="line" data-ln="2"><span class="pl-k">import</span> <span class="pl-smi">styles</span> <span class="pl-k">from</span> <span class="pl-s"><span class="pl-pds">'</span>./index.module.css<span class="pl-pds">'</span></span>;</span>
<span class="line" data-ln="3">
</span><span class="line" data-ln="4"><span class="pl-k">export</span> <span class="pl-k">default</span> <span class="pl-k">function</span> <span class="pl-en">ExampleAlertDialog</span>() {</span>
<span class="line" data-ln="5"> <span class="pl-k">return</span> (</span>
<span class="line" data-ln="6"> <<span class="pl-c1">AlertDialog.Root</span>></span>
<span class="line" data-ln="7"> <<span class="pl-c1">AlertDialog.Trigger</span> <span class="pl-e">data-color</span><span class="pl-k">=</span><span class="pl-s"><span class="pl-pds">"</span>red<span class="pl-pds">"</span></span> <span class="pl-e">className</span><span class="pl-k">=</span><span class="pl-pse">{</span><span class="pl-smi">styles</span>.<span class="pl-smi">Button</span><span class="pl-pse">}</span>></span>
<span class="line" data-ln="8"> Discard draft</span>
<span class="line" data-ln="9"> </<span class="pl-c1">AlertDialog.Trigger</span>></span>
<span class="line" data-ln="10"> <<span class="pl-c1">AlertDialog.Portal</span>></span>
<span class="line" data-ln="11"> <<span class="pl-c1">AlertDialog.Backdrop</span> <span class="pl-e">className</span><span class="pl-k">=</span><span class="pl-pse">{</span><span class="pl-smi">styles</span>.<span class="pl-smi">Backdrop</span><span class="pl-pse">}</span> /></span>
<span class="line" data-ln="12"> <<span class="pl-c1">AlertDialog.Popup</span> <span class="pl-e">className</span><span class="pl-k">=</span><span class="pl-pse">{</span><span class="pl-smi">styles</span>.<span class="pl-smi">Popup</span><span class="pl-pse">}</span>></span>
<span class="line" data-ln="13"> <<span class="pl-c1">AlertDialog.Title</span> <span class="pl-e">className</span><span class="pl-k">=</span><span class="pl-pse">{</span><span class="pl-smi">styles</span>.<span class="pl-smi">Title</span><span class="pl-pse">}</span>>Discard draft?</<span class="pl-c1">AlertDialog.Title</span>></span>
<span class="line" data-ln="14"> <<span class="pl-c1">AlertDialog.Description</span> <span class="pl-e">className</span><span class="pl-k">=</span><span class="pl-pse">{</span><span class="pl-smi">styles</span>.<span class="pl-smi">Description</span><span class="pl-pse">}</span>></span>
<span class="line" data-ln="15"> You can't undo this action.</span>
<span class="line" data-ln="16"> </<span class="pl-c1">AlertDialog.Description</span>></span>
<span class="line" data-ln="17"> <<span class="pl-ent">div</span> <span class="pl-e">className</span><span class="pl-k">=</span><span class="pl-pse">{</span><span class="pl-smi">styles</span>.<span class="pl-smi">Actions</span><span class="pl-pse">}</span>></span>
<span class="line" data-ln="18"> <<span class="pl-c1">AlertDialog.Close</span> <span class="pl-e">className</span><span class="pl-k">=</span><span class="pl-pse">{</span><span class="pl-smi">styles</span>.<span class="pl-smi">Button</span><span class="pl-pse">}</span>>Cancel</<span class="pl-c1">AlertDialog.Close</span>></span>
<span class="line" data-ln="19"> <<span class="pl-c1">AlertDialog.Close</span> <span class="pl-e">data-color</span><span class="pl-k">=</span><span class="pl-s"><span class="pl-pds">"</span>red<span class="pl-pds">"</span></span> <span class="pl-e">className</span><span class="pl-k">=</span><span class="pl-pse">{</span><span class="pl-smi">styles</span>.<span class="pl-smi">Button</span><span class="pl-pse">}</span>></span>
<span class="line" data-ln="20"> Discard</span>
<span class="line" data-ln="21"> </<span class="pl-c1">AlertDialog.Close</span>></span>
<span class="line" data-ln="22"> </<span class="pl-ent">div</span>></span>
<span class="line" data-ln="23"> </<span class="pl-c1">AlertDialog.Popup</span>></span>
<span class="line" data-ln="24"> </<span class="pl-c1">AlertDialog.Portal</span>></span>
<span class="line" data-ln="25"> </<span class="pl-c1">AlertDialog.Root</span>></span>
<span class="line" data-ln="26"> );</span>
<span class="line" data-ln="27">}</span>
</span></code></pre>
This HTML string is 4.79 KB.
For the AlertDialog hero demo, this HAST tree is 16.76 KB: 15.5× larger
than the 1.08 KB plain text. A highlighted HTML string (the <span>-wrapped
markup shown above) would be 4.79 KB, but rendering it safely often requires
shipping extra parsing logic. For example, if we were to parse this highlighted
HTML using rehype-parse on the client so that we can enhance the code block with interactivity,
we would increase the bundle size by 60 KB.
By contrast, the compression library we use only adds 4 KB to the bundle for async decompression.
We compress the HAST with DEFLATE and defer parsing until the snippet enters
the viewport via an IntersectionObserver. The compressed payload is 0.97 KB
(27% smaller than the escaped plain text HTML from Step 1, 80% smaller than
the highlighted <span> markup).
For better compression, we can optionally use a shared (preset) dictionary on
both server and client. This primes DEFLATE with repeated HAST patterns and
common text so the deferred payload is smaller. In this example, the deferred
payload drops from 0.97 KB to 0.82 KB (38% smaller than the escaped plain text
HTML and 83% smaller than the highlighted <span> markup), as shown by the "Compressed with Dict" results
below. See Content-Aware Dictionary for details.
This approach relies on the browser's built-in JSON.parse instead of shipping
a separate HTML parser. This is simpler to reason about, has no parser bundle
cost, and offers predictable behavior across clients.
For above-the-fold code where deferred highlighting would cause a visible
flash, we can skip deferral and decompress at hydration time instead. This
front-loads highlighting work into hydration but still benefits from the
compressed payload. The hydration cost is still smaller than passing
uncompressed HAST or HTML strings in props. This is how highlightAt: 'init'
works for the CodeHighlighter component.
When testing the code for console.log('Hello, world!'), we see the following sizes for different approaches:
| Scenario | Initial HTML | Initial Props | Suspended Props | Total Weight | % |
|---|---|---|---|---|---|
| Plain text | 97 bytes | 0 bytes | 0 bytes | = 97 bytes | 100% |
| Plain Text Hydrated | 97 bytes | 64 bytes | 0 bytes | = 161 bytes | 166% |
| HTML String in Props | 193 bytes | 193 bytes | 0 bytes | = 386 bytes | 398% |
| Compressed Props | 97 bytes | 64 bytes | 320 bytes | = 481 bytes | 496% |
| Compressed with Dict | 97 bytes | 64 bytes | 168 bytes | = 329 bytes | 339% |
| Highlight on Init | 193 bytes | 168 bytes | 0 bytes | = 361 bytes | 372% |
Using HTML String in Props requires dangerouslySetInnerHTML to render, which
can have security implications when content is untrusted. To add interactivity,
we would either need to parse the HTML string or directly use browser APIs
outside of React.
For a more realistic example, consider the AlertDialog hero demo (27 lines
of JSX). In the initial HTML, React escapes the plain text to 1.33 KB. The
serialized props for hydration are 1.08 KB.
The highlighted HAST for this snippet serializes to 16.76 KB of JSON, 15.5×
larger than the plain text. An equivalent highlighted HTML string (the
<span>-wrapped markup) would be 4.79 KB. By compressing the HAST with
DEFLATE, the deferred payload drops to 0.97 KB (27% smaller than the escaped
plain text HTML from Step 1). With a content-aware dictionary, it drops
further to 0.82 KB.
| Scenario | Initial HTML | Initial Props | Suspended Props | Total Weight | % |
|---|---|---|---|---|---|
| Plain text | 1.33 KB | 0 bytes | 0 bytes | = 1.33 KB | 100% |
| Plain Text Hydrated | 1.33 KB | 1.08 KB | 0 bytes | = 2.41 KB | 181% |
| HTML String in Props | 4.79 KB | 4.79 KB | 0 bytes | = 9.58 KB | 720% |
| Compressed Props | 1.33 KB | 1.08 KB | 0.97 KB | = 3.38 KB | 254% |
| Compressed with Dict | 1.33 KB | 1.08 KB | 0.82 KB | = 3.23 KB | 243% |
| Highlight on Init | 4.79 KB | 0.82 KB | 0 bytes | = 5.61 KB | 422% |
For the medium snippet, decompression takes ~0.5 ms and HAST-to-JSX rendering takes ~0.84 ms, well under a single frame budget.
With a very large code snippet, the savings become significant:
| Scenario | Initial HTML | Initial Props | Suspended Props | Total Weight | % |
|---|---|---|---|---|---|
| Plain Text | 49 KB | 0 bytes | 0 bytes | = 49 KB | 100% |
| Plain Text Hydrated | 49 KB | 47 KB | 0 bytes | = 96 KB | 196% |
| HTML String in Props | 203 KB | 203 KB | 0 bytes | = 406 KB | 829% |
| Compressed Props | 49 KB | 47 KB | 54 KB | = 151 KB | 308% |
| Compressed with Dict | 49 KB | 47 KB | 35 KB | = 131 KB | 267% |
| Highlight on Init | 203 KB | 35 KB | 0 bytes | = 238 KB | 486% |
Both "HTML String in Props" and "Compressed Props" deliver syntax-highlighted code on the client, but with different timing. The HTML string highlights immediately on hydration, while compressed props defer highlighting until the snippet enters the viewport. In exchange, compressed props achieve a total weight of 151 KB versus 406 KB, a 63% reduction. The highlighted payload itself is 54 KB instead of 203 KB because DEFLATE exploits the repetitive structure of HAST JSON. Meanwhile, compressed props keep the initial HTML at 49 KB of plain text, so first paint is fast. HTML String in Props sends 203 KB of highlighted markup upfront, which increases HTML parse time and style calculation.
Decompression for this snippet takes around 10 ms (compared to ~0.5 ms
for the medium snippet). With highlightAt: 'idle', each snippet decompresses
and renders as one task when it enters the viewport. Many small snippets on a
page are no problem, but a single giant snippet could blow past the 50 ms
long-task budget.
If we want to highlight on init instead of deferring, we can still use compressed props to reduce the hydration payload.
This delivers a 41% reduction in total weight (238 KB vs 406 KB) and a smaller initial hydration cost (35 KB vs 203 KB),
at the expense of front-loading all highlighting work into hydration instead of spreading it out during idle time.
Ideally, this is only done for code that you know will be above the fold on first render.
This is how highlightAt: 'init' works for the CodeHighlighter component.
The size tables above show total weight, but the order in which bytes arrive matters just as much. Using the very large snippet as an example, here is how each strategy streams data to the browser:
Compressed with Dict (lazy highlighting)
| Step | Bytes Sent | What Happens in the Browser |
|---|---|---|
| 1. Initial HTML | 49 KB | Plain text renders, fast HTML parse |
| 2. RSC payload | 47 KB | Hydration (minimal props, highlighting deferred) |
| 3. Deferred payload | 35 KB | Decompress + enhance on idle/intersection |
Total: 131 KB. The server can stream the plain text HTML before highlighting even finishes. Syntax highlighting is CPU-bound work that takes time, especially for large snippets. By not blocking the response on highlighting, the browser receives and paints the plain text sooner, improving FCP. Hydration is fast because the props contain only plain text + a compressed blob. Highlighting arrives later and is applied incrementally during idle time without blocking the main thread.
Highlight on Init
| Step | Bytes Sent | What Happens in the Browser |
|---|---|---|
| 1. Initial HTML | 203 KB | Highlighted HTML renders, slower HTML parse + style calc |
| 2. RSC payload | 35 KB | Hydration (decompress + render highlighted HAST) |
Total: 238 KB. The user sees highlighted code on first paint, but the initial HTML is over 4× larger, which increases HTML parse time and style calculation. The hydration step must decompress and render the HAST synchronously before the component becomes interactive.
When to use each
Lazy highlighting is the better default. It keeps initial HTML small, hydration fast, and total weight low. Highlight on Init is useful for code that is guaranteed to be above the fold on first render, so the user sees color immediately without a plain-text-to-highlighted flash.
A hybrid approach combines both: use highlightAt: 'init' only for the
visible lines of a snippet (the first frame), and plain text for the rest.
This way, only a small portion of the code carries the verbose highlighted
spans in the initial HTML, while lines that are scrolled out of view remain
plain text until the deferred payload enhances them. This limits the cost of
Highlight on Init to the visible surface area rather than the entire snippet.
When this pattern was applied to the Base UI docs, profiling showed significant improvements:
| Metric | Before | After | Change |
|---|---|---|---|
| FCP (Menubar page) | 0.27s | 0.21s | -22% |
| HTML parse time | 18.4ms | 7.8ms | -58% |
| HTML + CSS render time | 43ms | 27ms | -37% |
| LCP (Menu page, 6 demos) | 0.43s | 0.33s | -23% |
| Total Blocking Time | N/A | 0s | No long tasks |
By sending only plain text in the initial HTML, the browser parses fewer DOM
nodes and skips complex <span> styling entirely. Total Blocking Time remains
at zero because deferred highlighted payloads let pages enhance code blocks
incrementally during idle time instead of front-loading all highlighting work
into initial hydration.
Before this pattern, type pages passed fully highlighted React nodes across the server-client boundary for every prop's type signature. Each highlighted fragment was serialized as a nested JSX tree in the page payload, duplicating work the browser had already rendered in the initial HTML.
Applying prop compression to type documentation shows a different profile than demos. Type pages contain many smaller highlighted fragments spread across dozens of props, so even modest per-prop savings compound quickly.
Decreases the page's uncompressed HTML size by 30%, bringing it below the practical 2 MB crawl-budget limit for search engine crawlers. The tradeoff is a 5% increase in compressed (transfer-encoded) HTML because the pre-compressed props compress poorly under a second pass of DEFLATE.
| Uncompressed HTML | Compressed HTML | |
|---|---|---|
| Before | 2675 KB | 276 KB |
| After | 1886 KB | 290 KB |
| Change | -789 KB (30%) | +14 KB (5%) |
Beyond page weight, the reduction in serialized props also improves first contentful paint and hydration time by 42 ms. Fewer DOM nodes means faster HTML parsing, less scripting work to deserialize the RSC payload, and quicker style calculation.
| Parse Time | Scripting Time | Calculating Styles | Total | |
|---|---|---|---|---|
| Before | 56ms | 49ms | 27ms | = 132ms |
| After | 37ms | 34ms | 19ms | = 90ms |
| Change | -19ms (34%) | -15ms (31%) | -8ms (30%) | = -42ms (29%) |
For pages already well within the crawl budget, the pattern still provides a meaningful reduction in uncompressed HTML.
| Uncompressed HTML | Compressed HTML | |
|---|---|---|
| Change | -283 KB (23%) | +6 KB (5%) |
For very small snippets (for example, single-line inline code), the complexity of compression and decompression often outweighs the benefits. Reserve this pattern for larger blocks where deferred parsing and reduced prop payloads materially improve page weight and hydration behavior.
Also avoid deferring data that contains links that are semantically important for crawlers. For example, if TypeDoc output includes links to external type definitions that should be discoverable in the initial HTML, keep those links in the server-rendered markup.
DEFLATE supports a preset dictionary: a buffer of bytes the compressor assumes was already sent to the decompressor. Backreferences into the dictionary compress repetitive content more efficiently than starting cold.
The static HAST_DICTIONARY (~3 KB) contains JSON structural patterns and
class names that appear in every highlighted HAST tree. It is always included
at the end of the 32 KiB dictionary window so it stays in scope regardless
of how much text content precedes it.
When textContent is provided, the actual text of the HAST (extracted via
toText(hast, { whitespace: 'pre' })) is prepended to the dictionary. This
works because HAST JSON literally contains its text content as "value" fields
inside text nodes, so the dictionary seeds backreferences for those repetitions.
Dictionary layout (≤ 32 KiB total):
[truncated_text_content][HAST_DICTIONARY]
textContent is omitted, only the static dictionary is used (opt-out).When textContent is supplied at compression time, a 4-byte FNV-1a checksum
of the final dictionary is embedded at the start of the compressed payload:
base64([4-byte checksum][deflate bytes])
On decompression, the checksum is recomputed from buildDictionary(textContent)
and compared. If they don't match, a HastDictionaryMismatchError is thrown,
preventing silently rendered corrupted markup when the wrong text is passed.
When textContent is omitted, no checksum is embedded and none is verified.
The text dictionary is derived from the links-only fallback rather than passed
as a separate prop. On the server, hastToJsxDeferred builds a stripped
fallback HAST (highlighting spans removed) and converts it to a compact
FallbackNode[] tuple format before passing it to the client component as
the fallback prop. The text content is extracted from this compact format
and used as the DEFLATE dictionary for compression.
On the client, the same text extraction reconstructs the identical dictionary for decompression, so no duplicate text string crosses the boundary.
The fallback HAST is a simple tree (only links and text after stripping
highlighting spans), but raw HAST JSON is verbose. Every node carries
repeated type, tagName, properties, and children keys. The compact
FallbackNode[] format replaces this with variable-length tuples:
// Raw HAST element:
{ type: 'element', tagName: 'a', properties: { className: 'ref', href: '/api' },
children: [{ type: 'text', value: 'Ref' }] }
// Compact FallbackNode:
['a', 'ref', { href: '/api' }, 'Ref']
This is a good fit for simple trees where the structure is shallow and
predictable. For complex HAST (deep nesting, many element types, syntax
highlighting spans), DEFLATE compression produces better results because it
can exploit repetitive JSON patterns across the entire tree. HAST also
deserializes directly into a usable format via JSON.parse. A custom
intermediate format would need to be converted into HAST anyway (for the
enhancers API), creating extra memory pressure from throwaway objects that
must be garbage collected. That tradeoff is acceptable for the compact
fallback, which is small and shallow, but not for the full highlighted tree.
After the component enters the viewport, we decompress and parse the JSON
object to load a HAST object into memory, then render it as JSX.
Derived render output can be cached with a WeakMap keyed by HAST child
arrays, so the lightweight compressed payload stays small until rendering is
needed, while expanded render data can still be released when those objects are
no longer referenced. By contrast, storing the expanded HAST or rendered output
directly in module scope increases baseline memory pressure because that larger
structure stays resident for the lifetime of the module.
For compression and decompression, this project uses fflate.
We use the library
hast-util-to-jsx-runtime
to turn this HAST object into regular React components.
The HAST object is also very easy to manipulate using the rehype ecosystem,
allowing customization based on user preferences or dynamic data. If HAST
originates from an untrusted source, sanitize it before rendering.
Compressed props need to be produced somewhere. The
Built Factories pattern solves this: a
build-time loader processes each factory call (createDemo(import.meta.url, ...)) and injects a precompute object containing the DEFLATE-compressed HAST.
The
loadPrecomputedCodeHighlighter
loader's hastCompressed output mode is a concrete implementation of this.
At runtime, the factory receives precomputed data through its options, so no heavy highlighting or parsing libraries are shipped to the client. When precomputation isn't available (e.g. dynamic routes), the same component falls back to server- or client-time processing.
The Props Context Layering pattern keeps this compression logic out of consumer code. Props carry the precomputed compressed data from the server, and context provides client-side functions (like decompression) when the component hydrates. The loading structure looks like this:
<Code>
<Suspense fallback={<PlainCode />}>
<HighlightedCode />
</Suspense>
</Code>
A content handler only needs to consume the result:
'use client';
function ContentHandler(props) {
const { CodeBlock } = useCode(props);
return (
<div>
<CodeBlock />
</div>
);
}
This is roughly how the CodeHighlighter works.
This pattern is used throughout the docs-infra system:
CodeHighlighter: Renders plain text as the Suspense fallback, then progressively enhances with decompressed highlighted codeloadPrecomputedCodeHighlighter: Build-time loader that produces compressed HAST via the hastCompressed output formatuseCode: Hook that manages decompression and caching of compressed props on the client