MUI Docs Infra

Warning

This is an internal project, and is not intended for public use. No support or stability guarantees are provided.

Prop Compression

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.


The Problem

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.


How It Works

1. Send Plain Text in Initial HTML

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 &#x27;@base-ui/react/alert-dialog&#x27;;
  import styles from &#x27;./index.module.css&#x27;;

  export default function ExampleAlertDialog() {
    return (
      &lt;AlertDialog.Root&gt;
        &lt;AlertDialog.Trigger data-color=&quot;red&quot; className={styles.Button}&gt;
          Discard draft
        &lt;/AlertDialog.Trigger&gt;
        &lt;AlertDialog.Portal&gt;
          &lt;AlertDialog.Backdrop className={styles.Backdrop} /&gt;
          &lt;AlertDialog.Popup className={styles.Popup}&gt;
            &lt;AlertDialog.Title className={styles.Title}&gt;Discard draft?&lt;/AlertDialog.Title&gt;
            &lt;AlertDialog.Description className={styles.Description}&gt;
              You can&#x27;t undo this action.
            &lt;/AlertDialog.Description&gt;
            &lt;div className={styles.Actions}&gt;
              &lt;AlertDialog.Close className={styles.Button}&gt;Cancel&lt;/AlertDialog.Close&gt;
              &lt;AlertDialog.Close data-color=&quot;red&quot; className={styles.Button}&gt;
                Discard
              &lt;/AlertDialog.Close&gt;
            &lt;/div&gt;
          &lt;/AlertDialog.Popup&gt;
        &lt;/AlertDialog.Portal&gt;
      &lt;/AlertDialog.Root&gt;
    );
  }
</code></pre>

This HTML string is 1.33 KB.

2. Serialize Minimal Props for Hydration

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.

3. Defer Enhanced Data as Compressed Props

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">    &lt;<span class="pl-c1">AlertDialog.Root</span>&gt;</span>
<span class="line" data-ln="7">      &lt;<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>&gt;</span>
<span class="line" data-ln="8">        Discard draft</span>
<span class="line" data-ln="9">      &lt;/<span class="pl-c1">AlertDialog.Trigger</span>&gt;</span>
<span class="line" data-ln="10">      &lt;<span class="pl-c1">AlertDialog.Portal</span>&gt;</span>
<span class="line" data-ln="11">        &lt;<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> /&gt;</span>
<span class="line" data-ln="12">        &lt;<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>&gt;</span>
<span class="line" data-ln="13">          &lt;<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>&gt;Discard draft?&lt;/<span class="pl-c1">AlertDialog.Title</span>&gt;</span>
<span class="line" data-ln="14">          &lt;<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>&gt;</span>
<span class="line" data-ln="15">            You can't undo this action.</span>
<span class="line" data-ln="16">          &lt;/<span class="pl-c1">AlertDialog.Description</span>&gt;</span>
<span class="line" data-ln="17">          &lt;<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>&gt;</span>
<span class="line" data-ln="18">            &lt;<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>&gt;Cancel&lt;/<span class="pl-c1">AlertDialog.Close</span>&gt;</span>
<span class="line" data-ln="19">            &lt;<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>&gt;</span>
<span class="line" data-ln="20">              Discard</span>
<span class="line" data-ln="21">            &lt;/<span class="pl-c1">AlertDialog.Close</span>&gt;</span>
<span class="line" data-ln="22">          &lt;/<span class="pl-ent">div</span>&gt;</span>
<span class="line" data-ln="23">        &lt;/<span class="pl-c1">AlertDialog.Popup</span>&gt;</span>
<span class="line" data-ln="24">      &lt;/<span class="pl-c1">AlertDialog.Portal</span>&gt;</span>
<span class="line" data-ln="25">    &lt;/<span class="pl-c1">AlertDialog.Root</span>&gt;</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.

4. Highlight on Init (Optional)

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.


Size Comparison

Small Snippet

When testing the code for console.log('Hello, world!'), we see the following sizes for different approaches:

