Skip to content

Commit 1f8e416

Browse files
authored
Highlight code client-side (#2797)
1 parent cfccc44 commit 1f8e416

File tree

16 files changed

+517
-515
lines changed

16 files changed

+517
-515
lines changed

.changeset/thin-files-flow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'gitbook': patch
3+
---
4+
5+
Improve performances by highlighting code client-side if the code block is offscreen

packages/gitbook/src/components/Ads/Ad.tsx

Lines changed: 5 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { t, useLanguage } from '@/intl/client';
1313
import { ClassValue, tcls } from '@/lib/tailwind';
1414

1515
import { renderAd } from './renderAd';
16+
import { useHasBeenInViewport } from '../hooks/useHasBeenInViewport';
1617
import { useTrackEvent } from '../Insights';
1718
import { Link } from '../primitives';
1819

@@ -43,7 +44,6 @@ export function Ad({
4344
mode?: 'classic' | 'auto' | 'cover';
4445
}) {
4546
const containerRef = React.useRef<HTMLDivElement>(null);
46-
const [visible, setVisible] = React.useState(false);
4747
const [ad, setAd] = React.useState<
4848
{ children: React.ReactNode; insightsAd: SiteInsightsAd | null } | undefined
4949
>(undefined);
@@ -57,42 +57,14 @@ export function Ad({
5757
ad: ad.insightsAd,
5858
});
5959
}
60-
}, [ad]);
60+
}, [ad, trackEvent]);
6161

62-
// Observe the container visibility
63-
React.useEffect(() => {
64-
if (!containerRef.current) {
65-
return;
66-
}
67-
68-
if (typeof IntersectionObserver === 'undefined') {
69-
return;
70-
}
71-
72-
const observer = new IntersectionObserver(
73-
([entry]) => {
74-
if (entry.isIntersecting) {
75-
setVisible(true);
76-
}
77-
},
78-
{
79-
root: null,
80-
rootMargin: '0px',
81-
threshold: 0.1,
82-
},
83-
);
84-
85-
observer.observe(containerRef.current);
86-
87-
return () => {
88-
observer.disconnect();
89-
};
90-
}, []);
62+
const hasBeenInViewport = useHasBeenInViewport(containerRef, { threshold: 0.1 });
9163

9264
// When the container is visible,
9365
// track an impression on the ad and fetch it
9466
React.useEffect(() => {
95-
if (!visible) {
67+
if (!hasBeenInViewport) {
9668
return;
9769
}
9870

@@ -136,7 +108,7 @@ export function Ad({
136108
return () => {
137109
cancelled = true;
138110
};
139-
}, [visible, zoneId, ignore, placement, mode, siteAdsStatus]);
111+
}, [hasBeenInViewport, zoneId, ignore, placement, mode, siteAdsStatus]);
140112

141113
return (
142114
<div ref={containerRef} className={tcls(style)} data-visual-test="removed">

packages/gitbook/src/components/DocumentView/Annotation/Annotation.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Blocks } from '../Blocks';
77
import { InlineProps } from '../Inline';
88
import { Inlines } from '../Inlines';
99

10-
export async function Annotation(props: InlineProps<DocumentInlineAnnotation>) {
10+
export function Annotation(props: InlineProps<DocumentInlineAnnotation>) {
1111
const { inline, context, document, children } = props;
1212

1313
const fragment = getNodeFragmentByType(inline, 'annotation-body');
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
'use client';
2+
3+
import type { DocumentBlockCode } from '@gitbook/api';
4+
import { useEffect, useRef, useState } from 'react';
5+
6+
import { useHasBeenInViewport } from '@/components/hooks/useHasBeenInViewport';
7+
8+
import type { HighlightLine, RenderedInline } from './highlight';
9+
import type { BlockProps } from '../Block';
10+
import { CodeBlockRenderer } from './CodeBlockRenderer';
11+
import { plainHighlight } from './plain-highlight';
12+
13+
type ClientBlockProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'style'> & {
14+
inlines: RenderedInline[];
15+
};
16+
17+
/**
18+
* Render a code-block client-side by loading the highlighter asynchronously.
19+
* It allows us to defer some load to avoid blocking the rendering of the whole page with block highlighting.
20+
*/
21+
export function ClientCodeBlock(props: ClientBlockProps) {
22+
const { block, style, inlines } = props;
23+
const blockRef = useRef<HTMLDivElement>(null);
24+
const [lines, setLines] = useState<HighlightLine[]>(() => plainHighlight(block, []));
25+
26+
// Preload the highlighter when the block is mounted.
27+
useEffect(() => {
28+
import('./highlight').then(({ preloadHighlight }) => preloadHighlight(block));
29+
}, [block]);
30+
31+
// Check if the block is in the viewport to start highlighting it.
32+
const hasBeenInViewport = useHasBeenInViewport(blockRef, {
33+
rootMargin: '200px',
34+
});
35+
36+
// Highlight the block when it's in the viewport.
37+
useEffect(() => {
38+
if (hasBeenInViewport) {
39+
let canceled = false;
40+
import('./highlight').then(({ highlight }) => {
41+
// We use requestIdleCallback to avoid blocking the main thread
42+
// when scrolling.
43+
if (typeof requestIdleCallback === 'function') {
44+
requestIdleCallback(() => highlight(block, inlines).then(setLines));
45+
} else {
46+
highlight(block, inlines).then(setLines);
47+
}
48+
});
49+
return () => {
50+
canceled = true;
51+
};
52+
}
53+
}, [hasBeenInViewport, block, inlines]);
54+
55+
return <CodeBlockRenderer ref={blockRef} block={block} style={style} lines={lines} />;
56+
}

0 commit comments

Comments
 (0)