Shiki は非常に優れたコードハイライトライブラリです。
Github Repo not found
The embedded github repo could not be found…
よく知られているように、コードハイライトライブラリには多くの言語パーサーが含まれており、読み込みが遅くなり、無駄なパーサーを読み込むことで大量のトラフィックが浪費されます。
このセクションでは、Shiki に必要な言語パーサーをオンデマンドで読み込む方法を実装します。
動的読み込み#
始める前に、いくつかの準備作業を行う必要があります。
Shiki コンポーネントの定義はこのようになります。
```tsx import { bundledLanguages, getHighlighter } from 'shiki' import type { FC } from 'react'import { codeHighlighter } from './core'
const highlighter = await getHighlighter({
themes: [
import('shiki/themes/github-light.mjs'),
import('shiki/themes/github-dark.mjs'),
],
langs: Object.keys(bundledLanguages),
})
export const HighLighter: FC<{
lang: string
content: string
}> = async (props) => {
const { lang: language, content: value } = props
return (
<div
dangerouslySetInnerHTML={{
__html: codeHighlighter(highlighter, {
attrs: '',
code: value,
lang: language || '',
}),
}}
/>
)
}
</Tab>
<Tab label="page.tsx">
```tsx
const HighLighter = dynamic(() =>
import('../../components/Shiki/Shiki').then((mod) => mod.HighLighter),
)
export default () => {
return (
<HighLighter lang="javascript" content="console.log('Hello, World!')" />
)
}
このように、Shiki コンポーネントはページ上で遅延読み込みされ、ページが読み込まれた後に Shiki が読み込まれますが、すべての Shiki bundledLanguages
を参照しているため、たとえ一つの言語しか使用しなくても全パッケージが読み込まれます。
ページの読み込みは少し速くなりますが、トラフィックの浪費問題は解決されていません。
オンデマンドでの言語パーサーの読み込み#
Shiki では、必要な言語を動的に読み込む方法が提供されており、一度にすべてを読み込む必要はありません。
getLoadedLanguages
+ getLoadedLanguages
を使用して、この機能を実現するコンポーネントをラップできます。
上記のコードを改造します:
'use client'
import { use } from 'react'
const codeHighlighterPromise = (async () => {
if (typeof window === 'undefined') return
const [{ getHighlighterCore }, getWasm, { codeHighlighter }] =
await Promise.all([
// Next.js 14.2.x に問題があるため、ここではフロントエンドで dynamic import する必要があります
import('shiki/core'),
import('shiki/wasm').then((m) => m.default),
import('./core'),
])
const core = await getHighlighterCore({
themes: [
import('shiki/themes/github-light.mjs'),
import('shiki/themes/github-dark.mjs'),
],
langs: [],// ここでは言語を読み込まない
loadWasm: getWasm,
})
return {
codeHighlighter: core,
fn: (o: { lang: string; attrs: string; code: string }) => {
return codeHighlighter(core, o)
},
}
})()
export const HighLighter: FC<{
lang: string
content: string
}> = (props) => {
const highlighter = use(codeHighlighterPromise) // `use` を使用して Promise の解決を待つ
const { lang: language, content: value } = props
use(
useMemo(async () => {
async function loadShikiLanguage(language: string, languageModule: any) {
const shiki = highlighter?.codeHighlighter
if (!shiki) return
if (!shiki.getLoadedLanguages().includes(language)) { // 読み込みが必要かどうかを判断
await shiki.loadLanguage(await languageModule())
}
}
const { bundledLanguages } = await import('shiki/langs')
if (!language) return
const importFn = (bundledLanguages as any)[language]
if (!importFn) return
return loadShikiLanguage(language || '', importFn) // ここで動的に lang を読み込む
}, [highlighter?.codeHighlighter, language]),
)
const highlightedHtml = useMemo(() => {
return highlighter?.fn?.({
attrs: '',
code: value,
lang: language ? language.toLowerCase() : '',
})
}, [language, value, highlighter])
return (
<div
dangerouslySetInnerHTML={{
__html: highlightedHtml!,
}}
/>
)
}
上記の例では、use
フック + Promise の方法を使用して、必要な言語を動的に読み込むことを実現しました。
最後に、リクエスト数を見てみましょう。
かなり減りましたね、現在はリクエスト数が 17 になっています。
いくつかの後話#
Next.js App router で Shiki を Server Component で読み込まない場合、つまり動的にレンダリングされたコードのニーズがある場合、Shiki をラップするために Client Component を使用する必要がありますが、現在 App router の Client Component は一時的に top-level await をサポートしていません(おそらく永遠にサポートされないでしょう)。たとえば、上記の例では、use
を使用して Shiki HighLighter の完了を待っていますが、その代償は何でしょうか?
Shiki はサーバーサイドレンダリングされることは決してなく、これにより初回表示の CLS(ページの揺れと理解できます)が発生します。上記の例では、Shiki コンポーネントをこのように使用するしかありません。
import { lazy, Suspense } from 'react'
const HighLighter = lazy(() => // lazy または dynamic を使用します。Next.js 14.2.x では dynamic を使用するとエラーが発生しますが、低バージョンでは影響を受けません
import('../../components/Shiki/Shiki').then((mod) => ({
default: mod.HighLighter,
})),
)
export default () => {
return (
<Suspense fallback="Loading...">
<HighLighter lang="javascript" content="console.log('Hello, World!')" />
</Suspense>
)
}
結果は、リフレッシュ後に Suspense fallback Loading が表示され、その後レンダリングされるというプロセスがブラウザ内で完了します。
CLS を回避するための現在の解決策は、Suspense 内の fallback が事前に占有されることだけです。ハイライトの変換は視覚的な差異が発生しますが、少なくともページの揺れによる誤操作を避けることができます。
Note
下記の解決策は、あなたのコンポーネントの使用者が Client Component のみである場合に適用されます。したがって、以下の例では、このページは Server Component ではないと仮定します。なぜなら、ページが Server Component であれば、この解決策は無意味だからです。Server Component であれば、Suspense fallback 内で Shiki の非同期レンダリングによって生成されたコード構造をレンダリングできます。
実際には、ビジネスはこれよりもはるかに複雑です。
したがって、動的コンポーネントのネストが多すぎる場合は、Next.js App Router アーキテクチャを使用すべきではありません。少なくとも今はそうです。
import { lazy, Suspense } from 'react'
const HighLighter = lazy(() =>
import('../../components/Shiki/Shiki').then((mod) => ({
default: mod.HighLighter,
})),
)
export default () => {
const code = `console.log('Hello, World!')`
return (
<Suspense
fallback={
<pre>
<code>{code}</code>
</pre>
}
>
<HighLighter lang="javascript" content={code} />
</Suspense>
)
}
最後に、antfu に感謝します。
https://github.com/shikijs/shiki/issues/658
上記のデモは、Shiro の Shiki コンポーネント から簡略化されています。
Shiro はシンプルでありながら簡単ではない個人サイトで、すでに阮老師の週刊に登場しています。あなたもぜひ使ってみてください。🥰
Github Repo not found
The embedded github repo could not be found…
この記事は Mix Space によって xLog に同期更新されました。
元のリンクは https://innei.in/posts/dev-story/shiki-dynamic-load-language