Lezer + CSS Custom Highlight API

Syntax highlighting with ::highlight(). The DOM stays a plain <pre><code>text</code></pre>; token ranges are registered against named highlights.

Usage

<CodeBlock> is a server component: it takes the source text and a Lezer parser, runs the parser at render time, and ships only the resulting token ranges to the client - the parser itself never enters the browser bundle.

import { parser } from '@lezer/javascript';
import CodeBlock from '@/components/CodeBlock';

export default function Page() {
  return (
    <CodeBlock
      code={`const greeting = 'hello';`}
      parser={parser}
    />
  );
}

code accepts any ReactNode, not just a string. The text content is extracted for parsing, while the original nodes are rendered inside the <pre><code> - so you can interleave elements like links or regions and they'll still be highlighted:

<CodeBlock
  parser={parser}
  code={
    <>
      {`import { `}
      <a href="https://lezer.codemirror.net/">parser</a>
      {` } from '@lezer/javascript';`}
    </>
  }
/>

Which renders as:

import { parser } from '@lezer/javascript';

Crossing the client boundary

What actually gets serialized is two fields:

The client walks this array and registers each (start, length) pair against the corresponding CSS Custom Highlight - no per-token object allocation, no spans in the DOM.

Demos

Comparison

Sizes are real HTTP response bodies. pnpm measure runs next build, starts next start on port 3100, requests each variant with Accept-Encoding: gzip, deflate, br, and records the bytes received over the wire (compressed) and after decoding (uncompressed) into data/pageSizes.json. Pass a base URL (e.g. pnpm measure https://example.com) to skip the build and measure an already-running server instead. Last measured 2026-04-24T12:02:39.325Z.

VariantUncompressed HTMLCompressed HTMLTTFB (ms)FCP (ms)LCP (ms)INP (ms)CLSBefore scroll (ms)After scroll (ms)Server-side highlightingBrowser supportInitial HTML highlightedInteractivity
ScriptLayoutPaintScriptLayoutPaint
No highlighting
/plain-text68.1KB5.5KB (br)1041931938.00.00081191.1541.825—widely available✕React components
CSS Custom Highlight API
/build-time103.8KB7KB (br)1031921928.00.000100187.2551.7110✓Baseline 2026✕React components
/build-time-compressed89.4KB7.8KB (br)1001881888.00.00092177.1541.7107✓Baseline 2026✕React components
Span-based
/html-string648KB8.7KB (br)105236236160.00079571.4552.5133✓widely available✓event delegation
/html-string-hydrated466.8KB8.5KB (br)106222222160.00084471.5552.5127✓widely available✕event delegation
/jsx-spans926.2KB84.5KB (br)110257257160.000106571.5562.2124✓widely available✓React components
/mui68.1KB23KB (br)100196196320.135103181.216513862✓widely available✕React components

Web Vitals (TTFB, FCP, LCP, INP, CLS) are collected by pnpm measure via Playwright: each variant is loaded in a real Chromium page, useReportWebVitals forwards metrics to the Node runner, and a synthetic click + tab keystroke trigger INP. Each variant is measured across 20 runs and the table shows the 75th percentile. Numbers reflect unthrottled local rendering.

The Before/After scroll timings come from a Chrome DevTools Protocol Tracing session over the same Playwright run. The page is loaded and left to settle, a performance.mark delimits the "before scroll" window, the runner scrolls through the page to the bottom and back, and a second mark closes the "after scroll" window. Trace events are bucketed by self-time into Script (JS execution, parsing, compile), Layout (style recalc, layout), and Paint (paint, composite, raster) - so you can see how much work each variant does at first render vs. during scroll. Each variant runs 20 times and the table shows the 75th percentile.

Trade-offs of the CSS Custom Highlight API