From fe471dcbca82e9d4ec5575dfc1c972032c98a68c Mon Sep 17 00:00:00 2001 From: Nolann Biron Date: Wed, 19 Aug 2026 20:34:49 +0200 Subject: [PATCH] Load the expression runtime only for content that has expressions (RND-12524) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .../CodeBlock/ClientCodeBlock.tsx | 14 +++++--------- .../ClientCodeBlockWithExpressions.tsx | 19 +++++++++++++++++++ .../ClientCodeBlockWithExpressionsLazy.tsx | 19 +++++++++++++++++++ .../DocumentView/CodeBlock/CodeBlock.tsx | 4 ++++ .../CodeBlock/MermaidCodeBlock.tsx | 8 +++++++- .../CodeBlock/MermaidCodeBlockLazy.tsx | 4 +++- .../InlineExpression/InlineExpression.tsx | 4 ++-- .../InlineExpressionValueLazy.tsx | 19 +++++++++++++++++++ .../DocumentView/InlineExpression/index.ts | 1 - 9 files changed, 78 insertions(+), 14 deletions(-) create mode 100644 packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlockWithExpressions.tsx create mode 100644 packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlockWithExpressionsLazy.tsx create mode 100644 packages/gitbook/src/components/DocumentView/InlineExpression/InlineExpressionValueLazy.tsx diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlock.tsx b/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlock.tsx index 30315de29..fb3e40ae6 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlock.tsx +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlock.tsx @@ -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, '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['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(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] diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlockWithExpressions.tsx b/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlockWithExpressions.tsx new file mode 100644 index 000000000..33d4f8290 --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlockWithExpressions.tsx @@ -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 ; +} diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlockWithExpressionsLazy.tsx b/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlockWithExpressionsLazy.tsx new file mode 100644 index 000000000..42d564559 --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlockWithExpressionsLazy.tsx @@ -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 ; +} diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlock.tsx b/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlock.tsx index 0bae7b222..fe9303a44 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlock.tsx +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlock.tsx @@ -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 ? ( + ) : hasInlineExpression ? ( + ) : ( )} diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/MermaidCodeBlock.tsx b/packages/gitbook/src/components/DocumentView/CodeBlock/MermaidCodeBlock.tsx index 902370d52..b74d87ca4 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/MermaidCodeBlock.tsx +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/MermaidCodeBlock.tsx @@ -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 ; + return props.hasInlineExpression ? ( + + ) : ( + + ); } // The live diagram subtree. It is portaled into a stable host that moves between the diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/MermaidCodeBlockLazy.tsx b/packages/gitbook/src/components/DocumentView/CodeBlock/MermaidCodeBlockLazy.tsx index 26a9af3b0..d8bdfdc2d 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/MermaidCodeBlockLazy.tsx +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/MermaidCodeBlockLazy.tsx @@ -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 ; } diff --git a/packages/gitbook/src/components/DocumentView/InlineExpression/InlineExpression.tsx b/packages/gitbook/src/components/DocumentView/InlineExpression/InlineExpression.tsx index b548ff063..9a756d8cf 100644 --- a/packages/gitbook/src/components/DocumentView/InlineExpression/InlineExpression.tsx +++ b/packages/gitbook/src/components/DocumentView/InlineExpression/InlineExpression.tsx @@ -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) { return ( - + ); } diff --git a/packages/gitbook/src/components/DocumentView/InlineExpression/InlineExpressionValueLazy.tsx b/packages/gitbook/src/components/DocumentView/InlineExpression/InlineExpressionValueLazy.tsx new file mode 100644 index 000000000..dbcb5f424 --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/InlineExpression/InlineExpressionValueLazy.tsx @@ -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 ; +} diff --git a/packages/gitbook/src/components/DocumentView/InlineExpression/index.ts b/packages/gitbook/src/components/DocumentView/InlineExpression/index.ts index 59dec96a7..79ce6d991 100644 --- a/packages/gitbook/src/components/DocumentView/InlineExpression/index.ts +++ b/packages/gitbook/src/components/DocumentView/InlineExpression/index.ts @@ -1,3 +1,2 @@ export * from './InlineExpression'; export * from './types'; -export * from './useEvaluateInlineExpression';