Skip to content

perf(theme): add React Compiler support for automatic memoization & performance optimization 🚀 - #3586

Open
sanjaiyan-dev wants to merge 21 commits into
web-infra-dev:mainfrom
sanjaiyan-dev:sanjaiyan-react-compiler-optimize
Open

perf(theme): add React Compiler support for automatic memoization & performance optimization 🚀 #3586
sanjaiyan-dev wants to merge 21 commits into
web-infra-dev:mainfrom
sanjaiyan-dev:sanjaiyan-react-compiler-optimize

Conversation

@sanjaiyan-dev

@sanjaiyan-dev sanjaiyan-dev commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

📝 Summary

This PR integrates the React Compiler into Rspress core/theme packages following the React team's official guidance for shipping compiled libraries.

By pre-compiling Rspress components at build time:

  • Zero Consumer Config: Rspress users automatically get an optimized, memoized build out-of-the-box without needing to configure React Compiler in their own applications.
  • Consistent Performance: Guarantees optimized rendering regardless of the user's bundler or build setup.
  • Full Backwards Compatibility: Configured with react-compiler-runtime to seamlessly support both React 18 and React 19.

ℹ️ Note on Code Changes: These minor refactorings were strictly done to help the React Compiler safely analyze and optimize components. They are purely structural and have zero impact on runtime functionality, component behavior, or output logic.


🔍 Motivation & Problem Statement

Currently, the Rspress codebase relies heavily on manual optimization (useMemo, useCallback, React.memo), leading to missing memoization in several key UI components.

Key Issues Observed:

  1. Unnecessary Re-renders On Scroll / Interaction: While developing sites with Rspress (such as rspress-plugin-third-parties), frequent, unneeded re-renders occur during basic page scrolling and layout updates.
  2. Inline Object & Component Recreation: Static components and dynamic object maps are repeatedly recreated during render cycles.
  3. Manual Optimization Overhead: Requiring contributors to manually maintain dependency arrays and wrap hooks in useMemo/useCallback is error-prone and adds boilerplate.

💡 Examples in Current Codebase

1. Object recreation & manual hook wrapping: PackageManagerTabs/index.tsx

On every render pass, packageMangerToIcon is recreated and mutated, while Pre relies on explicit useMemo:

const packageMangerToIcon: Record<string, ReactNode> = {
  npm: <Npm />,
  yarn: <Yarn />,
  pnpm: <Pnpm />,
  bun: <Bun />,
  deno: <Deno />,
};
additionalTabs.forEach(tab => {
  packageMangerToIcon[tab.tool] = tab.icon;
});

const Pre = useMemo(() => {
  return getCustomMDXComponent().pre;
}, [getCustomMDXComponent]);

With React Compiler: The compiler automatically caches dynamic element maps and component references at build time.


2. Static / Pure UI Components: Badge/index.tsx

Pure components like Badge execute and re-render every time parent context/state changes:

export function Badge({
  children,
  type = 'tip',
  text,
  outline = false,
}: BadgeProps) {
  const content = children || text;

  return (
    <span
      className={clsx(
        'rp-badge',
        `rp-badge--${type}`,
        outline && 'rp-badge--outline',
      )}
    >
      {content}
    </span>
  );
}

With React Compiler: Render outputs are automatically memoized without needing to wrap every UI component in React.memo.


🛠️ Implementation Details (React Compiler Library Setup)

Following the React team's official recommendations for library authors:

  1. Babel Plugin: Added babel-plugin-react-compiler as a dev dependency to handle build-time compilation.
  2. Runtime Package: Added react-compiler-runtime as a direct dependency in package.json to handle runtime auto-memoization hooks across versions:
    {
      "dependencies": {
        "react-compiler-runtime": "^1.0.0"
      }
    }
  3. Target Version Config: Set target version configuration (target: '18') to maintain full backward compatibility for React 18 and React 19 users.

🚀 Performance Impact

  • Reduced Scrolling Re-renders: Smooths out interactive frame rates on documentation pages during scroll and drawer navigation.
  • Eliminates Manual Boilerplate: Developers no longer need to write manual useMemo / useCallback / React.memo for component-level performance gains.
  • Zero Overhead: Eliminates unnecessary render passes without requiring manual code refactoring across the codebase.

@sanjaiyan-dev

Copy link
Copy Markdown
Contributor Author

To provide additional context on why shipping React Compiler with Rspress is a safe and high-impact improvement, here is how other major production applications and libraries have benefited from it:

📊 Real-World Production Benchmarks

  • Meta Quest Store:

    • 12% improvement in initial load and cross-page navigation.
    • Up to 2.5× faster interaction speeds while keeping memory neutral.
  • Wakelet (100% User Rollout):

    • 15% overall INP improvement (275ms ➔ 240ms), with pure UI components seeing up to ~30% INP speedups.
    • 10% LCP improvement (2.6s ➔ 2.4s).
    • Described the rollout as a high-return, near "free lunch" improvement for core UX and Lighthouse metrics.
  • Sanity Studio & Component Libraries:

    • Successfully shipped compiled libraries targeting React 18 using react-compiler-runtime (@sanity/ui, @portabletext/editor).
    • Measured a 20–30% increase in eFPS (frames per second) during continuous typing and editing interactions simply by toggling the compiler on.

💡 Key Takeaways Beyond Performance

  1. Surfaces Hidden Bugs: Both Wakelet and Sanity noted that the compiler’s strict analysis helps surface long-standing "cold case" bugs and data races that were previously masked by excessive component re-renders.
  2. Proven Library Distribution Model: Sanity's rollout confirms that shipping compiled UI packages via react-compiler-runtime for React 18/19 compatibility works reliably in production npm ecosystems.

