概要
本ブログで使用しているSyntaxHighlighter(react-syntax-highlighter)からモダンなShikiへの移行について、ポイントとなる部分を記録として残しておきたいと思います。
主にNext.js(App Router)でブログやドキュメントサイトを運用している方を対象に、Shikiのサーバーコンポーネントを活用したシンタックスハイライトの実装方法やライトモード/ダークモード対応について紹介したいと思います。
Shikiとは
Shikiは、Visual Studio Codeでも使用されているTextMate文法やテーマを利用して高品質なシンタックスハイライトを提供するライブラリです。ShikiはサーバーサイドでコードをHTMLに変換するため、クライアント側のバンドルサイズを削減したり、クライアントサイドでの変換を行わないのでパフォーマンスが良いという特徴があります。
実際にShikiを使ってハイライトしたコードがこちら。見やすく、リロードしてもそのまま表示されます。
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.0 | Shiki 3.21.0 |
| レンダリング | クライアント側(動的) | サーバー側(静的HTML生成) |
| 実装方式 | <SyntaxHighlighter>コンポーネント | codeToHtml()でHTML文字列生成 |
| コンポーネント | クライアントコンポーネント | サーバーコンポーネント |
| バンドルサイズ | 150KB(クライアント側) | 0KB(サーバー側のみ) |
| ハイライト処理 | ブラウザ実行時 | ビルド時・リクエスト時 |
| テーマ切り替え | JavaScript + MutationObserver | CSS変数のみ |
なぜShikiに移行するのか
Shikiの強みの1つはサーバー側レンダリング対応です。React Server ComponentでShikiを使用することにより、クライアント側のバンドルサイズを完全にゼロにできます。これにより、react-syntax-highlighterの150KBから98%削減できます。
さらに、Visual Studio Codeと同じTextMate文法による高精度なハイライト、多くのテーマサポート、CSS変数のみでのテーマ切り替えなど、品質とパフォーマンスの両面で優れた特徴を持っています。
| 項目 | react-syntax-highlighter | Shiki |
|---|---|---|
| サーバー側レンダリング | 非対応(クライアント専用) | 対応(React Server Component) → クライアントバンドル0KB |
| クライアント側バンドル | フル: ~150KB Async: 17KB + 言語 | フル: ~200KB(4リクエスト) Fine-grained Bundle対応 |
| 初回表示速度 | 動的インポート待機 | サーバー: 即座(HTML埋め込み) クライアント: 動的インポート待機 |
| ハイライト精度 | Prism.js / highlight.js | VSCode同等(TextMate文法) |
| 対応言語数 | hljs: ~190言語 Prism: ~280言語 | 200+言語(TextMate文法) |
| 対応テーマ数 | 数十種類(hljs + Prism) | 100+(VSCodeテーマ全対応) |
| テーマ切り替え | JavaScript + MutationObserver | CSS変数のみ(デュアルテーマ) |
| カスタマイズ性 | PreTag/CodeTag/renderer linePropsなど | Transformers(HAST変換) デコレーション・カスタムテーマ |
MDXのコードブロックにShikiを組み込む
実際にShikiを組み込む手順を解説します。サーバーコンポーネントとしてコードブロックを実装し、MDXと統合することでクライアントバンドルを使用せずにシンタックスハイライトを導入します。
Shikiのインストール
まず、Shikiをプロジェクトにインストールします。
npm install shiki
CodeBlockコンポーネントの作成
コードブロック用のサーバーコンポーネントを作成します。Shikiの公式ドキュメントでは、codeToHtmlを使った実装が推奨されています。
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コンポーネントを使用するように設定します。
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でハイライトされるようになります。
\`\`\`typescript
const message: string = "Hello, Shiki!";
console.log(message);
\`\`\`
テーマ設定(ライト/ダークモード対応)
Shikiのデュアルテーマ機能を使うことで、JavaScriptなしでライトモード・ダークモードの切り替えを実現できます。
オプションのdefaultColor: falseを設定すると、ShikiはCSS変数(--shiki-lightと--shiki-dark)を生成します。
これらの変数を使ってテーマを切り替えるには、global.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による再レンダリングや色の再計算は不要です。
もしくは、メディアクエリでシステムのテーマ設定に応じて切り替えることもできます。
@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の導入をお勧めします。
