Skip to content

Addon-Docs: Avoid rerendering static Source blocks#34206

Merged
Sidnioulz merged 11 commits into
storybookjs:nextfrom
anchmelev:fix/23776-source-static-rerenders
Apr 13, 2026
Merged

Addon-Docs: Avoid rerendering static Source blocks#34206
Sidnioulz merged 11 commits into
storybookjs:nextfrom
anchmelev:fix/23776-source-static-rerenders

Conversation

@anchmelev
Copy link
Copy Markdown
Contributor

@anchmelev anchmelev commented Mar 19, 2026

Closes #23776

What I did

Static <Source code="..."> blocks were subscribing to SourceContext updates even though they do not depend on live story snippets. On docs pages with many Source blocks, each snippet update could trigger a wave of unnecessary rerenders and repeated syntax-highlighting work.

This change:

  • splits the render path so static code sources use an empty source context and no longer rerender on unrelated snippet updates
  • keeps story-derived Source blocks reactive to snippet updates
  • adds a regression unit test covering both behaviors
  • adds a manual benchmark story to make the regression easy to inspect visually
  • replaces the generic Error for of={undefined} with a typed StorybookError

Benchmark

Local benchmark result:

  • Scenario: 75 static <Source code="..."> blocks
  • Trigger: 5 snippet updates
  • Before: 375 extra renders
  • After: 0 extra renders

How to verify

  1. Run cd code && yarn storybook:ui
  2. Open Blocks/Source/ManyStaticCodeBlocksBenchmark
  3. Click Run 5 updates
  4. Verify the benchmark reports 0 extra renders ... for static Source code blocks
  5. Open Blocks/Source/Of and verify attached story source still behaves normally

Summary by CodeRabbit

  • New Features

    • Added an interactive benchmark story and UI to measure render performance of many static source blocks.
  • Bug Fixes

    • Consistent, clearer Storybook-specific error reporting when a block’s of prop is present but explicitly undefined.
  • Refactor

    • Preserves explicitly provided code (including empty strings) and bypasses story-derived sources when code is supplied.
  • Tests

    • Added tests validating Source rerender behavior for static and story-based sources.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 19, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a Profiler-based benchmark Story and tests for Source, treats explicit empty code as provided in useSourceProps, introduces InvalidBlockOfPropError, and updates block components to throw that error when of is present but undefined.

Changes

Cohort / File(s) Summary
Source implementation & error
code/addons/docs/src/blocks/blocks/Source.tsx, code/core/src/preview-errors.ts
Tighten useSourceProps typing and checks so code="" is treated as provided; short-circuit source-context when code is explicit; replace prior thrown generic error with exported InvalidBlockOfPropError.
Blocks throwing standardized error
code/addons/docs/src/blocks/blocks/ArgTypes.tsx, .../Canvas.tsx, .../Description.tsx, .../Story.tsx, .../Subtitle.tsx, .../Title.tsx
Replace inline generic Error throws for cases where of is present but undefined with InvalidBlockOfPropError across multiple block components.
Benchmark story & helpers
code/addons/docs/src/blocks/blocks/Source.stories.tsx
Add ManyStaticCodeBlocksBenchmark story and benchmarking utilities: BenchmarkHarness, StaticSourceList, BenchmarkControls, waitForNextPaint, Profiler-based render counting, and UI/result display; Chromatic snapshot disabled.
Tests: rerender behavior
code/addons/docs/src/blocks/blocks/Source.test.tsx
New Vitest tests (happy-dom) mocking Source to assert: memoized Source with explicit code (including empty string) does not rerender when snippet sources appear; story-attached Source rerenders when snippet-provided sources populate.

Sequence Diagram(s)