ScenarioInitial HTMLInitial PropsSuspended PropsTotal Weight%
Plain text97 bytes0 bytes0 bytes= 97 bytes100%
Plain Text Hydrated97 bytes64 bytes0 bytes= 161 bytes166%
HTML String in Props193 bytes193 bytes0 bytes= 386 bytes398%
Compressed Props97 bytes64 bytes320 bytes= 481 bytes496%
Compressed with Dict97 bytes64 bytes168 bytes= 329 bytes339%
Highlight on Init193 bytes168 bytes0 bytes= 361 bytes372%

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.

Medium Snippet

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.

ScenarioInitial HTMLInitial PropsSuspended PropsTotal Weight%
Plain text1.33 KB0 bytes0 bytes= 1.33 KB100%
Plain Text Hydrated1.33 KB1.08 KB0 bytes= 2.41 KB181%
HTML String in Props4.79 KB4.79 KB0 bytes= 9.58 KB720%
Compressed Props1.33 KB1.08 KB0.97 KB= 3.38 KB254%
Compressed with Dict1.33 KB1.08 KB0.82 KB= 3.23 KB243%
Highlight on Init4.79 KB0.82 KB0 bytes= 5.61 KB422%

For the medium snippet, decompression takes ~0.5 ms and HAST-to-JSX rendering takes ~0.84 ms, well under a single frame budget.

Very Large Snippet

With a very large code snippet, the savings become significant:

ScenarioInitial HTMLInitial PropsSuspended PropsTotal Weight%
Plain Text49 KB0 bytes0 bytes= 49 KB100%
Plain Text Hydrated49 KB47 KB0 bytes= 96 KB196%
HTML String in Props203 KB203 KB0 bytes= 406 KB829%
Compressed Props49 KB47 KB54 KB= 151 KB308%
Compressed with Dict49 KB47 KB35 KB= 131 KB267%
Highlight on Init203 KB35 KB0 bytes= 238 KB486%

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.

Streaming Comparison

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)

StepBytes SentWhat Happens in the Browser
1. Initial HTML49 KBPlain text renders, fast HTML parse
2. RSC payload47 KBHydration (minimal props, highlighting deferred)
3. Deferred payload35 KBDecompress + 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

StepBytes SentWhat Happens in the Browser
1. Initial HTML203 KBHighlighted HTML renders, slower HTML parse + style calc
2. RSC payload35 KBHydration (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.


Measured Impact

Core Web Vitals (Base UI Demos)

When this pattern was applied to the Base UI docs, profiling showed significant improvements:

MetricBeforeAfterChange
FCP (Menubar page)0.27s0.21s-22%
HTML parse time18.4ms7.8ms-58%
HTML + CSS render time43ms27ms-37%
LCP (Menu page, 6 demos)0.43s0.33s-23%
Total Blocking TimeN/A0sNo 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.

Page Weight & Performance (Type Documentation)

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.

Base UI Combobox

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 HTMLCompressed HTML
Before2675 KB276 KB
After1886 KB290 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 TimeScripting TimeCalculating StylesTotal
Before56ms49ms27ms= 132ms
After37ms34ms19ms= 90ms
Change-19ms (34%)-15ms (31%)-8ms (30%)= -42ms (29%)

docs-infra CodeHighlighter

For pages already well within the crawl budget, the pattern still provides a meaningful reduction in uncompressed HTML.

Uncompressed HTMLCompressed HTML
Change-283 KB (23%)+6 KB (5%)

When Not to Use This Pattern

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.


Implementation Details

Content-Aware Dictionary

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]
  • Text content fills the remaining ~29 KB budget.
  • Text is truncated from the end (the start is kept because first-rendered content produces the most valuable backreferences).
  • When textContent is omitted, only the static dictionary is used (opt-out).

Checksum Verification

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.

Server → Client Data Flow

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.

Compact Fallback Format

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.

Decompression & Rendering

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.

How Compressed Data Arrives

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.

Abstracting Complexity from Users

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.


Real-World Usage

This pattern is used throughout the docs-infra system:

  • CodeHighlighter: Renders plain text as the Suspense fallback, then progressively enhances with decompressed highlighted code
  • loadPrecomputedCodeHighlighter: Build-time loader that produces compressed HAST via the hastCompressed output format
  • useCode: Hook that manages decompression and caching of compressed props on the client