Skip to content

Reexport styles from to improve TS performance - #47069

Closed
siriwatknp wants to merge 36 commits into
mui:masterfrom
siriwatknp:fix/types-theme-components2
Closed

Reexport styles from to improve TS performance#47069
siriwatknp wants to merge 36 commits into
mui:masterfrom
siriwatknp:fix/types-theme-components2

Conversation

@siriwatknp

@siriwatknp siriwatknp commented Oct 13, 2025

Copy link
Copy Markdown
Member

closes #42772, closes #47099

For Reviewer

  • Moved Theme to stylesOptimized to remove cyclic deps and set theme.components to empty
  • Rexport the Theme from styles with augmented theme.components to preserve the behavior
  • Mirror the export of stylesOptimized to styles so that user can switch the import path without breaking change
  • Update all <component>.d.ts to use from stylesOptimized and export its Theme types for selective augmentation

Summary

  • No changes for existing user
  • For user who wants to optimize TS instantiation time, do the following:
    • Replace every @mui/material/styles import with @mui/material/stylesOptimized including module augmentation
    • Selectively augment component to the theme for autocompletion
      import { ButtonTheme } from '@mui/material/Button';
      import { createTheme } from '@mui/material/stylesOptimized";
      
      declare module "@mui/material/stylesOptimized" {
        interface ThemeComponents extends ButtonTheme {}
      }
      
      createTheme({
        components: {
          //. ✅ type-safe
          MuiButton: {}
        }
      })

To test the change, checkout this PR:

cd packages/mui-material/perf-test
npx tsc --noEmit --diagnostics

Then edit the packages/mui-material/perf-test/test-createTheme.tsx to import createTheme from @mui/material/styles and run diagnosis again.

Compare the result between the two.

Root Cause

The issue stems from circular TypeScript dependency in the type definitions:

Original definition (packages/mui-material/src/styles/createThemeNoVars.d.ts):

export interface ThemeOptions extends Omit<SystemThemeOptions, 'zIndex'>, CssVarsOptions {
  components?: Components<Omit<Theme, 'components'>>; // ← References Theme
  // ... other properties
}

export interface BaseTheme extends SystemTheme {
  mixins: Mixins;
  palette: Palette & (CssThemeVariables extends { enabled: true } ? CssVarsPalette : {});
  shadows: Shadows;
  transitions: Transitions;
  typography: TypographyVariants;
  zIndex: ZIndex;
  unstable_strictMode?: boolean;
}

export interface Theme extends BaseTheme, CssVarsProperties {
  components?: Components<BaseTheme>; // ← Used by ThemeOptions
  // ... other properties
}

The circular path:

  1. ThemeOptions.componentsComponents<Omit<Theme, 'components'>>
  2. This requires resolving the full Theme interface
  3. Theme extends BaseTheme and CssVarsProperties, inlining all their type definitions
  4. Theme is referenced back in ThemeOptionscircular dependency

Why exponential type computation:

The Components<Theme> interface is massive - for each of 80+ MUI components, it references:

export interface Components<Theme = unknown> {
  MuiButton?: {
    defaultProps?: ComponentsProps['MuiButton']; // ← Button's props interface
    styleOverrides?: ComponentsOverrides<Theme>['MuiButton']; // ← Needs Theme generic
    variants?: ComponentsVariants<Theme>['MuiButton']; // ← Needs Theme generic
  };
  MuiCard?: {
    defaultProps?: ComponentsProps['MuiCard']; // ← Card's props interface
    styleOverrides?: ComponentsOverrides<Theme>['MuiCard']; // ← Needs Theme generic
    variants?: ComponentsVariants<Theme>['MuiCard']; // ← Needs Theme generic
  };
  // ... 80+ more components
}

When TypeScript resolves Components<Omit<Theme, 'components'>>:

  1. It must instantiate all 80+ component definitions
  2. Each component references its full Props interface (from the actual component file)
  3. Each ComponentsOverrides and ComponentsVariants uses the Theme generic with complex Interpolation types
  4. The circular dependency causes TypeScript to repeatedly re-instantiate this massive type
  5. During Webpack builds with ts-loader, these types are resolved for every module importing from @mui/material

Memory spike: From ~460MB to ~2.2GB (4× increase), causing OOM errors in CI/CD

User Journey:

// ❌ ANY import from @mui/material/styles triggers memory spike
import { createTheme, ThemeOptions } from '@mui/material/styles';
//       ^^^^^^^^^^^ ← Even just importing createTheme loads circular types
export const themeOptions: ThemeOptions = {
  palette: { primary: { main: '#1976d2' } },
};
// Webpack with ts-loader: 2.2GB heap usage

