MDXのシンタックスハイライト:react-syntax-highlighterからShikiへの移行

7分で読めます

概要

本ブログで使用しているSyntaxHighlighter(react-syntax-highlighter)からモダンなShikiへの移行について、ポイントとなる部分を記録として残しておきたいと思います。

主にNext.js(App Router)でブログやドキュメントサイトを運用している方を対象に、Shikiのサーバーコンポーネントを活用したシンタックスハイライトの実装方法やライトモード/ダークモード対応について紹介したいと思います。

Shikiとは

Shikiは、Visual Studio Codeでも使用されているTextMate文法やテーマを利用して高品質なシンタックスハイライトを提供するライブラリです。ShikiはサーバーサイドでコードをHTMLに変換するため、クライアント側のバンドルサイズを削減したり、クライアントサイドでの変換を行わないのでパフォーマンスが良いという特徴があります。

実際にShikiを使ってハイライトしたコードがこちら。見やすく、リロードしてもそのまま表示されます。

TypeScript
import { codeToHtml } from 'shiki';
const html = await codeToHtml(`const message: string = "Hello, Shiki!";`, {
  lang: 'typescript',
  themes: {
    light: 'github-light',
    dark: 'github-dark',
  },
  defaultColor: false,
});
console.log(html);

前提条件

本ブログはNext.js(App Router)を使用し、MDXによる記事管理を行っています。今回はMDX内のコードブロックに使用しているSyntaxHighlighterをShikiに置き換えます。

  • Next.js 15.5.9 (App Router)
  • React 19.0.0
  • TypeScript 5
  • MDX 3.1.0 (@mdx-js/loader 3.1.0, @mdx-js/react 3.1.0)
  • Tailwind CSS 4.1.11

ShikiはNext.js以外にも、Astro・Vite・Nuxt・Remix・Gatsby・SvelteKitなど主要なフレームワークで利用できます。また、Rehype/Remarkプラグインとして任意のMDXパイプラインに統合することも可能です。

移行対象

react-syntax-highlighter を使用して動的にコードブロックをハイライトしていますが、Shikiに移行してサーバー側でHTMLを生成する形に変更します。

項目移行前移行後
パッケージreact-syntax-highlighter 16.1.0Shiki 3.21.0
レンダリングクライアント側(動的)サーバー側(静的HTML生成)
実装方式<SyntaxHighlighter>コンポーネントcodeToHtml()でHTML文字列生成
コンポーネントクライアントコンポーネントサーバーコンポーネント
バンドルサイズ150KB(クライアント側)0KB(サーバー側のみ)
ハイライト処理ブラウザ実行時ビルド時・リクエスト時
テーマ切り替えJavaScript + MutationObserverCSS変数のみ

なぜShikiに移行するのか

Shikiの強みの1つはサーバー側レンダリング対応です。React Server ComponentでShikiを使用することにより、クライアント側のバンドルサイズを完全にゼロにできます。これにより、react-syntax-highlighterの150KBから98%削減できます。

さらに、Visual Studio Codeと同じTextMate文法による高精度なハイライト、多くのテーマサポート、CSS変数のみでのテーマ切り替えなど、品質とパフォーマンスの両面で優れた特徴を持っています。

項目react-syntax-highlighterShiki
サーバー側レンダリング非対応(クライアント専用)対応(React Server Component)
→ クライアントバンドル0KB
クライアント側バンドルフル: ~150KB
Async: 17KB + 言語
フル: ~200KB(4リクエスト)
Fine-grained Bundle対応
初回表示速度動的インポート待機サーバー: 即座(HTML埋め込み)
クライアント: 動的インポート待機
ハイライト精度Prism.js / highlight.jsVSCode同等(TextMate文法)
対応言語数hljs: ~190言語
Prism: ~280言語
200+言語(TextMate文法)
対応テーマ数数十種類(hljs + Prism)100+(VSCodeテーマ全対応)
テーマ切り替えJavaScript + MutationObserverCSS変数のみ(デュアルテーマ)
カスタマイズ性PreTag/CodeTag/renderer
linePropsなど
Transformers(HAST変換)
デコレーション・カスタムテーマ

MDXのコードブロックにShikiを組み込む

実際にShikiを組み込む手順を解説します。サーバーコンポーネントとしてコードブロックを実装し、MDXと統合することでクライアントバンドルを使用せずにシンタックスハイライトを導入します。

Shikiのインストール

まず、Shikiをプロジェクトにインストールします。

Bash
npm install shiki

CodeBlockコンポーネントの作成

コードブロック用のサーバーコンポーネントを作成します。Shikiの公式ドキュメントでは、codeToHtmlを使った実装が推奨されています。

TypeScript
import { codeToHtml } from 'shiki';
import type { BundledLanguage } from 'shiki';

type CodeBlockProps = {
  children: string;
  lang: BundledLanguage; // 対応言語を型で指定
};

export async function CodeBlock({ children, lang }: CodeBlockProps) { // codeToHtmlは非同期なためasync関数に
  const html = await codeToHtml(children, {
    lang,
    themes: { // デュアルテーマ設定
      light: 'github-light',
      dark: 'github-dark',
    },
    defaultColor: false, // デフォルトカラー無効化(CSS変数使用)
  });

  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

MDXとの統合

次に、MDXProviderで先ほど作成したCodeBlockコンポーネントを使用するように設定します。

TypeScript
import { MDXProvider } from '@mdx-js/react';
import { CodeBlock } from './CodeBlock';

const components = {
  pre: ({ children }: { children: React.ReactNode }) => {
    // preタグの中のcodeタグを取得
    const child = React.Children.only(children) as React.ReactElement;
    if (child.type === 'code') {
      return (
        <CodeBlock lang={child.props.className?.replace(/language-/, '')}>
          {child.props.children}
        </CodeBlock>
      );
    }
    return <pre>{children}</pre>;
  },
};

export function CustomMDXProvider({ children }: { children: React.ReactNode }) {
  return <MDXProvider components={components}>{children}</MDXProvider>;
}

これで、MDX内のコードブロックが自動的にShikiでハイライトされるようになります。

Markdown
\`\`\`typescript
const message: string = "Hello, Shiki!";
console.log(message);
\`\`\`

テーマ設定(ライト/ダークモード対応)

Shikiのデュアルテーマ機能を使うことで、JavaScriptなしでライトモード・ダークモードの切り替えを実現できます。
オプションのdefaultColor: falseを設定すると、ShikiはCSS変数(--shiki-light--shiki-dark)を生成します。

これらの変数を使ってテーマを切り替えるには、global.cssに以下のスタイルを追加します。

CSS
/* ライトモード時にShikiのライトテーマを表示 */
html:not(.dark) .shiki,
html:not(.dark) .shiki span {
  color: var(--shiki-light) !important;
  background-color: var(--shiki-light-bg) !important;
}

/* ダークモード時にShikiのダークテーマを表示 */
html.dark .shiki,
html.dark .shiki span {
  color: var(--shiki-dark) !important;
  background-color: var(--shiki-dark-bg) !important;
}

テーマ切り替えの仕組み

ShikiはdefaultColor: falseを設定すると、生成するHTMLの各要素に--shiki-light--shiki-darkの両方のCSS変数をインラインスタイルとして埋め込みます。つまり、すべての色情報が最初からHTMLに含まれているため、CSSで「どちらの変数を使うか」を切り替えるだけでテーマが変わります。JavaScriptによる再レンダリングや色の再計算は不要です。

もしくは、メディアクエリでシステムのテーマ設定に応じて切り替えることもできます。

CSS
@media (prefers-color-scheme: dark) {
  .shiki,
  .shiki span {
    color: var(--shiki-dark) !important;
    background-color: var(--shiki-dark-bg) !important;
  }
}

この設定により、ユーザーのテーマ設定(ライト/ダーク)に応じて、自動的にコードブロックのテーマが切り替わります。JavaScriptで処理する必要がないのはとても楽で良いですね。

まとめ

Shikiを活用することでVSCode同等のハイライトを実現しながら、クライアントバンドルサイズをゼロにできました。テーマ管理はCSS変数のみで完結するため、JavaScriptによる動的な切り替えも不要になります。パフォーマンスとユーザー体験を両立できる良い仕組みだと思います。

Next.js(App Router)でMDXを使用しているプロジェクトにおいて、シンタックスハイライトの最適化を検討している方には、ぜひShikiの導入をお勧めします。

この記事は役に立ちましたか?

この記事をシェア

X
Facebook
はてな
utsusieのプロフィール画像

utsusie

UI Designer / Web Director