sequenceDiagram
    participant BH as BenchmarkHarness
    participant Prof as ReactProfiler
    participant Src as Source
    participant DOM as BrowserPaint

    BH->>Prof: mount StaticSourceList (N blocks)
    Prof->>Src: onRender callback (record render)
    Src-->>Prof: render
    Prof->>DOM: paint

    BH->>BH: start benchmark (5 iterations)
    loop 5x
        BH->>BH: update iteration key (force re-mount)
        BH->>DOM: waitForNextPaint()
        BH->>Prof: remount StaticSourceList (new key)
        Prof->>Src: onRender callback (increment)
        Prof->>DOM: paint
    end

    BH->>BH: compute elapsed time & extra renders
    BH->>DOM: update result UI
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
code/addons/docs/src/blocks/blocks/Source.test.tsx (2)

57-139: Please add one regression for the new InvalidBlockOfPropError.

This suite locks in the rerender split, but the typed error introduced on Line 141 of Source.tsx still has no automated coverage. A small toThrow(InvalidBlockOfPropError) case would keep that contract from regressing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/addons/docs/src/blocks/blocks/Source.test.tsx` around lines 57 - 139,
Add a regression test in Source.test.tsx that asserts the new typed error is
thrown: render a Source block (e.g., StorySource or StaticSource as appropriate)
with an invalid "of" prop value that triggers the InvalidBlockOfPropError from
Source.tsx and wrap the render in an
expect(...).toThrow(InvalidBlockOfPropError). Use the existing test helpers
(createMockDocsContext, render) and the InvalidBlockOfPropError symbol to ensure
the test specifically checks for that error type so the contract cannot regress.

16-25: Please align this module mock with the repo's Vitest spy pattern.

Fully replacing ../components/Source here diverges from the spy: true + vi.mocked() + beforeEach convention and forces this test to hand-maintain the mocked module shape. Spying on the real module would keep SourceError in sync while still letting you capture props.

♻️ Suggested update
 import React, { memo } from 'react';
+import * as SourceComponentModule from '../components/Source';
 
 import type { PreparedStory } from 'storybook/internal/types';
@@
 const pureSourceSpy = vi.fn();
 
-vi.mock('../components/Source', () => ({
-  Source: (props: unknown) => {
-    pureSourceSpy(props);
-    return null;
-  },
-  SourceError: {
-    NO_STORY: 'There’s no story here.',
-    SOURCE_UNAVAILABLE: 'Oh no! The source is not available.',
-  },
-}));
+vi.mock('../components/Source', { spy: true });
@@
 describe('Source', () => {
   beforeEach(() => {
     pureSourceSpy.mockClear();
+    vi.mocked(SourceComponentModule.Source).mockImplementation((props: unknown) => {
+      pureSourceSpy(props);
+      return null;
+    });
   });
As per coding guidelines, "Use `vi.mock()` with the `spy: true` option for all package and file mocks in Vitest tests" and "Implement mock behaviors in `beforeEach` blocks in Vitest tests".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/addons/docs/src/blocks/blocks/Source.test.tsx` around lines 16 - 25,
Replace the full-module replacement of '../components/Source' with a spy-style
mock: call vi.mock('../components/Source', { spy: true }) so the real module is
loaded and SourceError remains intact, then in a beforeEach use vi.mocked(...)
to override only the Source export with a wrapper that calls
pureSourceSpy(props) and returns null; reference the exports Source and
SourceError and use pureSourceSpy and vi.mocked to implement the per-test mock
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@code/addons/docs/src/blocks/blocks/Source.tsx`:
- Around line 190-193: SourceImpl currently branches on truthiness of props.code
which treats an empty string as absent and keeps the component subscribed to
SourceContext; change the conditional to check for explicit undefined (e.g.,
props.code !== undefined) so a provided empty string uses SourceWithCode; mirror
the same explicit undefined checks inside useSourceProps and any guards so they
choose the code path (and avoid SourceContext subscription) whenever props.code
is present, and continue to use SourceWithStorySnippet only when props.code is
strictly undefined.

---

Nitpick comments:
In `@code/addons/docs/src/blocks/blocks/Source.test.tsx`:
- Around line 57-139: Add a regression test in Source.test.tsx that asserts the
new typed error is thrown: render a Source block (e.g., StorySource or
StaticSource as appropriate) with an invalid "of" prop value that triggers the
InvalidBlockOfPropError from Source.tsx and wrap the render in an
expect(...).toThrow(InvalidBlockOfPropError). Use the existing test helpers
(createMockDocsContext, render) and the InvalidBlockOfPropError symbol to ensure
the test specifically checks for that error type so the contract cannot regress.
- Around line 16-25: Replace the full-module replacement of
'../components/Source' with a spy-style mock: call
vi.mock('../components/Source', { spy: true }) so the real module is loaded and
SourceError remains intact, then in a beforeEach use vi.mocked(...) to override
only the Source export with a wrapper that calls pureSourceSpy(props) and
returns null; reference the exports Source and SourceError and use pureSourceSpy
and vi.mocked to implement the per-test mock behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1ef494ab-2921-45b8-8d97-78700c833dc6

📥 Commits

Reviewing files that changed from the base of the PR and between 8e95824 and 621f5ad.

📒 Files selected for processing (4)
  • code/addons/docs/src/blocks/blocks/Source.stories.tsx
  • code/addons/docs/src/blocks/blocks/Source.test.tsx
  • code/addons/docs/src/blocks/blocks/Source.tsx
  • code/core/src/preview-errors.ts

Comment thread code/addons/docs/src/blocks/blocks/Source.tsx
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
code/addons/docs/src/blocks/blocks/Source.stories.tsx (1)

133-139: Minor: Redundant DOM property assignment.

Setting both .value and .textContent on the <output> element is redundant—.textContent will display the value regardless. However, this doesn't affect functionality.

Optional simplification
   const updateCount = useCallback((value: number) => {
     renderCount.current = value;
     if (countRef.current) {
-      countRef.current.value = `${value}`;
       countRef.current.textContent = `${value}`;
     }
   }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/addons/docs/src/blocks/blocks/Source.stories.tsx` around lines 133 -