// ✅ Real solution: Use stylesOptimized entry point
import { createTheme, ThemeOptions } from '@mui/material/stylesOptimized';
export const themeOptions: ThemeOptions = {
  palette: { primary: { main: '#1976d2' } },
};
// Webpack with ts-loader: ~460MB heap usage (normal)

Solution

Created alternative entry point stylesOptimized that breaks circular dependency by moving complete Theme definition there, making createThemeNoVars.d.ts reference it instead of defining inline.

Key Changes

1. New optimized entry point (packages/mui-material/src/stylesOptimized/createTheme.d.ts):

// Define complete Theme and ThemeOptions without circular dependency
export interface ThemeComponents {
  mergeClassNameAndStyles?: boolean;
  [componentName: string]: any;
}

export interface ThemeOptions extends Omit<SystemThemeOptions, 'zIndex'>, CssVarsOptions {
  components?: ThemeComponents; // ← Simple, non-generic type
  palette?: PaletteOptions;
  // ... other properties
}

export interface BaseTheme extends SystemTheme {
  mixins: Mixins;
  palette: Palette & (CssThemeVariables extends { enabled: true } ? CssVarsPalette : {});
  shadows: Shadows;
  transitions: Transitions;
  typography: TypographyVariants;
  zIndex: ZIndex;
  unstable_strictMode?: boolean;
}

export interface Theme extends BaseTheme, CssVarsProperties {
  cssVariables?: false;
  components?: ThemeComponents; // ← No generic, no circular reference
  unstable_sx: (props: SxProps<Theme>) => CSSObject;
  // ... other properties
}

2. Mirror exports (packages/mui-material/src/stylesOptimized/index.ts):

/**
 * This file must mirror the exports of `@mui/material/styles` for non-breaking changes in v7.
 * This entry point is an alternative for `@mui/material/styles` for optimizing TypeScript interface instantiation
 */

export {
  default as createTheme,
  ThemeOptions,
  Theme,
  // ... all other exports from @mui/material/styles
} from './createTheme';

3. Update original definition (packages/mui-material/src/styles/createThemeNoVars.d.ts):

// Before: Inline Theme definition (causes circular dependency)
export interface BaseTheme extends SystemTheme {
  mixins: Mixins;
  palette: Palette & (CssThemeVariables extends { enabled: true } ? CssVarsPalette : {});
  // ... 50+ lines
}

export interface Theme extends BaseTheme, CssVarsProperties {
  components?: Components<BaseTheme>;
  // ... 10+ lines
}

// After: Reference pre-defined Theme from stylesOptimized
import { Theme as ThemeOptimized } from '../stylesOptimized';

export interface Theme extends ThemeOptimized {
  components?: Components<Omit<ThemeOptimized, 'components'>>;
}

Why This Works

Breaks circular dependency:

  • stylesOptimized/createTheme.d.ts defines Theme with simple components?: ThemeComponents (no generics, no circular references)
  • styles/createThemeNoVars.d.ts extends ThemeOptimized instead of defining inline
  • TypeScript resolves ThemeOptimized once (from stylesOptimized), avoiding repeated instantiations

Non-breaking for v7:

  • Users continue using import { ThemeOptions } from '@mui/material/styles' as before
  • The original Theme interface still uses Components<T> generic for backward compatibility
  • Library authors can opt-in to @mui/material/stylesOptimized for better build performance

Performance impact (from analysis):

Before (with circular dependency):
  Instantiations: 744,661
  Memory used:    ~2,200MB (in Webpack builds)
  Build time:     High memory pressure, OOM failures

After (with stylesOptimized):
  Instantiations: ~300,000 (-60%)
  Memory used:    ~600MB (-73%)
  Build time:     Significantly reduced

Usage for Library Authors

To benefit from improved TypeScript performance, replace ALL imports from @mui/material/styles with @mui/material/stylesOptimized:

// Before: Using @mui/material/styles (causes memory spike)
import { createTheme, ThemeOptions } from '@mui/material/styles';

declare module '@mui/material/styles' {
  interface Theme {
    customProperty: string;
  }
  interface ThemeOptions {
    customProperty?: string;
  }
}

export const themeOptions: ThemeOptions = {
  /* ... */
};
// After: Using @mui/material/stylesOptimized (optimized performance)
import { createTheme, ThemeOptions } from '@mui/material/stylesOptimized';

declare module '@mui/material/stylesOptimized' {
  // ← Change module augmentation too!
  interface Theme {
    customProperty: string;
  }
  interface ThemeOptions {
    customProperty?: string;
  }
}

export const themeOptions: ThemeOptions = {
  /* ... */
};

Important:

  • This is an opt-in optimization - no breaking changes for existing code
  • Users continuing to import from @mui/material/styles will work but with higher memory usage
  • Library authors building design systems should migrate to stylesOptimized for CI/CD stability

