Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 71 additions & 9 deletions package/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ A performant client-side syntax highlighting component and hook for React, built
- [Integration with react-markdown](#integration-with-react-markdown)
- [Handling Inline Code](#handling-inline-code)
- [Performance](#performance)
- [Deferred Rendering](#deferred-rendering)
- [Throttling Real-time Highlighting](#throttling-real-time-highlighting)
- [Output Format Optimization](#output-format-optimization)
- [Streaming and LLM Chat UI](#streaming-and-llm-chat-ui)
Expand Down Expand Up @@ -229,15 +230,16 @@ See [Shiki - RegExp Engines](https://shiki.style/guide/regex-engines) for more i

The `ShikiHighlighter` component offers minimal built-in styling and customization options out-of-the-box:

| Prop | Type | Default | Description |
| ------------------ | --------- | ------- | ---------------------------------------------------------- |
| `showLanguage` | `boolean` | `true` | Displays language label in top-right corner |
| `addDefaultStyles` | `boolean` | `true` | Adds minimal default styling to the highlighted code block |
| `as` | `string` | `'pre'` | Component's Root HTML element |
| `className` | `string` | - | Custom class name for the code block |
| `langClassName` | `string` | - | Class name for styling the language label |
| `style` | `object` | - | Inline style object for the code block |
| `langStyle` | `object` | - | Inline style object for the language label |
| Prop | Type | Default | Description |
| ------------------ | -------------------- | ------- | ---------------------------------------------------------- |
| `showLanguage` | `boolean` | `true` | Displays language label in top-right corner |
| `addDefaultStyles` | `boolean` | `true` | Adds minimal default styling to the highlighted code block |
| `as` | `string` | `'pre'` | Component's Root HTML element |
| `className` | `string` | - | Custom class name for the code block |
| `langClassName` | `string` | - | Class name for styling the language label |
| `style` | `object` | - | Inline style object for the code block |
| `langStyle` | `object` | - | Inline style object for the language label |
| `deferRender` | `boolean \| object` | `false` | Defer rendering until element enters viewport |

### Multi-theme Support

Expand Down Expand Up @@ -605,6 +607,66 @@ const CodeHighlight = ({

## Performance

### Deferred Rendering

For pages with many code blocks, defer syntax highlighting until blocks enter the viewport:

```tsx
// Enable with defaults (300px root margin, 300ms debounce)
<ShikiHighlighter deferRender language="tsx" theme="github-dark">
{code}
</ShikiHighlighter>

// With custom options
<ShikiHighlighter
deferRender={{
rootMargin: '500px', // Start loading 500px before viewport
debounceDelay: 200, // Debounce delay in ms
idleTimeout: 300 // requestIdleCallback timeout in ms
}}
language="tsx"
theme="github-dark"
>
{code}
</ShikiHighlighter>
```

This uses Intersection Observer + debounce + `requestIdleCallback` for optimal performance.

#### Using with the Hook

The `deferRender` prop is component-only. When using `useShikiHighlighter`, use the exported `useDeferredRender` hook directly:

```tsx
import { useShikiHighlighter, useDeferredRender } from "react-shiki";

function DeferredCodeBlock({ code, language }) {
const { shouldRender, containerRef } = useDeferredRender();

const highlighted = useShikiHighlighter(
shouldRender ? code : '',
language,
"github-dark"
);

return (
<pre ref={containerRef}>
{shouldRender ? highlighted : null}
</pre>
);
}
```

With custom options:

```tsx
const { shouldRender, containerRef } = useDeferredRender({
rootMargin: '500px',
debounceDelay: 200,
idleTimeout: 300
});
```

### Throttling Real-time Highlighting

For improved performance when highlighting frequently changing code:
Expand Down
4 changes: 4 additions & 0 deletions package/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import type {
} from './lib/types';

export { isInlineCode, rehypeInlineCodeProperty } from './lib/plugins';
export {
useDeferredRender,
type UseDeferredRenderOptions,
} from './lib/hooks/use-deferred-render';

import {
createShikiHighlighterComponent,
Expand Down
4 changes: 4 additions & 0 deletions package/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import type {
} from './lib/types';

export { isInlineCode, rehypeInlineCodeProperty } from './lib/plugins';
export {
useDeferredRender,
type UseDeferredRenderOptions,
} from './lib/hooks/use-deferred-render';

import {
createShikiHighlighterComponent,
Expand Down
44 changes: 36 additions & 8 deletions package/src/lib/component.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import './styles.css';
import { clsx } from 'clsx';
import { resolveLanguage } from './language';
import {
useDeferredRender,
type UseDeferredRenderOptions,
} from './hooks/use-deferred-render';

import type {
HighlighterOptions,
Expand All @@ -10,7 +14,7 @@ import type {
UseShikiHighlighter,
} from './types';
import type { ReactNode } from 'react';
import { forwardRef } from 'react';
import { forwardRef, useImperativeHandle } from 'react';

// 'tokens' not included: returns raw data, use hook directly for custom rendering
type ComponentRenderableFormat = 'react' | 'html';
Expand Down Expand Up @@ -104,6 +108,14 @@ export interface ShikiHighlighterProps
* @default 'pre'
*/
as?: React.ElementType;

/**
* Defer rendering until element enters viewport.
* Pass `true` for defaults or an options object.
* Uses Intersection Observer + requestIdleCallback for optimal performance.
* @default false
*/
deferRender?: boolean | UseDeferredRenderOptions;
}

export const createShikiHighlighterComponent = (
Expand All @@ -130,10 +142,22 @@ export const createShikiHighlighterComponent = (
children: code,
as: Element = 'pre',
customLanguages,
deferRender = false,
...shikiOptions
},
ref
) => {
const deferOptions: UseDeferredRenderOptions =
deferRender === true
? { immediate: false }
: deferRender === false
? { immediate: true }
: deferRender;

const { shouldRender, containerRef } =
useDeferredRender(deferOptions);
useImperativeHandle(ref, () => containerRef.current as HTMLElement);

const options: HighlighterOptions<ComponentRenderableFormat> = {
delay,
transformers,
Expand All @@ -152,7 +176,7 @@ export const createShikiHighlighterComponent = (
);

const highlightedCode = useShikiHighlighterImpl(
code,
shouldRender ? code : '',
language,
theme,
options
Expand All @@ -162,7 +186,7 @@ export const createShikiHighlighterComponent = (

return (
<Element
ref={ref}
ref={containerRef}
data-testid="shiki-container"
className={clsx(
'relative',
Expand All @@ -182,11 +206,15 @@ export const createShikiHighlighterComponent = (
{displayLanguageId}
</span>
) : null}
{isHtmlOutput ? (
<div dangerouslySetInnerHTML={{ __html: highlightedCode }} />
) : (
highlightedCode
)}
{shouldRender ? (
isHtmlOutput ? (
<div
dangerouslySetInnerHTML={{ __html: highlightedCode }}
/>
) : (
highlightedCode
)
) : null}
</Element>
);
}
Expand Down
Loading