139, In updateCount (the useCallback that updates renderCount and uses
countRef), remove the redundant DOM assignment to countRef.current.value on the
<output> element and only set countRef.current.textContent = `${value}` (or
alternatively only set .value) to simplify the code; keep renderCount.current
update and the null check for countRef.current intact and update any related
comments to reflect the single property being used.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@code/addons/docs/src/blocks/blocks/Source.stories.tsx`:
- Around line 133-139: In updateCount (the useCallback that updates renderCount
and uses countRef), remove the redundant DOM assignment to
countRef.current.value on the <output> element and only set
countRef.current.textContent = `${value}` (or alternatively only set .value) to
simplify the code; keep renderCount.current update and the null check for
countRef.current intact and update any related comments to reflect the single
property being used.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ce3bad83-ec70-47fd-a4d2-28b3936c4285

📥 Commits

Reviewing files that changed from the base of the PR and between 621f5ad and bdfaeb9.

📒 Files selected for processing (1)
  • code/addons/docs/src/blocks/blocks/Source.stories.tsx

@valentinpalkovic valentinpalkovic moved this to Empathy Queue (prioritized) in Core Team Projects Mar 24, 2026
Copy link
Copy Markdown
Contributor

@JReinhold JReinhold left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! The idea looks good, added a few remarks on the specifics.

Comment thread code/addons/docs/src/blocks/blocks/Source.tsx
Comment thread code/core/src/preview-errors.ts
Comment thread code/addons/docs/src/blocks/blocks/Source.stories.tsx Outdated
Comment thread code/addons/docs/src/blocks/blocks/Source.test.tsx Outdated
@JReinhold JReinhold self-assigned this Mar 25, 2026
@JReinhold JReinhold moved this from Empathy Queue (prioritized) to In Progress in Core Team Projects Mar 25, 2026
@anchmelev anchmelev force-pushed the fix/23776-source-static-rerenders branch from 5c35882 to 0472750 Compare March 26, 2026 01:49
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
code/addons/docs/src/blocks/blocks/Source.stories.tsx (1)

292-296: Consider adding a play function for automated verification.

The !vitest tag disables automated testing, making this purely manual. Since data-testid attributes are already present, a play function could automate the verification:

play: async ({ canvasElement }) => {
  const canvas = within(canvasElement);
  const runButton = await canvas.findByTestId('run-benchmark');
  await userEvent.click(runButton);
  const result = await canvas.findByTestId('benchmark-result');
  await waitFor(() => {
    expect(result.textContent).toMatch(/^0 extra renders/);
  });
},

This would catch future regressions automatically. However, if the intent is to keep this as a manual performance inspection tool only (with functional coverage in Source.test.tsx), the current approach is acceptable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/addons/docs/src/blocks/blocks/Source.stories.tsx` around lines 292 -
296, Add an automated play function to the ManyStaticCodeBlocksBenchmark story
so the benchmark is executed and verified in Storybook tests: in the
ManyStaticCodeBlocksBenchmark object (which currently calls <BenchmarkHarness
blocks={75} />) add a play: async ({ canvasElement }) => { const canvas =
within(canvasElement); const runButton = await
canvas.findByTestId('run-benchmark'); await userEvent.click(runButton); const
result = await canvas.findByTestId('benchmark-result'); await waitFor(() =>
expect(result.textContent).toMatch(/^0 extra renders/)); } using the existing
data-testid attributes ('run-benchmark' and 'benchmark-result'); keep or adjust
the parameters/tags as desired (you can keep '!vitest' if you intentionally want
to skip other test suites).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@code/addons/docs/src/blocks/blocks/Source.stories.tsx`:
- Around line 292-296: Add an automated play function to the
ManyStaticCodeBlocksBenchmark story so the benchmark is executed and verified in
Storybook tests: in the ManyStaticCodeBlocksBenchmark object (which currently
calls <BenchmarkHarness blocks={75} />) add a play: async ({ canvasElement }) =>
{ const canvas = within(canvasElement); const runButton = await
canvas.findByTestId('run-benchmark'); await userEvent.click(runButton); const
result = await canvas.findByTestId('benchmark-result'); await waitFor(() =>
expect(result.textContent).toMatch(/^0 extra renders/)); } using the existing
data-testid attributes ('run-benchmark' and 'benchmark-result'); keep or adjust
the parameters/tags as desired (you can keep '!vitest' if you intentionally want
to skip other test suites).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bc0ba5f9-d15e-4604-975f-73682e3d936c

📥 Commits

Reviewing files that changed from the base of the PR and between c0a8b4c and 0472750.

📒 Files selected for processing (4)
  • code/addons/docs/src/blocks/blocks/Source.stories.tsx
  • code/addons/docs/src/blocks/blocks/Source.test.tsx
  • code/addons/docs/src/blocks/blocks/Source.tsx
  • code/core/src/preview-errors.ts
✅ Files skipped from review due to trivial changes (1)
  • code/addons/docs/src/blocks/blocks/Source.test.tsx

@anchmelev
Copy link
Copy Markdown
Contributor Author

Thanks for the PR! The idea looks good, added a few remarks on the specifics.

Thanks for the review - I really appreciate it. I went through the remarks, addressed them inline, and pushed follow-up changes. Please take another look when you have a chance.

@anchmelev anchmelev requested a review from JReinhold March 26, 2026 03:15
Comment thread code/addons/docs/src/blocks/blocks/Source.stories.tsx Outdated
@anchmelev
Copy link
Copy Markdown
Contributor Author

Hi @JReinhold, thanks again for the approval on this PR.
The follow-up changes from review have been pushed, and the PR should be in good shape now. If there’s anything else you’d like me to adjust, or any remaining threads I should help close out, please let me know.
Thanks!

@Sidnioulz Sidnioulz self-assigned this Apr 10, 2026
@Sidnioulz Sidnioulz merged commit 054c23f into storybookjs:next Apr 13, 2026
117 of 118 checks passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in Core Team Projects Apr 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[Bug]: Performance regression when using many Source components in one mdx page

4 participants