Result

Metric Baseline (ThemeOptions) Fix Improvement
Instantiations 747348 337500 54.9%
Memory used 553 MB 364879K 34.0%
Check time 4.43s 2.38s 46.3%
Total time 5.65s 3.29s 41.7%

Before:

Files:              815
Lines:           164674
Identifiers:     130208
Symbols:         377356
Types:           117528
Instantiations:  747348
Memory used:    553221K
I/O read:         0.15s
I/O write:        0.00s
Parse time:       1.02s
Bind time:        0.20s
Check time:       4.43s
Emit time:        0.00s
Total time:       5.65s

After:

Files:              355
Lines:           139531
Identifiers:     110988
Symbols:         258356
Types:            92894
Instantiations:  337500
Memory used:    364879K
I/O read:         0.08s
I/O write:        0.00s
Parse time:       0.73s
Bind time:        0.18s
Check time:       2.38s
Emit time:        0.00s
Total time:       3.29s

@siriwatknp siriwatknp added typescript type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature. package: material-ui Specific to Material UI. labels Oct 13, 2025
@mui-bot

mui-bot commented Oct 13, 2025

Copy link
Copy Markdown

Netlify deploy preview

Bundle size report

Bundle Parsed size Gzip size
@mui/material 0B(0.00%) 0B(0.00%)
@mui/lab 0B(0.00%) 0B(0.00%)
@mui/system 0B(0.00%) 0B(0.00%)
@mui/utils 0B(0.00%) 0B(0.00%)

Details of bundle changes

Generated by 🚫 dangerJS against c9f76a5

@zannager zannager added scope: system The system, the design tokens / styling foundations used across components. eg. @mui/system with MUI and removed package: material-ui Specific to Material UI. labels Oct 13, 2025
@siriwatknp
siriwatknp force-pushed the fix/types-theme-components2 branch from b3d075e to e050e50 Compare October 14, 2025 05:52
@ZeeshanTamboli

Copy link
Copy Markdown
Member

@siriwatknp Nice approach! The solution looks good from what I’ve seen. Could we optimize the sx prop with theme too (SxProps<Theme>)?

@possum-enjoyer

possum-enjoyer commented Oct 19, 2025

Copy link
Copy Markdown

Ran the test and can confirm the numbers.
Also the ts lsp in vscode was faster again.

If you need some support with some chores around moving from styles to styles optimized and test stuff lmk :)

Edit: This fixes a very "weird" Problem with circular Depdencies regarding createTheme and Theme:

const theme1 = createTheme({
    components: {
        MuiButton: {
            styleOverrides: {
                root: {
                    backgroundColor: 'primary.main'
                }
            }
        }
    }
});

const theme2 = createTheme(theme1)

if you create a theme (theme1) with a ThemeOptions objects as the parameter and then pass the theme to another createTheme the compiler almost takes almost 3x as long. Its a weird problem because i would say no one "should" do that in real life bug due to how the ThemeOptions and Theme Types are constrcuted this is not an error and it produces a valid theme at the end. A way to circumvent this currently is to extract the ThemeOptions of theme1 and pass it to theme1 and theme2. If that is not possible in the code base, casting theme1 as ThemeOptions helps too:

const theme1Options: ThemeOptions = {
    components: {
        MuiButton: {
            styleOverrides: {
                root: {
                    backgroundColor: 'primary.main'
                }
            }
        }
    }
}
const theme1 = createTheme(theme1Options);

const theme2 = createTheme(theme1Options);

const theme3 = createTheme(theme1 as ThemeOptions)

I created an issue for the this behavior, to adress it separatley and would suggest to add a warning to the page.
Why do i highlight this issue in detail: It shows that this solution has the potential of dealing with many circular dependencies inside the createTheme / Theme / styles space

Comment thread packages/mui-material/src/stylesOptimized/createTheme.d.ts Outdated
@siriwatknp

Copy link
Copy Markdown
Member Author

@siriwatknp Nice approach! The solution looks good from what I’ve seen. Could we optimize the sx prop with theme too (SxProps<Theme>)?

I would leave the SxProps out of this PR. I recalled that I did update the SxProps type to handle several cases. To optimized it, it will be a breaking change.

@possum-enjoyer

Copy link
Copy Markdown

Can you tried the latest comment pnpm add https://pkg.pr.new/mui/material-ui/@mui/material@61ac3e1

yep that fixes the old Components type references, if all imports are from stylesOptimized and there are no barrelimports :)

Is it ok for the ButtonTheme etc. to be exported from the root i.e. being barrel importable? Or should they only be importable from their subdirectory i.e. import type {ButtonTheme} from '@mui/matertial/Button and not import type {ButtonTheme} from '@mui/matertial

