Load the expression runtime only for content that has expressions (RND-12524)

Every code block rendered client-side called useEvaluateInlineExpression, so
@gitbook/expr — and with it acorn, acorn-loose, acorn-walk, escodegen, esutils,
estraverse, source-map and eval-estree-expression — sat in the eager entry of
every docs page. That is 254 KB of JavaScript parser and code generator, more
than React itself, shipped to readers of static prose.

The server already computes hasInlineExpression, so let it pick the boundary:
blocks without expressions render a ClientCodeBlock that no longer imports the
runtime, and the ones that need it go through a next/dynamic wrapper. Same for
inline expressions. ssr: true throughout, so evaluated values stay in the HTML.

First-party client JS on a docs page: 1814 KB -> 1562 KB decoded (-252 KB).
This commit is contained in:
Nolann Biron
2026-08-19 20:34:49 +02:00
parent 56c25587db
commit fe471dcbca
9 changed files with 78 additions and 14 deletions
@@ -6,11 +6,10 @@ import { useDebounceCallback } from 'usehooks-ts';
import type { CustomizationThemedCodeTheme, DocumentBlockCode } from '@gitbook/api';
import type { BlockProps } from '../Block';
import { type InlineExpressionVariables, useEvaluateInlineExpression } from '../InlineExpression';
import type { InlineExpressionVariables } from '../InlineExpression/types';
import { CodeBlockRenderer } from './CodeBlockRenderer';
import type { HighlightTheme, RenderedInline } from './highlight-tokens';
import { plainHighlight } from './plain-highlight';
import { useAdaptiveVisitor } from '@/components/Adaptive';
import { useInViewportListener } from '@/components/hooks/useInViewportListener';
import { useScrollListener } from '@/components/hooks/useScrollListener';
import { Button, ToggleChevron } from '@/components/primitives';
@@ -20,6 +19,9 @@ import { type ClassValue, tcls } from '@/lib/tailwind';
export type ClientBlockProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'style'> & {
inlines: RenderedInline[];
inlineExprVariables: InlineExpressionVariables;
/** Only supplied for blocks that contain one: evaluating needs `@gitbook/expr`, which is a
* quarter of a megabyte of JS parser we keep out of the bundle otherwise. */
evaluateInlineExpression?: (expression: string) => string;
mode: BlockProps<DocumentBlockCode>['context']['mode'];
themes?: CustomizationThemedCodeTheme;
embedded?: boolean;
@@ -32,17 +34,11 @@ export const CODE_BLOCK_DEFAULT_COLLAPSED_LINE_COUNT = 10;
* It allows us to defer some load to avoid blocking the rendering of the whole page with block highlighting.
*/
export function ClientCodeBlock(props: ClientBlockProps) {
const { block, mode, style, inlines, inlineExprVariables, themes, embedded } = props;
const { block, mode, style, inlines, evaluateInlineExpression, themes, embedded } = props;
const blockRef = useRef<HTMLDivElement>(null);
const isInViewportRef = useRef(false);
const [isInViewport, setIsInViewport] = useState(false);
const getAdaptiveVisitorClaims = useAdaptiveVisitor();
const visitorClaims = getAdaptiveVisitorClaims();
const evaluateInlineExpression = useEvaluateInlineExpression({
visitorClaims,
variables: inlineExprVariables,
});
const plainTheme = useMemo(
() => plainHighlight(block, inlines, { evaluateInlineExpression, themes }),
[block, inlines, evaluateInlineExpression, themes]
@@ -0,0 +1,19 @@
'use client';
import { ClientCodeBlock, type ClientBlockProps } from './ClientCodeBlock';
import { useAdaptiveVisitor } from '@/components/Adaptive';
import { useEvaluateInlineExpression } from '@/components/DocumentView/InlineExpression/useEvaluateInlineExpression';
/**
* Code block whose content contains inline expressions, so it needs the expression runtime.
* Kept in its own module: importing it pulls `@gitbook/expr` and its JS parser.
*/
export function ClientCodeBlockWithExpressions(props: ClientBlockProps) {
const getAdaptiveVisitorClaims = useAdaptiveVisitor();
const evaluateInlineExpression = useEvaluateInlineExpression({
visitorClaims: getAdaptiveVisitorClaims(),
variables: props.inlineExprVariables,
});
return <ClientCodeBlock {...props} evaluateInlineExpression={evaluateInlineExpression} />;
}
@@ -0,0 +1,19 @@
'use client';
import dynamic from 'next/dynamic';
import type { ClientBlockProps } from './ClientCodeBlock';
// `ssr: true` keeps the evaluated code in the server-rendered HTML; the point of the boundary is to
// move `@gitbook/expr` (~254 KB with its parser) out of the route's eager entry.
const ClientCodeBlockWithExpressions = dynamic(
() =>
import('./ClientCodeBlockWithExpressions').then(
(mod) => mod.ClientCodeBlockWithExpressions
),
{ ssr: true }
);
export function ClientCodeBlockWithExpressionsLazy(props: ClientBlockProps) {
return <ClientCodeBlockWithExpressions {...props} />;
}
@@ -9,6 +9,7 @@ import type {
import type { BlockProps } from '../Block';
import { Blocks } from '../Blocks';
import { ClientCodeBlock } from './ClientCodeBlock';
import { ClientCodeBlockWithExpressionsLazy } from './ClientCodeBlockWithExpressionsLazy';
import { CodeBlockRenderer } from './CodeBlockRenderer';
import { highlight } from './highlight';
import { type RenderedInline, getInlines } from './highlight-tokens';
@@ -121,8 +122,11 @@ export async function CodeBlock(
{isMermaid ? (
<MermaidCodeBlockLazy
{...clientProps}
hasInlineExpression={hasInlineExpression}
mermaidRuntimeURL={getAssetURL(MERMAID_RUNTIME_PATH)}
/>
) : hasInlineExpression ? (
<ClientCodeBlockWithExpressionsLazy {...clientProps} />
) : (
<ClientCodeBlock {...clientProps} />
)}
@@ -8,6 +8,7 @@ import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useSta
import { createPortal } from 'react-dom';
import { type ClientBlockProps, ClientCodeBlock } from './ClientCodeBlock';
import { ClientCodeBlockWithExpressions } from './ClientCodeBlockWithExpressions';
import { getPlainCodeBlock } from './highlight-tokens';
import { MermaidPanZoomControls } from './MermaidPanZoomControls';
import { useHasBeenInViewport } from '@/components/hooks/useHasBeenInViewport';
@@ -20,6 +21,7 @@ import { tcls } from '@/lib/tailwind';
export function MermaidCodeBlock(
props: ClientBlockProps & {
mermaidRuntimeURL: string;
hasInlineExpression: boolean;
}
) {
const { block, mode, style, mermaidRuntimeURL } = props;
@@ -169,7 +171,11 @@ export function MermaidCodeBlock(
}, []);
if (error) {
return <ClientCodeBlock {...props} />;
return props.hasInlineExpression ? (
<ClientCodeBlockWithExpressions {...props} />
) : (
<ClientCodeBlock {...props} />
);
}
// The live diagram subtree. It is portaled into a stable host that moves between the
@@ -11,6 +11,8 @@ const MermaidCodeBlock = dynamic(
{ ssr: true }
);
export function MermaidCodeBlockLazy(props: ClientBlockProps & { mermaidRuntimeURL: string }) {
export function MermaidCodeBlockLazy(
props: ClientBlockProps & { mermaidRuntimeURL: string; hasInlineExpression: boolean }
) {
return <MermaidCodeBlock {...props} />;
}
@@ -3,7 +3,7 @@ import * as React from 'react';
import type { DocumentInlineExpression } from '@gitbook/api';
import type { InlineProps } from '../Inline';
import { InlineExpressionValue } from './InlineExpressionValue';
import { InlineExpressionValueLazy } from './InlineExpressionValueLazy';
/**
* Render an inline expression.
@@ -25,7 +25,7 @@ export function InlineExpression(props: InlineProps<DocumentInlineExpression>) {
return (
<React.Suspense fallback={null}>
<InlineExpressionValue expression={data.expression} variables={variables} />
<InlineExpressionValueLazy expression={data.expression} variables={variables} />
</React.Suspense>
);
}
@@ -0,0 +1,19 @@
'use client';
import dynamic from 'next/dynamic';
import type { InlineExpressionVariables } from './types';
// `ssr: true` keeps the evaluated value in the server-rendered HTML; the boundary exists to move
// `@gitbook/expr` and its JS parser out of the route's eager entry.
const InlineExpressionValue = dynamic(
() => import('./InlineExpressionValue').then((mod) => mod.InlineExpressionValue),
{ ssr: true }
);
export function InlineExpressionValueLazy(props: {
expression: string;
variables: InlineExpressionVariables;
}) {
return <InlineExpressionValue {...props} />;
}
@@ -1,3 +1,2 @@
export * from './InlineExpression';
export * from './types';
export * from './useEvaluateInlineExpression';