ToolSite

Syntax Highlighting Without JavaScript Libraries

Syntax highlighting with zero client JS. Build-time via Shiki, Prism, highlight.js. No layout shift, no bundle cost, instant paint. Try our free highlighter.

By ToolSite5 min readguides

The Problem with Client-Side Highlighters

Most blogs add syntax highlighting with a client-side library like Prism.js or highlight.js. The page loads as plain HTML. Then JavaScript runs. It scans every <pre><code> block, tokenizes the source code inside, wraps keywords and strings in <span> elements, and injects CSS classes. The browser repaints the code blocks in color.

This approach works. But it has real costs:

  • Layout shift. The code block renders as unstyled monospace text first. After JavaScript parses and highlights, the content jumps to colored, styled form. On a slow connection, the flash of unstyled code is visible and jarring. Cumulative Layout Shift (CLS) scores take a hit.
  • JavaScript dependency. Without JavaScript, your code blocks look broken. Users who disable JS, read via RSS, or view the page in Reader Mode see plain text. Your technical blog looks unfinished.
  • Bundle weight. Prism.js with support for 12 languages is roughly 15 KB minified and gzipped. Not huge, but not zero. highlight.js with a similar language set is about 18 KB. These numbers grow with each additional language.
  • CPU cost on every page. The highlighting runs on every page load, scanning every code block. On a documentation page with 20 code samples, the parser iterates over thousands of lines of text. On a fast desktop, it is imperceptible. On a budget Android phone, it adds measurable delay to interactivity.

The alternative is server-side highlighting. You generate the <span>-wrapped HTML at build time or request time. The browser receives already-highlighted code. No JavaScript runs. No layout shift. No flash of unstyled text.

How Build-Time Highlighting Works

A syntax highlighter takes two inputs: source code and a language identifier. It outputs HTML with <span> elements that carry CSS classes for each token type.

Input (JavaScript source):

function greet(name) {
  return `Hello, ${name}`;
}

Output (simplified, using highlight.js class names):

<span class="hljs-keyword">function</span>
<span class="hljs-title function_">greet</span>(<span class="hljs-params">name</span>) {
  <span class="hljs-keyword">return</span>
  <span class="hljs-string">`Hello, ${name}`</span>;
}

The output is static HTML with semantic class names. A CSS theme (roughly 2 KB) provides the colors:

.hljs-keyword { color: #d73a49; }
.hljs-string { color: #032f62; }
.hljs-title { color: #6f42c1; }
.hljs-params { color: #24292e; }

The browser receives HTML and CSS together. Both are in the initial payload. The code renders highlighted on the first paint. Zero JavaScript.

Using Build-Time Highlighters

If you use a static site generator, highlight code at build time:

  • Next.js (MDX). Add rehype-highlight or rehype-shiki to your MDX plugin chain. Code blocks are highlighted during the static build and baked into the HTML output. No client JS ships.
  • Astro. Shiki integration is built in and runs at build time by default. You configure a theme in astro.config.mjs and every fenced code block in your Markdown files gets highlighted during the build.
  • Hugo. Uses Chroma, a Go syntax highlighter, at build time. Configure it in config.toml with [markup.highlight]. No client-side library needed.
  • Eleventy. The @11ty/eleventy-plugin-syntaxhighlight plugin wraps Prism but runs it at build time. The output is static HTML. The Prism JS library never ships to the client.
  • Jekyll. Rouge (Ruby) runs at build time. Add kramdown with syntax_highlighter: rouge in _config.yml.

All five approaches produce static HTML with syntax highlighting and zero client-side JavaScript for the highlighting engine.

One-Off Highlighting

If you are not using a static site generator, or you are writing a one-off blog post and just need highlighted HTML to paste in, use the Syntax Highlighter:

  1. Paste your code into the tool and select the language.
  2. Copy the highlighted HTML output. It contains <span> elements with class names from your chosen highlighter theme.
  3. Paste the HTML into your blog post or CMS.
  4. Include the highlighter theme CSS in your page's <head> (a static 2 KB stylesheet, no JavaScript).

The HTML is static. The CSS is static. No JavaScript runs on the client to produce the highlighting.

The CSS Options

Build-time highlighting only solves the HTML generation. You still need CSS to color the <span> elements. You have three options:

  • External theme stylesheet. Include a highlight.js or Prism theme CSS file in your global styles. Approximately 2 KB. One HTTP request. Works everywhere.
  • Inline the theme CSS. For a page with one or two code blocks, put the 2 to 3 KB of theme CSS in a <style> tag in the <head>. This avoids an extra HTTP request at the cost of a slightly larger HTML payload.
  • Inline styles on each token. Some highlighters (Shiki) can output style attributes directly on each <span> instead of CSS class names. The HTML is self-styled with no external CSS file at all:
<span style="color:#d73a49">function</span>
<span style="color:#6f42c1">greet</span>(<span style="color:#24292e">name</span>) {
  <span style="color:#d73a49">return</span>
  <span style="color:#032f62">`Hello, ${name}`</span>;
}

The tradeoff is larger HTML (each <span> carries a style attribute) versus zero external dependencies. For a page with many code blocks, the class-based approach with a shared stylesheet is smaller because each class name is reused across all tokens.

Performance Comparison

Measured on a blog post with 8 code blocks (roughly 200 lines of code total):

| Approach | JS shipped | CSS shipped | First paint with highlighting | |---|---|---|---| | Client-side (Prism.js) | 15 KB | 2 KB | After JS executes (~200ms on fast desktop) | | Build-time (highlight.js) | 0 KB | 2 KB | Immediate | | Build-time (Shiki, inline styles) | 0 KB | 0 KB | Immediate |

The build-time approaches eliminate the JavaScript dependency entirely. The highlighted code is present in the initial HTML. There is no flash of unstyled text and no parse delay.

For most blogs, a 2 KB theme stylesheet with build-time highlighting is the right tradeoff. The CSS is loaded once and cached. No JavaScript runs. Every code block renders highlighted on the first paint.

Try it yourself: open the Syntax Highlighter. Paste const x = 42; and select JavaScript. The tool generates highlighted HTML with <span> tags and class names. Copy the HTML and paste it into a test page with a highlight.js theme CSS. The code renders in color with zero JavaScript. Then paste the same code into the Markdown to HTML Converter to see how a fenced code block with a language tag produces the pre/code structure that a highlighter operates on.

Related Reading