@github-actions github-actions Bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Oct 30, 2025
Comment thread test/ts-performance/package.json Outdated
@github-actions github-actions Bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Nov 6, 2025
Comment thread test/ts-performance/package.json Outdated
@github-actions github-actions Bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Nov 19, 2025
@github-actions github-actions Bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Nov 20, 2025
@github-actions github-actions Bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Dec 1, 2025
@thaindq

thaindq commented Jan 22, 2026

Copy link
Copy Markdown

@siriwatknp: any update on this?

@siriwatknp siriwatknp changed the title [material-ui] Reexport styles from stylesOptimized to improve TS performance Reexport styles from to improve TS performance Mar 18, 2026
@silviuaavram silviuaavram modified the milestones: MUI X v9, Material UI v9.x Apr 20, 2026
@connorshea

Copy link
Copy Markdown
Contributor

I would love to see this merged or iterated upon, around 40% of our application's typechecking time is spent on computations related to MUI's createTheme

@siriwatknp

Copy link
Copy Markdown
Member Author

Update after testing this PR more deeply with the built package and the test/ts-performance harness.

The numbers in the PR are correct. But I found a structural problem with the opt-in model.

The optimization turns off when any file imports from @mui/material/styles or @mui/material.

The strict types come back through the module augmentation in styles/createThemeNoVars.d.ts. Module augmentation is global to the TS program. So one import anywhere brings the cost back for every file, even files that already migrated:

scenario files instantiations check time
all imports from stylesOptimized 187 187 0.03s
+ one 2-line file with createTheme({}) from styles 620 145,230 0.46s
+ one import from @mui/material root 726 145,230 0.43s

The bigger issue: dependencies. MUI X, Toolpad, any theme library on npm imports @mui/material/styles. App developers cannot fix those imports. So for most real apps, the opt-in can never activate.

Other findings from testing:

  1. Typography.d.ts still imports Theme from ../styles. Any app that uses Typography gets the strict types back, even with perfect import discipline.
  2. styles and stylesOptimized now have two different ThemeOptions interfaces. Libraries that augment ThemeOptions via declare module '@mui/material/styles' do not reach the optimized one.
  3. In the build output, imports are rewritten to ../stylesOptimized/index.js but declare module '../stylesOptimized' stays without extension. Not sure it resolves under nodenext ESM. skipLibCheck would hide the failure.

New direction I want to try: a type-level flag instead of a new entry point.

Same pattern as the existing CssThemeVariables toggle. Users opt in with one augmentation, no import changes:

declare module '@mui/material/styles' {
  interface TypeFeatures {
    optimizedTheme: true;
  }
}

Inside styles, every strict Components<...> site becomes conditional:

export interface TypeFeatures {}

// in Theme, ThemeOptions, CssVarsThemeOptions:
components?: TypeFeatures extends { optimizedTheme: true }
  ? ThemeComponents
  : Components<Omit<Theme, 'components'>>;

Conditional type branches are lazy, so the strict branch is never instantiated when the flag is on.

I prototyped this on the built package of this PR and measured with the same harness:

scenario (all imports stay @mui/material/styles) instantiations check time
flag off 144,499 0.47s
flag on 173 0.02s
flag on + stray styles import + root import 173 0.02s
flag on + selective ThemeComponents extends ButtonTheme 4,173 0.06s

Flag off is identical to today (144,499 vs 144,498 on this branch). Flag on matches the pristine stylesOptimized numbers. And stray imports or dependencies cannot turn it off.

This removes the need for the stylesOptimized mirror, the codemod, and the import migration. The per-component <X>Theme exports stay, they are needed for the selective augmentation.

Known tradeoffs:

  1. the file graph still loads (~620 files vs 355 with the entry point), so parse time and some memory remain. The instantiation explosion, which causes the OOM, is gone.
  2. a published library that ships the flag augmentation would turn it on for all consumers. Needs a docs warning, same class of hazard as themeCssVarsAugmentation.

I will open a draft PR with this approach from the current master to compare.

@siriwatknp

Copy link
Copy Markdown
Member Author

Opened the flag-based alternative as a draft: #49004

Numbers on current master with TypeScript 6.0.3: flag off 145,559 instantiations / 6.24s check, flag on 1,922 / 0.06s. Stray imports and dependencies cannot turn it off.

@siriwatknp siriwatknp closed this Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

PR: out-of-date The pull request has merge conflicts and can't be merged. scope: system The system, the design tokens / styling foundations used across components. eg. @mui/system with MUI type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature. typescript

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Passing a Theme object into createTheme increases compile time Some bad ts performance cases

9 participants