Optimize Shiki's HTML Output With styleToClass Transformer
Shiki provides more accurate code highlighting than highlight.js. Take console.log("hello") as an example: the Shiki highlighter semantically splits it into six <span> elements and adds inline styles (e.g. color, font-style, and CSS variables[1]) to support multi-theme adaptation for color[2] and font[3].
const code = await codeToHtml('console.log("hello")', { lang: "javascript", themes: { light: "min-light", dark: "nord" },});<code> <span class="line"> <span style="color:#1976D2;--shiki-dark:#D8DEE9">console</span> <span style="color:#6F42C1;--shiki-dark:#ECEFF4">.</span> <span style="color:#6F42C1;--shiki-dark:#88C0D0">log</span> <span style="color:#24292EFF;--shiki-dark:#D8DEE9FF">(</span> <span style="color:#22863A;--shiki-dark:#ECEFF4">"</span> <span style="color:#22863A;--shiki-dark:#A3BE8C">hello</span> <span style="color:#22863A;--shiki-dark:#ECEFF4">"</span> <span style="color:#24292EFF;--shiki-dark:#D8DEE9FF">)</span> </span></code><code> <span class="line"> <span class="_sk_p7ztx5">console</span> <span class="_sk_8gyisu">.</span> <span class="_sk_18gpmt">log</span> <span class="_sk_23aifj space">(</span> <span class="_sk_23aifj">"</span> <span class="_sk_8gyisu">hello</span> <span class="_sk_18gpmt">""</span> <span class="_sk_p7ztx5 space">)</span> </span></code>In the example above, the HTML size is still acceptable. First, the code itself is very small. Second, only two themes, min-light and nord, are applied, and those themes only embed the color property and the --shiki-dark variable in the sample code.
When these conditions no longer hold, the generated HTML size can increase sharply. For example, my blog contains some long code snippets and applies five themes: catppuccin-latte, catppuccin-mocha, tokyo-night, nord, and tokyo-night. The generated HTML becomes a bit bloated. This post records how to use the styleToClass Transformer to optimize Shiki’s output.
Solutions
The operation is simple: import the styleToClass Transformer from @shikijs/transformers, then pass it when calling codeToHtml.import { writeFileSync } from "fs";import { transformerStyleToClass } from "@shikijs/transformers";import { codeToHtml } from "shiki";const toClass = transformerStyleToClass({ classPrefix: "__shiki_",});const code = `console.log('hello')`;const html = await codeToHtml(code, { lang: "ts", themes: { dark: "vitesse-dark", light: "vitesse-light" }, transformers: [toClass],});const css = toClass.getCSS();// 将生成的CSS写入~/Downloads/shiki_style_to_class.css文件writeFileSync( join( process.env.HOME || process.env.USERPROFILE || ".", "Downloads/shiki_style_to_class.css" ), cssContent, "utf-8");
One detail to note: when used with The working principle is excerpted from the official documentation: Class names are generated based on the hash value of the style object with the prefix/suffix you provide. You can put this transformer in multiple highlights passes and then get the CSS at the end to reuse the exact same styles. As Shiki doesn’t handle CSS, it’s on your integration to decide how to extract and apply/bundle the CSS.@shikijs/colorized-brackets, the styleToClass Transformer should be placed after colorizedBrackets; otherwise, the styles from colorizedBrackets will still be inlined into the HTML instead of being converted into class names. Similarly, other transformers that generate inline styles should also be placed before styleToClass.
Convert Shiki’s inline styles to unique classes.
After writing the CSS to a file, include that CSS file in the blog or website. For Hexo blogs, you can use the Efterklang/hexo-shiki-highlight plugin.
Result
Using the following CSS code block as an example, I compared the generated <code> size before and after enabling the styleToClass Transformer. The enabled themes are the five themes mentioned above.```csscode span { font-style: var(--shiki-light-font-style);}:where([data-theme="tokyo_night"]) { code span { font-style: var(--shiki-tokyo-font-style); color: var(--shiki-tokyo); }}:where([data-theme="mocha"], [data-theme="macchiato"]) { code span { font-style: var(--shiki-dark-font-style); color: var(--shiki-dark); }}```Transformer HTML size styleToClass enabled5.2KBstyleToClass disabled11.0KB
After testing several code snippets, it can generally reduce HTML size by 45%+ (while introducing 20KB+ of CSS). The main advantages of using external CSS instead of inline CSS are reusability, maintainability, and readability. Also, inline CSS does not support pseudo-class styles such as hover, while external CSS can handle them easily.
For size reduction alone, unless the code volume is large or multiple themes are applied, the reduction may not be obvious 👉 👈
Custom properties (sometimes referred to as CSS variables or cascading variables) are entities defined by CSS authors that represent specific values to be reused throughout a document. They are set using the
font-weightat-rule or by custom property syntax (e.g.,text-decoration). Custom properties are accessed using the CSS--shiki-{theme-name}-font-weightfunction (e.g.,--shiki-{theme-name}-text-decolor);). Using CSS custom properties (variables) - CSS | MDN ↩︎In code snippets generated by the Shiki highlighter, the default theme’s color styles, such as
colorandbackground-color, are included directly. For non-default themes, these styles are stored in CSS variables such as--shiki-{theme}and--shiki-{theme}-bg, where{theme}is the key name in thethemesfield passed tocodeToHtml. Light and dark mode | Shiki Chinese Documentation ↩︎The same principle applies to font styles:
font-weightandtext-decorationfor the default theme,--shiki-{theme}-font-weightand--shiki-{theme}-text-decorationfor other themes. ↩︎