[material-ui] Reexport styles from `stylesOptimized` to improve TS performance
closes https://github.com/mui/material-ui/issues/42772, closes #47099
For Reviewer
- Moved
ThemetostylesOptimizedto remove cyclic deps and set theme.components to empty - Rexport the
Themefromstyleswith augmented theme.components to preserve the behavior - Mirror the export of
stylesOptimizedtostylesso that user can switch the import path without breaking change - Update all
<component>.d.tsto 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/stylesimport with@mui/material/stylesOptimizedincluding 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: {…} } })
- Replace every
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:
-
ThemeOptions.components→Components<Omit<Theme, 'components'>> - This requires resolving the full
Themeinterface -
ThemeextendsBaseThemeandCssVarsProperties, inlining all their type definitions -
Themeis referenced back inThemeOptions→ circular 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'>>:
- It must instantiate all 80+ component definitions
- Each component references its full Props interface (from the actual component file)
- Each
ComponentsOverridesandComponentsVariantsuses theThemegeneric with complexInterpolationtypes - The circular dependency causes TypeScript to repeatedly re-instantiate this massive type
- 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.tsdefinesThemewith simplecomponents?: ThemeComponents(no generics, no circular references) -
styles/createThemeNoVars.d.tsextendsThemeOptimizedinstead of defining inline - TypeScript resolves
ThemeOptimizedonce (fromstylesOptimized), avoiding repeated instantiations
Non-breaking for v7:
- Users continue using
import { ThemeOptions } from '@mui/material/styles'as before - The original
Themeinterface still usesComponents<T>generic for backward compatibility - Library authors can opt-in to
@mui/material/stylesOptimizedfor 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/styleswill work but with higher memory usage - Library authors building design systems should migrate to
stylesOptimizedfor 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
- [x] I have followed (at least) the PR section of the contributing guide.
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%) |
Generated by :no_entry_sign: dangerJS against c9f76a5f00fb34df7e2b1491e88366377c3a4a83
@siriwatknp Nice approach! The solution looks good from what I’ve seen. Could we optimize the sx prop with theme too (SxProps<Theme>)?
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
@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.
If you need some support with some chores around moving from styles to styles optimized and test stuff lmk :)
@possum-enjoyer please help test it out (best to test with any existing project to ensure compatibility)! it will help this PR to be merged faster.
From my first Tests I found that some Components were missing a theme like CssBaseline and The x Packages (but I guess they will be updated later on). Everything else was compatible without any problems. No eslint or tsc errors and no refactoring of code was necessary besides changing the import and augmenting the ThemeCompoments type
Also for some reason if I split up my custom theme into smaller objects like
const MuiButton: ThemeComponents["MuiButton"] ={...};
Behaves weird (investigating the type of MuiButton via vscode always pointed me to the old Components type). But that could very much user error, I will have a second look.
The only thing that was a little bit "painful" was extending the ThemeComponents Type with many Components. It's 5 minutes at most when using a good ide. On the other side I like augmenting the ThemeComponents type, it makes building a theme a little bit more like building a Lego set.
Besides that, the breaking of this circular dependency reduced tsc buid times drastically (more than 2.5x in one project). Great job finding that problem!
Small update: I did a median via hyperfine and before the build time for one project where I changed stuff for around 30 components was ~10 seconds. After installing the artifact from this draft pr and doing all necessary steps the build time was ~4 seconds. This is based on the artifact that was available 10/24/2025
Also for some reason if I split up my custom theme into smaller objects like
Can you tried the latest comment pnpm add https://pkg.pr.new/mui/material-ui/@mui/material@61ac3e1
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