Skip to content

Commit cf043d6

Browse files
ntuckercursoragent
andauthored
perf(website): Defer Demo playground hydration without SSG code loss (#4030)
* perf(website): Mount only the selected Demo playground Avoid hydrating Monaco and DataProvider for every protocol tab on the homepage. Align CodeProvider tab sync with Docusaurus and fix useTabStorage double-prefixing localStorage keys. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(website): Keep Demo protocols mounted for SSG, defer hydration Restore mounting all protocol sandboxes so open-file source stays in static HTML for crawlers. Skip LivePreview while hidden and latch Monaco hydration on first show so inactive protocols stay cheap without losing editor state after a visit. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent f1c0831 commit cf043d6

7 files changed

Lines changed: 81 additions & 61 deletions

File tree

website/src/components/Demo/CodeEditor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ const DemoPlayground = memo(
4848
title={capitalizeFirstLetter(path)}
4949
path={`${value}/${path}.tsx`}
5050
collapsed={!open}
51-
autoFocus={autoFocus && Object.values(code).length === i + 1}
51+
autoFocus={autoFocus && code.length === i + 1}
5252
>
5353
{instanceCode}
5454
</PlaygroundCode>

website/src/components/Demo/CodeProvider.tsx

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useScrollPositionBlocker } from '@docusaurus/theme-common/internal';
2+
import useIsomorphicLayoutEffect from '@docusaurus/useIsomorphicLayoutEffect';
23
import React, { memo, useState } from 'react';
34

45
import CodeTabContext from './CodeTabContext';
@@ -27,39 +28,32 @@ export function CodeProvider<V extends CodeTabValue[]>({
2728
playgroundRef,
2829
children,
2930
}: Props<V>) {
30-
const [choice, setTabGroupChoice] = useTabStorage(
31-
`docusaurus.tab.${groupId}`,
32-
);
33-
const [selectedValue, setLocalSelectedValue] = useState(defaultValue);
31+
const [choice, setTabGroupChoice] = useTabStorage(groupId);
32+
const [selectedValue, setSelectedValue] = useState(defaultValue);
3433
const { blockElementScrollPositionUntilNextRender } =
3534
useScrollPositionBlocker();
3635

37-
if (
38-
choice != null &&
39-
choice !== selectedValue &&
40-
isTabValue(values, choice)
41-
) {
42-
setLocalSelectedValue(choice);
43-
}
44-
45-
const setSelectedValue = (newTabValue: string) => {
46-
if (newTabValue !== selectedValue) {
47-
const el = playgroundRef.current;
48-
if (el) {
49-
blockElementScrollPositionUntilNextRender(el);
50-
}
51-
setLocalSelectedValue(newTabValue as V[number]['value']);
52-
53-
if (groupId != null) {
54-
setTabGroupChoice(newTabValue);
55-
}
36+
// Local state stays interactive if storage no-ops; sync like Docusaurus tabs.
37+
useIsomorphicLayoutEffect(() => {
38+
if (choice != null && isTabValue(values, choice)) {
39+
setSelectedValue(choice);
5640
}
57-
};
41+
}, [choice, values]);
5842

5943
const value = {
6044
selectedValue,
6145
values,
62-
setSelectedValue,
46+
setSelectedValue: (newTabValue: string) => {
47+
if (!isTabValue(values, newTabValue) || newTabValue === selectedValue) {
48+
return;
49+
}
50+
const el = playgroundRef.current;
51+
if (el) {
52+
blockElementScrollPositionUntilNextRender(el);
53+
}
54+
setSelectedValue(newTabValue);
55+
setTabGroupChoice(newTabValue);
56+
},
6357
};
6458
return (
6559
<CodeTabContext.Provider value={value}>{children}</CodeTabContext.Provider>

website/src/components/HooksPlayground.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,14 @@ const HooksPlayground = ({
3535
);
3636
export default memo(HooksPlayground);
3737

38-
//child.props.children.props.title
39-
4038
interface PlaygroundProps<T = any> {
4139
groupId: string;
4240
defaultOpen?: 'y' | 'n';
4341
row: boolean;
44-
hidden: boolean;
42+
hidden?: boolean;
4543
fixtures?: (Fixture | Interceptor<T>)[];
4644
getInitialInterceptorData?: () => T;
4745
children: React.ReactNode;
48-
reverse?: boolean;
4946
defaultTab?: string;
5047
headerControls?: React.ReactNode;
5148
}

website/src/components/Playground/Preview.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,8 @@ function Preview<T>({
2222
fixtures,
2323
getInitialInterceptorData,
2424
}: PreviewProps<T>) {
25-
const [choice, setTabGroupChoice] = useTabStorage(
26-
`docusaurus.tab.${groupId}`,
27-
);
28-
const selectedValue = (choice ?? defaultOpen) as 'y' | 'n';
25+
const [choice, setTabGroupChoice] = useTabStorage(groupId);
26+
const selectedValue = choice === 'y' || choice === 'n' ? choice : defaultOpen;
2927
const { blockElementScrollPositionUntilNextRender } =
3028
useScrollPositionBlocker();
3129

website/src/components/Playground/editor/EditorSurface.tsx

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import React, {
1111
import { LiveEditor } from 'react-live';
1212

1313
import Header from '../Header';
14-
import { isGoogleBot } from '../isMobileOrBot';
1514
import Editor from '../PlaygroundEditor';
1615
import styles from '../styles.module.css';
1716
import TabList from '../TabList';
@@ -20,6 +19,8 @@ import type { CodeDocument, CodeModel } from './codeModel';
2019
export interface EditorSurfaceProps extends CodeModel {
2120
layout: 'row' | 'stacked';
2221
variant: 'playground' | 'standalone';
22+
/** When false, keep static SSR-friendly editors (no Monaco). Defaults true. */
23+
interactive?: boolean;
2324
fixtureContent?: React.ReactNode;
2425
headerControls?: React.ReactNode;
2526
}
@@ -29,6 +30,7 @@ export default function EditorSurface({
2930
update,
3031
layout,
3132
variant,
33+
interactive = true,
3234
fixtureContent,
3335
headerControls,
3436
}: EditorSurfaceProps) {
@@ -103,6 +105,7 @@ export default function EditorSurface({
103105
: null}
104106
<TextEditTab
105107
hidden={closedList[index]}
108+
interactive={interactive}
106109
tabIndex={index}
107110
onFocus={
108111
row && !document.col && documents.length > 1 ?
@@ -125,18 +128,33 @@ export default function EditorSurface({
125128

126129
function TextEditTab({
127130
hidden,
131+
interactive,
128132
code,
129133
language,
130134
tabIndex,
131135
...rest
132-
}: ComponentProps<typeof Editor> & { hidden: boolean }) {
133-
const fallback =
134-
hidden ? <></>
135-
: isGoogleBot ?
136-
<pre className="prism-code language-tsx" spellCheck="false">
137-
{code}
138-
</pre>
139-
: <LiveEditor language={language} code={code} disabled />;
136+
}: ComponentProps<typeof Editor> & {
137+
hidden: boolean;
138+
interactive: boolean;
139+
}) {
140+
// Stable across SSR/hydration (do not branch on navigator / isGoogleBot here).
141+
const staticView = <LiveEditor language={language} code={code} disabled />;
142+
143+
// Deferred protocols: keep open-file source in the DOM for SSG/crawlers;
144+
// skip Monaco until the sandbox has been shown once.
145+
if (!interactive) {
146+
return (
147+
<div
148+
className={clsx(styles.playgroundEditor, {
149+
[styles.hidden]: hidden,
150+
})}
151+
>
152+
{hidden ? null : staticView}
153+
</div>
154+
);
155+
}
156+
157+
const fallback = hidden ? <></> : staticView;
140158

141159
return (
142160
<div

website/src/components/Playground/index.tsx

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
2+
import useIsomorphicLayoutEffect from '@docusaurus/useIsomorphicLayoutEffect';
23
import clsx from 'clsx';
3-
import React, { lazy, useDeferredValue } from 'react';
4+
import React, { lazy, useDeferredValue, useState } from 'react';
45

56
import Boundary from './Boundary';
67
import { useCodeDocuments } from './editor/codeModel';
@@ -55,6 +56,7 @@ export default function Playground<T>({
5556
<PlaygroundContent
5657
reverse={playgroundPosition === 'top'}
5758
row={row}
59+
hidden={hidden}
5860
fixtures={fixtures}
5961
groupId={groupId}
6062
defaultOpen={defaultOpen}
@@ -73,6 +75,7 @@ function PlaygroundContent<T>({
7375
reverse,
7476
children,
7577
row,
78+
hidden,
7679
fixtures,
7780
groupId,
7881
defaultOpen,
@@ -85,10 +88,18 @@ function PlaygroundContent<T>({
8588
const code = useDeferredValue(
8689
model.documents.map(document => document.value).join('\n'),
8790
);
91+
92+
// Hydrate Monaco on first show and keep it (preserves undo / go-to-def).
93+
const [editorInteractive, setEditorInteractive] = useState(!hidden);
94+
useIsomorphicLayoutEffect(() => {
95+
if (!hidden) setEditorInteractive(true);
96+
}, [hidden]);
97+
8898
const editor = (
8999
<EditorShell key="editor">
90100
<EditorSurface
91101
{...model}
102+
interactive={editorInteractive}
92103
layout={row ? 'row' : 'stacked'}
93104
variant="playground"
94105
fixtureContent={
@@ -98,25 +109,28 @@ function PlaygroundContent<T>({
98109
/>
99110
</EditorShell>
100111
);
101-
const preview = (
102-
<Boundary key="preview" fallback={previewLoading}>
103-
<PreviewWithScopeLazy
104-
code={code}
105-
groupId={groupId}
106-
defaultOpen={defaultOpen}
107-
row={row}
108-
fixtures={fixtures}
109-
getInitialInterceptorData={getInitialInterceptorData}
110-
/>
111-
</Boundary>
112-
);
112+
// Live preview only while visible — unmounts when hidden (resets store).
113+
const preview =
114+
hidden ? previewLoading : (
115+
<Boundary key="preview" fallback={previewLoading}>
116+
<PreviewWithScopeLazy
117+
code={code}
118+
groupId={groupId}
119+
defaultOpen={defaultOpen}
120+
row={row}
121+
fixtures={fixtures}
122+
getInitialInterceptorData={getInitialInterceptorData}
123+
/>
124+
</Boundary>
125+
);
113126

114127
return <>{reverse ? [preview, editor] : [editor, preview]}</>;
115128
}
116129

117130
interface ContentProps<T> extends PreviewProps<T> {
118131
children: PlaygroundProps<T>['children'];
119132
reverse: boolean;
133+
hidden: boolean;
120134
defaultTab?: string;
121135
headerControls?: React.ReactNode;
122136
}

website/src/utils/tabStorage.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { useStorageSlot } from '@docusaurus/theme-common';
22
import { useCallback } from 'react';
33

4-
export function useTabStorage(groupId: string) {
5-
const key = getStorageKey(groupId);
4+
/** Persist a tab choice under `docusaurus.tab.${groupId}`. Pass the raw group id. */
5+
export function useTabStorage(groupId: string | null | undefined) {
6+
const key = groupId ? `docusaurus.tab.${groupId}` : null;
67
const [value, storageSlot] = useStorageSlot(key);
78

89
const setValue = useCallback(
910
(newValue: string) => {
1011
if (!key) {
11-
return; // no-op
12+
return;
1213
}
1314
storageSlot.set(newValue);
1415
},
@@ -17,5 +18,3 @@ export function useTabStorage(groupId: string) {
1718

1819
return [value, setValue] as const;
1920
}
20-
21-
const getStorageKey = (groupId: string) => `docusaurus.tab.${groupId}`;

0 commit comments

Comments
 (0)