Integrating this into Rspress gives our documentation users these exact out-of-the-box rendering gains without requiring any manual setup on their end.


// TODO: fallback should be a loading spinner
export const Content = ({ fallback = <></> }: { fallback?: ReactNode }) => {
export const Content = ({

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This change was made to fix the following error from react-compiler.

(BuildHIR::node.lowerReorderableExpression) Expression type JSXFragment cannot be safely reordered.


const Pre = useMemo(() => {
return getCustomMDXComponent().pre;
}, [getCustomMDXComponent]);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This change was made to fix the following error, which is a stable external function getCustomMDXComponent.

Found 1 error:
Error: Found extra memoization dependencies
Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often.


const defaultValueIndex =
defaultValue !== undefined
? tabValues.findIndex(item => item.value === defaultValue)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Move the calling of React hook top to resolve the React compiler error and to maintain React standards:

Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)

SoonIter commented Aug 6, 2026

Copy link
Copy Markdown
Member

Thanks for taking the time to contribute this to Rspress. However, benchmarks from other products do not demonstrate that this change provides a meaningful benefit to Rspress. This PR adds a runtime dependency, changes the build pipeline, and requires source refactors, so I think it needs Rspress-specific evidence to justify the trade-off.

Could you provide before-and-after measurements on representative Rspress sites and interactions—for example, render counts, INP or frame time, bundle size, and build time—and identify the concrete bottlenecks this PR improves? Without that data, I don't think we have enough evidence to merge this change.

@SoonIter

SoonIter commented Aug 6, 2026

Copy link
Copy Markdown
Member

Bundle size comparison

https://github.com/web-infra-dev/rspress/actions/runs/31038294665?pr=3586#summary-92540812580

image

These bundle size changes are within an acceptable range.

@SoonIter

SoonIter commented Aug 6, 2026

Copy link
Copy Markdown
Member

😄 This part of the bundle size seems to be a constant, causing similar newly initialized projects to be about 10% larger.

image

@SoonIter SoonIter changed the title feat: add React Compiler support for automatic memoization & performance optimization 🚀 perf(theme): add React Compiler support for automatic memoization & performance optimization 🚀 Aug 6, 2026
@sanjaiyan-dev

Copy link
Copy Markdown
Contributor Author

Hi @SoonIter,

Extremely sorry for the delay! Here is the benchmark comparison and analysis I gathered from the React DevTools Profiler with screenshot proof.


1. PackageManagerTabs Component (Tab Switching & Re-renders)

  • Without Memoization (Screenshot 2):

    • Commit Render Duration: Consistently sitting at ~13.6ms across repeated state updates (commits at 1.3s, 2.0s, 2.7s, 3.4s, 4.3s).
    • Flamegraph Behavior: Every tab switch triggers unnecessary cascading re-renders down through sub-items (Bun, Yarn, BlogBanner, Anonymous wrappers, etc.).
  • With React.memo (Screenshot 1 - marked with Memo ✨):

    • Commit Render Duration: Drops to ~1.8ms – 2.0ms (commits at 2.0s, 2.8s, 3.4s, 4.0s, 5.1s).
    • Result: ~86% reduction in commit render time. Subtree re-rendering is skipped cleanly when switching active tabs.
Screenshot 2026-08-25 at 20 22 16 Screenshot 2026-08-25 at 20 26 27

2. Overview Page (Search / Filter Interaction)

  • Without Memoization (Screenshot 3):

    • Commit Render Duration: Takes ~12.1ms on every keystroke when typing into the filter box (root).
    • Flamegraph Behavior: Large waterfall where static UI parts re-render on every character typed (LlmsCopyButton, H1, MyH1, Nav, SvgCopy, and multiple SVG icons).
  • With React.memo (Screenshot 4 - marked with Memo ✨):

    • Commit Render Duration: Down to ~1.8ms – 2.0ms per filter update.
    • Result: ~85% speedup. All unaffected header items, SVG icons, and copy buttons are bypassed during filtering, keeping keystroke input smooth with zero frame lag.
Generated Image August 25, 2026 - 9_03PM Screenshot 2026-08-25 at 20 44 41

Summary

  • Both interactive paths (PackageManagerTabs switching and Overview input filtering) went from 12ms–13.6ms down to ~1.8ms–2ms.
  • Avoids re-rendering static icons and non-dependent sibling trees.

Let me know what you think or if you'd like me to profile any other specific component before merging!

@sanjaiyan-dev

Copy link
Copy Markdown
Contributor Author
  • Frame Budget: At 60fps (16.6ms budget) or 120fps (8.3ms), taking 13.6ms just for React render guarantees dropped frames during interactive operations.

  • Filter input lag: In the Overview filter, this 12ms runs on every keystroke. Stacking keystrokes can easily block the main thread.

  • Device scaling: On low-end hardware or 4x CPU throttle, that 13ms turns into 50ms+ (crossing the Long Task / INP threshold).

Dropping it to ~1.8ms leaves plenty of room on the main thread for layout and paint.

@sanjaiyan-dev

Copy link
Copy Markdown
Contributor Author

Added the profiler benchmark above (~85% drop in render time on search/tab updates).

Alternatively, if we want to keep the code cleaner, we can opt into React Compiler with "use memo" annotation mode for these specific components. Happy to adjust either way :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants