Investigate performance issues with InlineLink.

This commit is contained in:
Steven Hall
2025-06-16 22:27:37 +01:00
parent b7a0db3339
commit 71dfd77c8e
14 changed files with 190 additions and 196 deletions
@@ -4,16 +4,13 @@ import { getNodeFragmentByType } from '@/lib/document';
import { Blocks } from '../Blocks';
import type { InlineProps } from '../Inline';
import { Inlines } from '../Inlines';
import { AnnotationPopover } from './AnnotationPopover';
export function Annotation(props: InlineProps<DocumentInlineAnnotation>) {
const { inline, context, document, children } = props;
const fragment = getNodeFragmentByType(inline, 'annotation-body');
const content = children ?? (
<Inlines document={document} context={context} nodes={inline.nodes} ancestorInlines={[]} />
);
const content = children;
if (!fragment) {
return <>{content}</>;
@@ -46,7 +46,7 @@ export interface BlockProps<Block extends DocumentBlock> extends DocumentContext
style?: ClassValue;
}
export function Block<T extends DocumentBlock>(props: BlockProps<T>) {
export function renderBlock<T extends DocumentBlock>(props: BlockProps<T>) {
const { block, style, isEstimatedOffscreen, context } = props;
const content = (() => {
@@ -1,8 +1,7 @@
import type { DocumentBlock, JSONDocument } from '@gitbook/api';
import { type ClassValue, tcls } from '@/lib/tailwind';
import { Block } from './Block';
import { renderBlock } from './Block';
import type { DocumentContextProps } from './DocumentView';
import { isBlockOffscreen } from './utils';
@@ -25,7 +24,7 @@ export function Blocks<TBlock extends DocumentBlock, Tag extends React.ElementTy
return (
<Tag {...wrapperProps} className={tcls(style)}>
<UnwrappedBlocks {...blocksProps} />
{renderUnwrappedBlocks(blocksProps)}
</Tag>
);
}
@@ -50,7 +49,9 @@ type UnwrappedBlocksProps<TBlock extends DocumentBlock> = DocumentContextProps &
/**
* Renders a list of blocks without a wrapper element.
*/
export function UnwrappedBlocks<TBlock extends DocumentBlock>(props: UnwrappedBlocksProps<TBlock>) {
export function renderUnwrappedBlocks<TBlock extends DocumentBlock>(
props: UnwrappedBlocksProps<TBlock>
) {
const { nodes, blockStyle, isOffscreen: defaultIsOffscreen = false, ...contextProps } = props;
let isOffscreen = defaultIsOffscreen;
@@ -63,20 +64,18 @@ export function UnwrappedBlocks<TBlock extends DocumentBlock>(props: UnwrappedBl
ancestorBlocks: props.ancestorBlocks,
});
return (
<Block
key={node.key || `${node.type}-${index}`}
block={node}
style={[
'mx-auto w-full decoration-primary/6',
node.data && 'fullWidth' in node.data && node.data.fullWidth
? 'max-w-screen-2xl'
: 'page-full-width:ml-0 max-w-3xl',
blockStyle,
]}
isEstimatedOffscreen={isOffscreen}
{...contextProps}
/>
);
return renderBlock({
key: node.key || `${node.type}-${index}`,
block: node,
style: [
'mx-auto w-full decoration-primary/6',
node.data && 'fullWidth' in node.data && node.data.fullWidth
? 'max-w-screen-2xl'
: 'page-full-width:ml-0 max-w-3xl',
blockStyle,
],
isEstimatedOffscreen: isOffscreen,
...contextProps,
});
});
}
@@ -10,7 +10,6 @@ import { getNodeFragmentByName, isNodeEmpty } from '@/lib/document';
import { type ClassValue, tcls } from '@/lib/tailwind';
import type { DocumentContextProps } from './DocumentView';
import { Inlines } from './Inlines';
/**
* Wrap a content of a block that has a potential caption.
@@ -62,14 +61,6 @@ export function Caption(
return (
<picture className={tcls('relative', style)}>
<div className={tcls(wrapperStyle, 'mx-auto')}>{children}</div>
<figcaption className={tcls('text-sm', 'text-center', 'mt-2', 'text-tint')}>
<Inlines
nodes={captionParagraph.nodes}
document={document}
context={context}
ancestorInlines={[]}
/>
</figcaption>
</picture>
);
}
@@ -6,7 +6,6 @@ import { tcls } from '@/lib/tailwind';
import type { BlockProps } from '../Block';
import { Blocks } from '../Blocks';
import { Inlines } from '../Inlines';
import { Details } from './Details';
export function Expandable(props: BlockProps<DocumentBlockExpandable>) {
@@ -54,12 +53,6 @@ export function Expandable(props: BlockProps<DocumentBlockExpandable>) {
'group-open/expandable:rotate-90'
)}
/>
<Inlines
nodes={titleParagraph.nodes}
document={document}
context={context}
ancestorInlines={[]}
/>
<a
href={`#${id}`}
aria-label="Direct link to heading"
@@ -4,7 +4,6 @@ import { tcls } from '@/lib/tailwind';
import type { BlockProps } from './Block';
import { HashLinkButton, hashLinkButtonWrapperStyles } from './HashLinkButton';
import { Inlines } from './Inlines';
import { getBlockTextStyle } from './spacing';
export function Heading(props: BlockProps<DocumentBlockHeading>) {
@@ -46,9 +45,7 @@ export function Heading(props: BlockProps<DocumentBlockHeading>) {
textStyle.lineHeight,
textStyle.marginTop
)}
>
<Inlines {...rest} context={context} nodes={block.nodes} ancestorInlines={[]} />
</div>
></div>
</Tag>
);
}
@@ -3,7 +3,7 @@ import { Icon, type IconName } from '@gitbook/icons';
import { type ClassValue, tcls } from '@/lib/tailwind';
import { Block, type BlockProps } from './Block';
import type { BlockProps } from './Block';
import { Blocks } from './Blocks';
import { getBlockTextStyle } from './spacing';
@@ -49,17 +49,6 @@ export function Hint(props: BlockProps<DocumentBlockHint>) {
className={tcls('size-[1.2em]', 'mt-px', firstLine.lineHeight)}
/>
</div>
{hasHeading ? (
<Block
style={tcls(
'flip-heading-hash p-4 pl-3 text-[1em] *:mt-0',
hasHeading ? hintStyle.header : null
)}
ancestorBlocks={[...ancestorBlocks, block]}
{...contextProps}
block={firstNode}
/>
) : null}
<Blocks
{...contextProps}
ancestorBlocks={[...ancestorBlocks, block]}
@@ -5,7 +5,7 @@ import { Icon } from '@gitbook/icons';
import { StyledLink } from '../primitives';
import type { InlineProps } from './Inline';
import { InlineLinkTooltip } from './InlineLinkTooltip';
import { Inlines } from './Inlines';
import { renderInlines } from './Inlines';
export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
const { inline, document, context, ancestorInlines } = props;
@@ -20,12 +20,12 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
if (!context.contentContext || !resolved) {
return (
<span title="Broken link" className="underline">
<Inlines
context={context}
document={document}
nodes={inline.nodes}
ancestorInlines={[...ancestorInlines, inline]}
/>
{renderInlines({
context,
document,
nodes: inline.nodes,
ancestorInlines: [...ancestorInlines, inline],
})}
</span>
);
}
@@ -43,12 +43,12 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
},
}}
>
<Inlines
context={context}
document={document}
nodes={inline.nodes}
ancestorInlines={[...ancestorInlines, inline]}
/>
{renderInlines({
context,
document,
nodes: inline.nodes,
ancestorInlines: [...ancestorInlines, inline],
})}
{isExternal ? (
<Icon
icon="arrow-up-right"
@@ -4,7 +4,7 @@ import type { DocumentContextProps } from './DocumentView';
import { Inline } from './Inline';
import { Text } from './Text';
export function Inlines<T extends DocumentInline | DocumentText>(
export function renderInlines<T extends DocumentInline | DocumentText>(
props: DocumentContextProps & {
/**
* Document being rendered.
@@ -12,7 +12,11 @@ import { tcls } from '@/lib/tailwind';
import type { BlockProps } from './Block';
import { Blocks } from './Blocks';
import { getBlockTextStyle } from './spacing';
const UNORDERED_STYLE = {
'--font-family': 'inherit',
fontSize: 'min(1em, 24px)',
};
export function ListItem(props: BlockProps<DocumentBlockListItem>) {
const { block, ancestorBlocks, ...contextProps } = props;
@@ -25,31 +29,15 @@ export function ListItem(props: BlockProps<DocumentBlockListItem>) {
'Invalid parent list type'
);
const blocksElement = (
<Blocks
{...contextProps}
nodes={block.nodes}
ancestorBlocks={[...ancestorBlocks, block]}
blockStyle={tcls(
'min-h-[1lh]',
// flip heading hash icon if list item is a heading
'flip-heading-hash',
// remove margin-top for the first heading in a list
'[&:is(h2)>div]:mt-0',
'[&:is(h3)>div]:mt-0',
'[&:is(h4)>div]:mt-0',
// Override the "mx-auto" class from UnwrappedBlocks
'mx-0'
)}
style="flex min-w-0 flex-1 flex-col space-y-2"
/>
);
switch (parent.type) {
case 'list-tasks':
return (
<ListItemLI block={block}>
<ListItemPrefix block={block}>
<li className={tcls('flex items-start')}>
<div
className={tcls(
'mr-1 flex min-h-[1lh] min-w-6 items-center justify-center text-tint'
)}
>
<Checkbox
id={block.key!}
disabled
@@ -57,45 +45,117 @@ export function ListItem(props: BlockProps<DocumentBlockListItem>) {
className="relative"
size="small"
/>
</ListItemPrefix>
</div>
<label htmlFor={block.key} className={tcls('flex-1')}>
{blocksElement}
<Blocks
{...contextProps}
nodes={block.nodes}
ancestorBlocks={[...ancestorBlocks, block]}
blockStyle={tcls(
'min-h-[1lh]',
// flip heading hash icon if list item is a heading
'flip-heading-hash',
// remove margin-top for the first heading in a list
'[&:is(h2)>div]:mt-0',
'[&:is(h3)>div]:mt-0',
'[&:is(h4)>div]:mt-0',
// Override the "mx-auto" class from UnwrappedBlocks
'mx-0'
)}
style="flex min-w-0 flex-1 flex-col space-y-2"
/>
</label>
</ListItemLI>
</li>
);
case 'list-ordered':
return (
<ListItemLI block={block}>
<ListItemPrefix block={block}>
<PseudoBefore
content={getOrderedListItemPrefixContent({
depth: getListItemDepth({ ancestorBlocks, type: parent.type }),
block,
parent,
})}
style={{
fontSize: 'min(1em, 24px)',
}}
<li className={tcls('flex items-start')}>
<div
className={tcls(
'mr-1 flex min-h-[1lh] min-w-6 items-center justify-center text-tint'
)}
>
<div
className="before:font-var before:content-[--pseudoBefore--content]"
style={
{
'--pseudoBefore--content': `'${getOrderedListItemPrefixContent({
depth: getListItemDepth({
ancestorBlocks,
type: parent.type,
}),
block,
parent,
})}'`,
'--font-family': 'inherit',
fontSize: 'min(1em, 24px)',
} as React.CSSProperties
}
/>
</ListItemPrefix>
{blocksElement}
</ListItemLI>
</div>
<Blocks
{...contextProps}
nodes={block.nodes}
ancestorBlocks={[...ancestorBlocks, block]}
blockStyle={tcls(
'min-h-[1lh]',
// flip heading hash icon if list item is a heading
'flip-heading-hash',
// remove margin-top for the first heading in a list
'[&:is(h2)>div]:mt-0',
'[&:is(h3)>div]:mt-0',
'[&:is(h4)>div]:mt-0',
// Override the "mx-auto" class from UnwrappedBlocks
'mx-0'
)}
style="flex min-w-0 flex-1 flex-col space-y-2"
/>
</li>
);
case 'list-unordered':
return (
<ListItemLI block={block}>
<ListItemPrefix block={block}>
<PseudoBefore
content={getUnorderedListItemsPrefixContent({
depth: getListItemDepth({ ancestorBlocks, type: parent.type }),
})}
fontFamily="Arial"
style={{ fontSize: 'min(1.5em, 24px)', lineHeight: 1 }}
<li className={tcls('flex items-start')}>
<div
className={tcls(
'mr-1 flex min-h-[1lh] min-w-6 items-center justify-center text-tint'
)}
>
<div
className="before:font-var before:content-[--pseudoBefore--content]"
style={
{
'--pseudoBefore--content': `'${getUnorderedListItemsPrefixContent(
{
depth: getListItemDepth({
ancestorBlocks,
type: parent.type,
}),
}
)})}'`,
...UNORDERED_STYLE,
} as React.CSSProperties
}
/>
</ListItemPrefix>
{blocksElement}
</ListItemLI>
</div>
<Blocks
{...contextProps}
nodes={block.nodes}
ancestorBlocks={[...ancestorBlocks, block]}
blockStyle={tcls(
'min-h-[1lh]',
// flip heading hash icon if list item is a heading
'flip-heading-hash',
// remove margin-top for the first heading in a list
'[&:is(h2)>div]:mt-0',
'[&:is(h3)>div]:mt-0',
'[&:is(h4)>div]:mt-0',
// Override the "mx-auto" class from UnwrappedBlocks
'mx-0'
)}
style="flex min-w-0 flex-1 flex-col space-y-2"
/>
</li>
);
default:
assertNever(parent);
@@ -125,26 +185,6 @@ function getListItemDepth(input: {
return depth;
}
function ListItemLI(props: { block: DocumentBlockListItem; children: React.ReactNode }) {
const textStyle = getBlockTextStyle(props.block);
return <li className={tcls(textStyle.lineHeight, 'flex items-start')}>{props.children}</li>;
}
function ListItemPrefix(props: { block: DocumentBlockListItem; children: React.ReactNode }) {
const textStyle = getBlockTextStyle(props.block);
return (
<div
className={tcls(
textStyle.textSize,
textStyle.lineHeight,
'mr-1 flex min-h-[1lh] min-w-6 items-center justify-center text-tint'
)}
>
{props.children}
</div>
);
}
function getUnorderedListItemsPrefixContent(input: { depth: number }): string {
switch (input.depth % 3) {
case 0:
@@ -3,14 +3,18 @@ import type { DocumentBlockParagraph } from '@gitbook/api';
import { tcls } from '@/lib/tailwind';
import type { BlockProps } from './Block';
import { Inlines } from './Inlines';
import { renderInlines } from './Inlines';
export function Paragraph(props: BlockProps<DocumentBlockParagraph>) {
const { block, style, ...contextProps } = props;
return (
<p className={tcls(style)}>
<Inlines {...contextProps} nodes={block.nodes} ancestorInlines={[]} />
{renderInlines({
...contextProps,
nodes: block.nodes,
ancestorInlines: [],
})}
</p>
);
}
@@ -5,7 +5,7 @@ import { resolveContentRef } from '@/lib/references';
import type { GitBookSpaceContext } from '@v2/lib/context';
import { getDataOrNull } from '@v2/lib/data';
import type { BlockProps } from './Block';
import { UnwrappedBlocks } from './Blocks';
import { renderUnwrappedBlocks } from './Blocks';
export async function ReusableContent(props: BlockProps<DocumentBlockReusableContent>) {
const { block, context, ancestorBlocks } = props;
@@ -62,15 +62,13 @@ export async function ReusableContent(props: BlockProps<DocumentBlockReusableCon
shareKey: undefined,
};
return (
<UnwrappedBlocks
nodes={document.nodes}
document={document}
ancestorBlocks={[...ancestorBlocks, block]}
context={{
...context,
contentContext: reusableContentContext,
}}
/>
);
return renderUnwrappedBlocks({
nodes: document.nodes,
document: document,
ancestorBlocks: [...ancestorBlocks, block],
context: {
...context,
contentContext: reusableContentContext,
},
});
}
@@ -1,3 +1,8 @@
import { CookiesToast } from '@/components/Cookies';
import { LoadIntegrations } from '@/components/Integrations';
import { SpaceLayout } from '@/components/SpaceLayout';
import { buildVersion } from '@/lib/build';
import { isSiteIndexable } from '@/lib/seo';
import { CustomizationThemeMode } from '@gitbook/api';
import type { GitBookSiteContext } from '@v2/lib/context';
import type { Metadata, Viewport } from 'next';
@@ -5,17 +10,9 @@ import { NuqsAdapter } from 'nuqs/adapters/next/app';
import React from 'react';
import * as ReactDOM from 'react-dom';
import { AdminToolbar } from '@/components/AdminToolbar';
import { CookiesToast } from '@/components/Cookies';
import { LoadIntegrations } from '@/components/Integrations';
import { SpaceLayout } from '@/components/SpaceLayout';
import { buildVersion } from '@/lib/build';
import { isSiteIndexable } from '@/lib/seo';
import type { VisitorAuthClaims } from '@/lib/adaptive';
import { GITBOOK_API_PUBLIC_URL, GITBOOK_ASSETS_URL, GITBOOK_ICONS_URL } from '@v2/lib/env';
import { getResizedImageURL } from '@v2/lib/images';
import { ClientContexts } from './ClientContexts';
import { RocketLoaderDetector } from './RocketLoaderDetector';
/**
@@ -48,40 +45,30 @@ export async function SiteLayout(props: {
return (
<NuqsAdapter>
<ClientContexts
nonce={nonce}
forcedTheme={
forcedTheme ??
(customization.themes.toggeable ? undefined : customization.themes.default)
}
<SpaceLayout
context={context}
withTracking={withTracking}
visitorAuthClaims={visitorAuthClaims}
>
<SpaceLayout
context={context}
withTracking={withTracking}
visitorAuthClaims={visitorAuthClaims}
>
{children}
</SpaceLayout>
{children}
</SpaceLayout>
{scripts.length > 0 ? (
<>
<LoadIntegrations />
{scripts.map(({ script }) => (
<script key={script} async src={script} nonce={nonce} />
))}
</>
) : null}
{scripts.length > 0 ? (
<>
<LoadIntegrations />
{scripts.map(({ script }) => (
<script key={script} async src={script} nonce={nonce} />
))}
</>
) : null}
{scripts.some((script) => script.cookies) || customization.privacyPolicy.url ? (
<React.Suspense fallback={null}>
<CookiesToast privacyPolicy={customization.privacyPolicy.url} />
</React.Suspense>
) : null}
{scripts.some((script) => script.cookies) || customization.privacyPolicy.url ? (
<React.Suspense fallback={null}>
<CookiesToast privacyPolicy={customization.privacyPolicy.url} />
</React.Suspense>
) : null}
<RocketLoaderDetector nonce={nonce} />
<AdminToolbar context={context} />
</ClientContexts>
<RocketLoaderDetector nonce={nonce} />
</NuqsAdapter>
);
}
@@ -11,7 +11,6 @@ import { getPagePath } from '@/lib/pages';
import { isPageIndexable, isSiteIndexable } from '@/lib/seo';
import { getResizedImageURL } from '@v2/lib/images';
import { PageContextProvider } from '../PageContext';
import { PageClientLayout } from './PageClientLayout';
import { type PagePathParams, fetchPageData, getPathnameParam } from './fetch';
@@ -65,7 +64,7 @@ export async function SitePage(props: SitePageProps) {
const document = await getPageDocument(context, page);
return (
<PageContextProvider pageId={page.id} spaceId={context.space.id} title={page.title}>
<>
{withFullPageCover && page.cover ? (
<PageCover as="full" page={page} cover={page.cover} context={context} />
) : null}
@@ -90,7 +89,7 @@ export async function SitePage(props: SitePageProps) {
<React.Suspense fallback={null}>
<PageClientLayout withSections={withSections} />
</React.Suspense>
</PageContextProvider>
</>
);
}