diff --git a/.circleci/config.yml b/.circleci/config.yml index b6e9212f4a483e..c7195afe2d115d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -214,6 +214,47 @@ jobs: command: pnpm test:browser --no-isolate --no-file-parallelism - store_test_results: path: test-results + test_benchmark: + <<: *default-job + # 2 vCPUs starve V8's background compilers, which makes the same code's speed vary from sample to + # sample; see the @mui/internal-benchmark README. + resource_class: medium+.gen2 + executor: + name: code-infra/mui-node-browser + playwright-img-version: v1.63.0-noble + steps: + - checkout + - code-infra/install-deps + - run: + name: Build packages + command: pnpm release:build + - run: + name: Resolve the baseline commit + command: | + set -e + BASE_SHA=$(pnpm code-infra baseline --base-branch master) + echo "Baseline commit: $BASE_SHA" + echo "$BASE_SHA" > .benchmark-baseline-sha + - restore_cache: + name: Restore the packed-baseline cache + keys: + - benchmark-cache-v1-{{ checksum ".benchmark-baseline-sha" }} + - run: + name: Run the benchmarks + no_output_timeout: 30m + # Only the smoke benchmarks, to keep a pull request's check short; the other benchmark files + # are left to manual runs. + command: pnpm test:benchmark smoke --baseline "$(cat .benchmark-baseline-sha)" + - run: + name: Upload the benchmark report + # Also after a failed run, so the comment says what happened. + when: always + command: pnpm test:benchmark upload + - save_cache: + name: Save the packed-baseline cache + key: benchmark-cache-v1-{{ checksum ".benchmark-baseline-sha" }} + paths: + - test/benchmark/.benchmark/packed test_e2e: <<: *default-job resource_class: medium @@ -474,6 +515,8 @@ workflows: <<: *default-context - test_browser: <<: *default-context + - test_benchmark: + <<: *default-context - test_browser_legacy: <<: *default-context - test_regressions: diff --git a/.gitignore b/.gitignore index 48a54e6d773432..418a2d289f56d3 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,9 @@ docs/public/static/blog/feed/* screenshots/ packed test-results/ +# `benchmark` CLI output and generated pages +.benchmark/ +test/benchmark/src/__bench__/ .env .claude/worktrees/ diff --git a/package.json b/package.json index 8e483ce8e8ec0f..30d18ecede0d1f 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "test:coverage": "pnpm test:unit run --coverage", "vitest": "vitest", "test:coverage:html": "pnpm test:unit run --coverage --coverage.reporter html", + "test:benchmark": "pnpm -F ./test/benchmark benchmark", "test:e2e": "pnpm -F ./test/e2e start", "test:e2e:dev": "pnpm -F ./test/e2e dev", "test:e2e-website": "playwright test test/e2e-website --config test/e2e-website/playwright.config.ts", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1edbcadfd18026..2f43b0faaa50bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -209,7 +209,7 @@ importers: version: 5.1.0(@types/express@5.0.6)(supports-color@11.0.0)(undici@7.29.0) execa: specifier: ^10.0.1 - version: 10.0.1 + version: 10.2.0 devDependencies: '@arethetypeswrong/cli': specifier: 0.18.5 @@ -291,7 +291,7 @@ importers: version: typescript@7.0.2 '@vitejs/plugin-react': specifier: ^6.0.0 - version: 6.0.5(babel-plugin-react-compiler@1.0.0)(vite@8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.0)) + version: 6.1.2(babel-plugin-react-compiler@1.0.0)(vite@8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.0)) '@vitest/browser-playwright': specifier: ^5.0.0 version: 5.0.0(playwright@1.63.0)(vite@8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.0))(vitest@5.0.0) @@ -1655,6 +1655,40 @@ importers: specifier: 18.1.0 version: 18.1.0 + test/benchmark: + dependencies: + '@emotion/react': + specifier: catalog:docs + version: 11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@11.0.0) + '@emotion/styled': + specifier: catalog:docs + version: 11.14.1(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@11.0.0))(@types/react@19.3.0)(react@19.3.0)(supports-color@11.0.0) + '@mui/material': + specifier: workspace:* + version: link:../../packages/mui-material/build + react: + specifier: 19.3.0 + version: 19.3.0 + react-dom: + specifier: 19.3.0 + version: 19.3.0(react@19.3.0) + devDependencies: + '@mui/internal-benchmark': + specifier: 0.1.1-canary.8 + version: 0.1.1-canary.8(@playwright/test@1.63.0)(@vitest/browser-playwright@5.0.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(vite@8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.1))(vitest@5.0.0) + '@playwright/test': + specifier: 1.63.0 + version: 1.63.0 + '@types/react': + specifier: 19.3.0 + version: 19.3.0 + '@types/react-dom': + specifier: 19.3.0 + version: 19.3.0(@types/react@19.3.0) + vite: + specifier: ^8.0.0 + version: 8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.1) + test/bundle-size: dependencies: '@mui/internal-core-docs': @@ -2969,6 +3003,9 @@ packages: '@babel/runtime@8.0.5': resolution: {integrity: sha512-7NK+Lz3spQ52XsUGTxIEVU4jYN2/dIaX8sTxRFAUtYmttYZnVh3aehiihv+/Gb7+duMNHl2OrFcBQRyOHScxpg==} + '@babel/runtime@8.0.7': + resolution: {integrity: sha512-k0hagDDq/6w4v+0Sp4wFvIz/yswH5O7Va6aRTnD7N62Tnborp4X+YB+H3XUcWVSR4aE47WPEXiIIZchBs0GmYg==} + '@babel/template@7.29.7': resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} @@ -4182,6 +4219,20 @@ packages: peerDependencies: '@babel/core': ^8.0.0 + '@mui/internal-benchmark@0.1.1-canary.8': + resolution: {integrity: sha512-2BTfvDhlnyPwWwGsMnrjKGqvSnlYpeC3iBsYuNm7oNXiTc+ESH4wnEGTk+lWYgm6gsPR3MzgtAulhvc8g76jNA==} + hasBin: true + peerDependencies: + '@playwright/test': '>=1.50.0' + '@vitest/browser-playwright': '>=4.1' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + vite: ^7.0.0 || ^8.0.0 + vitest: '>=4.1' + peerDependenciesMeta: + vite: + optional: true + '@mui/internal-bundle-size-checker@1.1.1-canary.2': resolution: {integrity: sha512-FJPyn+vIH/IbdHef9qY2BCdXKHPJAsnwAV/fz9Im01o1blE7MeAVn1CsjWoAdeijylm7Zb+qKgZaJma5+24w/w==} hasBin: true @@ -6458,18 +6509,21 @@ packages: cpu: [x64] os: [win32] - '@vitejs/plugin-react@6.0.5': - resolution: {integrity: sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==} + '@vitejs/plugin-react@6.1.2': + resolution: {integrity: sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 babel-plugin-react-compiler: ^1.0.0 + oxc-transform-react: ^0.152.0 vite: ^8.0.0 peerDependenciesMeta: '@rolldown/plugin-babel': optional: true babel-plugin-react-compiler: optional: true + oxc-transform-react: + optional: true '@vitest/browser-playwright@5.0.0': resolution: {integrity: sha512-N+gED9y4/8pypaHjz/x0ah3CjoBr+N0hWWT+Gq4VXtMzyB+rUIdHni0ulzpD1j4H77iWy5Jg8PRMlVn6kjxo6g==} @@ -8062,8 +8116,8 @@ packages: resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==} engines: {node: '>=6'} - execa@10.0.1: - resolution: {integrity: sha512-ge98qjkRK4IB7tL7Ju/6qmm5LHoH1eEMt5FNZrz3f4UIYhF28lggX20z3FaX1sgc67msLEn0N0BscOs29iuwyw==} + execa@10.2.0: + resolution: {integrity: sha512-j1pFOVxaybMVCTNHSBuISL2KQcDS3KIE1wBmUp4n2X+1FgaQI2ZDJVy9TYxDyMCzYojRIMh9O8X/UoWZtxTJdw==} engines: {node: '>=22'} execa@5.0.0: @@ -10549,8 +10603,8 @@ packages: resolution: {integrity: sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==} engines: {node: '>= 0.8'} - pretty-ms@9.3.0: - resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + pretty-ms@9.3.1: + resolution: {integrity: sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA==} engines: {node: '>=18'} pretty-quick@4.2.2: @@ -12364,8 +12418,8 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - zod@4.6.5: - resolution: {integrity: sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==} + zod@4.6.7: + resolution: {integrity: sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==} zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -13991,6 +14045,8 @@ snapshots: '@babel/runtime@8.0.5': {} + '@babel/runtime@8.0.7': {} + '@babel/template@7.29.7': dependencies: '@babel/code-frame': 7.29.7 @@ -15029,19 +15085,45 @@ snapshots: '@babel/core': 8.0.6 resolve: 1.22.12 + '@mui/internal-benchmark@0.1.1-canary.8(@playwright/test@1.63.0)(@vitest/browser-playwright@5.0.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(vite@8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.1))(vitest@5.0.0)': + dependencies: + '@babel/runtime': 8.0.7 + '@playwright/test': 1.63.0 + '@pnpm/find-workspace-dir': 1000.1.5 + '@vitejs/plugin-react': 6.1.2(babel-plugin-react-compiler@1.0.0)(vite@8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.1)) + '@vitest/browser-playwright': 5.0.0(playwright@1.63.0)(vite@8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.1))(vitest@5.0.0) + chalk: 6.0.1 + env-ci: 11.2.0 + es-toolkit: 1.52.0 + execa: 10.2.0 + globby: 16.2.4 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + sirv: 3.0.2 + vitest: 5.0.0(@types/node@20.19.43)(@vitest/browser-playwright@5.0.0)(@vitest/coverage-v8@5.0.0)(@vitest/ui@5.0.0)(happy-dom@20.11.2)(jsdom@30.0.1)(vite@8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.1)) + yaml: 2.9.1 + yargs: 18.2.0 + zod: 4.6.7 + optionalDependencies: + vite: 8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.1) + transitivePeerDependencies: + - '@rolldown/plugin-babel' + - babel-plugin-react-compiler + - oxc-transform-react + '@mui/internal-bundle-size-checker@1.1.1-canary.2(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(rolldown@1.2.4)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.0)': dependencies: '@octokit/rest': 22.0.1 chalk: 6.0.1 env-ci: 11.2.0 - execa: 10.0.1 + execa: 10.2.0 git-url-parse: 16.1.0 micromatch: 4.0.8 piscina: 5.3.2 rollup-plugin-visualizer: 7.1.1(rolldown@1.2.4) vite: 8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.0) yargs: 18.2.0 - zod: 4.6.5 + zod: 4.6.7 transitivePeerDependencies: - '@types/node' - '@vitejs/devtools' @@ -15113,7 +15195,7 @@ snapshots: eslint-plugin-react-compiler: 19.1.0-rc.2(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) eslint-plugin-react-hooks: 7.1.1(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) eslint-plugin-testing-library: 7.16.2(@typescript/typescript6@6.0.2)(eslint@10.10.0(jiti@2.7.0)(supports-color@11.0.0))(supports-color@11.0.0) - execa: 10.0.1 + execa: 10.2.0 git-url-parse: 16.1.0 globals: 17.13.0 globby: 16.2.4 @@ -17345,13 +17427,20 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.12.2': optional: true - '@vitejs/plugin-react@6.0.5(babel-plugin-react-compiler@1.0.0)(vite@8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.0))': + '@vitejs/plugin-react@6.1.2(babel-plugin-react-compiler@1.0.0)(vite@8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.0))': dependencies: '@rolldown/pluginutils': 1.0.1 vite: 8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.0) optionalDependencies: babel-plugin-react-compiler: 1.0.0 + '@vitejs/plugin-react@6.1.2(babel-plugin-react-compiler@1.0.0)(vite@8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.1))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.1) + optionalDependencies: + babel-plugin-react-compiler: 1.0.0 + '@vitest/browser-playwright@5.0.0(playwright@1.63.0)(vite@8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.0))(vitest@5.0.0)': dependencies: '@vitest/browser': 5.0.0(vite@8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.0))(vitest@5.0.0) @@ -17365,6 +17454,19 @@ snapshots: - utf-8-validate - vite + '@vitest/browser-playwright@5.0.0(playwright@1.63.0)(vite@8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.1))(vitest@5.0.0)': + dependencies: + '@vitest/browser': 5.0.0(vite@8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.1))(vitest@5.0.0) + '@vitest/mocker': 5.0.0(vite@8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.1)) + playwright: 1.63.0 + tinyrainbow: 3.1.1 + vitest: 5.0.0(@types/node@20.19.43)(@vitest/browser-playwright@5.0.0)(@vitest/coverage-v8@5.0.0)(@vitest/ui@5.0.0)(happy-dom@20.11.2)(jsdom@30.0.1)(vite@8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.1)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + '@vitest/browser@5.0.0(vite@8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.0))(vitest@5.0.0)': dependencies: '@blazediff/core': 1.10.0 @@ -17383,6 +17485,24 @@ snapshots: - utf-8-validate - vite + '@vitest/browser@5.0.0(vite@8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.1))(vitest@5.0.0)': + dependencies: + '@blazediff/core': 1.10.0 + '@vitest/mocker': 5.0.0(vite@8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.1)) + '@vitest/ui': 5.0.0(vitest@5.0.0) + '@vitest/utils': 5.0.0 + magic-string: 1.3.1 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.1 + vitest: 5.0.0(@types/node@20.19.43)(@vitest/browser-playwright@5.0.0)(@vitest/coverage-v8@5.0.0)(@vitest/ui@5.0.0)(happy-dom@20.11.2)(jsdom@30.0.1)(vite@8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.1)) + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + '@vitest/coverage-v8@5.0.0(@vitest/browser@5.0.0)(vitest@5.0.0)': dependencies: '@bcoe/v8-coverage': 1.0.2 @@ -17434,6 +17554,15 @@ snapshots: optionalDependencies: vite: 8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.0) + '@vitest/mocker@5.0.0(vite@8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.1))': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.0 + estree-walker: 3.0.3 + magic-string: 1.3.1 + optionalDependencies: + vite: 8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.1) + '@vitest/pretty-format@4.1.10': dependencies: tinyrainbow: 3.1.1 @@ -19123,8 +19252,8 @@ snapshots: '@babel/parser': 7.29.8 eslint: 10.10.0(jiti@2.7.0)(supports-color@11.0.0) hermes-parser: 0.25.1 - zod: 4.6.5 - zod-validation-error: 4.0.2(zod@4.6.5) + zod: 4.6.7 + zod-validation-error: 4.0.2(zod@4.6.7) transitivePeerDependencies: - supports-color @@ -19263,7 +19392,7 @@ snapshots: signal-exit: 3.0.7 strip-eof: 1.0.0 - execa@10.0.1: + execa@10.2.0: dependencies: '@sindresorhus/merge-streams': 4.0.0 figures: 6.1.0 @@ -19272,7 +19401,7 @@ snapshots: is-plain-obj: 4.1.0 is-stream: 4.0.1 npm-run-path: 6.0.0 - pretty-ms: 9.3.0 + pretty-ms: 9.3.1 signal-exit: 4.1.0 strip-final-newline: 4.0.0 which-command: 0.1.0 @@ -19324,7 +19453,7 @@ snapshots: is-plain-obj: 4.1.0 is-stream: 4.0.1 npm-run-path: 6.0.0 - pretty-ms: 9.3.0 + pretty-ms: 9.3.1 signal-exit: 4.1.0 strip-final-newline: 4.0.0 yoctocolors: 2.2.0 @@ -22423,7 +22552,7 @@ snapshots: pretty-hrtime@1.0.3: {} - pretty-ms@9.3.0: + pretty-ms@9.3.1: dependencies: parse-ms: 4.0.0 @@ -24280,6 +24409,22 @@ snapshots: tsx: 4.23.13 yaml: 2.9.0 + vite@8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.1): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.28 + rolldown: 1.2.12 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 20.19.43 + esbuild: 0.28.2 + fsevents: 2.3.3 + jiti: 2.7.0 + terser: 5.50.0 + tsx: 4.23.13 + yaml: 2.9.1 + vitest-fail-on-console@0.10.1(@vitest/utils@5.0.0)(vite@8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.0))(vitest@5.0.0): dependencies: '@vitest/utils': 5.0.0 @@ -24313,6 +24458,32 @@ snapshots: transitivePeerDependencies: - msw + vitest@5.0.0(@types/node@20.19.43)(@vitest/browser-playwright@5.0.0)(@vitest/coverage-v8@5.0.0)(@vitest/ui@5.0.0)(happy-dom@20.11.2)(jsdom@30.0.1)(vite@8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.1)): + dependencies: + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.0(vite@8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.1)) + chai: 6.2.2 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 1.3.1 + obug: 2.1.4 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 6.1.4 + tinyexec: 1.3.0 + tinyglobby: 0.2.17 + vite: 8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.19.43 + '@vitest/browser-playwright': 5.0.0(playwright@1.63.0)(vite@8.3.3(@types/node@20.19.43)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.13)(yaml@2.9.1))(vitest@5.0.0) + '@vitest/coverage-v8': 5.0.0(@vitest/browser@5.0.0)(vitest@5.0.0) + '@vitest/ui': 5.0.0(vitest@5.0.0) + happy-dom: 20.11.2 + jsdom: 30.0.1 + transitivePeerDependencies: + - msw + w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 @@ -24632,12 +24803,12 @@ snapshots: dependencies: zod: 3.25.76 - zod-validation-error@4.0.2(zod@4.6.5): + zod-validation-error@4.0.2(zod@4.6.7): dependencies: - zod: 4.6.5 + zod: 4.6.7 zod@3.25.76: {} - zod@4.6.5: {} + zod@4.6.7: {} zwitch@2.0.4: {} diff --git a/test/benchmark/package.json b/test/benchmark/package.json new file mode 100644 index 00000000000000..939dc6ba2603e2 --- /dev/null +++ b/test/benchmark/package.json @@ -0,0 +1,25 @@ +{ + "private": true, + "description": "Render performance benchmarks for MUI packages, run with the `benchmark` CLI from @mui/internal-benchmark", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "typescript": "tsc -p tsconfig.json", + "benchmark": "benchmark" + }, + "dependencies": { + "@emotion/react": "catalog:docs", + "@emotion/styled": "catalog:docs", + "@mui/material": "workspace:*", + "react": "19.3.0", + "react-dom": "19.3.0" + }, + "devDependencies": { + "@mui/internal-benchmark": "0.1.1-canary.8", + "@playwright/test": "1.63.0", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "vite": "^8.0.0" + } +} diff --git a/test/benchmark/src/autocomplete/autocomplete.bench.tsx b/test/benchmark/src/autocomplete/autocomplete.bench.tsx new file mode 100644 index 00000000000000..399519a77273d1 --- /dev/null +++ b/test/benchmark/src/autocomplete/autocomplete.bench.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { reactBenchmark } from '@mui/internal-benchmark/page'; +import Autocomplete, { type AutocompleteProps } from '@mui/material/Autocomplete'; +import Checkbox from '@mui/material/Checkbox'; +import TextField from '@mui/material/TextField'; + +const options = Array.from({ length: 1000 }, (_, index) => `Option ${index}`); + +const renderOption: AutocompleteProps['renderOption'] = ( + props, + option, + { selected }, +) => { + const { key, ...optionProps } = props; + return ( +
  • + + {option} +
  • + ); +}; + +// The checkbox list of a multiple selection, as in mui/material-ui#34712. +function CheckboxesAutocomplete() { + return ( + } + /> + ); +} + +const setInputValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')!.set!; + +reactBenchmark( + 'Autocomplete type', + () => , + async ({ resumeReactRecording }) => { + const input = document.querySelector('input')!; + resumeReactRecording(); + setInputValue.call(input, '1'); + input.dispatchEvent(new Event('input', { bubbles: true })); + }, + { reactRecordingPaused: true }, +); diff --git a/test/benchmark/src/button/button.bench.tsx b/test/benchmark/src/button/button.bench.tsx new file mode 100644 index 00000000000000..359f9781d3d979 --- /dev/null +++ b/test/benchmark/src/button/button.bench.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; +import { reactBenchmark } from '@mui/internal-benchmark/page'; +import Button from '@mui/material/Button'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; + +// Customized the way applications commonly do, through component style overrides. +const theme = createTheme({ + components: { + MuiButton: { + styleOverrides: { + root: { textTransform: 'none', borderRadius: 8 }, + contained: { boxShadow: 'none' }, + }, + }, + }, +}); + +function Buttons({ count }: { count: number }) { + return ( + + {Array.from({ length: count }, (_, index) => ( + + ))} + + ); +} + +reactBenchmark('Button mount', () => ); diff --git a/test/benchmark/src/checkbox/checkbox.bench.tsx b/test/benchmark/src/checkbox/checkbox.bench.tsx new file mode 100644 index 00000000000000..31e8fc7de769de --- /dev/null +++ b/test/benchmark/src/checkbox/checkbox.bench.tsx @@ -0,0 +1,46 @@ +import * as React from 'react'; +import { reactBenchmark } from '@mui/internal-benchmark/page'; +import Checkbox from '@mui/material/Checkbox'; +import FormControlLabel from '@mui/material/FormControlLabel'; + +// A list whose selection lives in the parent, so checking one item re-renders every row. +function CheckboxList({ count }: { count: number }) { + const [checked, setChecked] = React.useState>(() => new Set()); + + return ( +
    + {Array.from({ length: count }, (_, index) => ( + { + setChecked((previous) => { + const next = new Set(previous); + if (next.has(index)) { + next.delete(index); + } else { + next.add(index); + } + return next; + }); + }} + /> + } + /> + ))} +
    + ); +} + +reactBenchmark( + 'Checkbox toggle', + () => , + async ({ resumeReactRecording }) => { + resumeReactRecording(); + document.querySelector('input[type="checkbox"]')!.click(); + }, + { reactRecordingPaused: true }, +); diff --git a/test/benchmark/src/menu/menu.bench.tsx b/test/benchmark/src/menu/menu.bench.tsx new file mode 100644 index 00000000000000..73c2d650f4c835 --- /dev/null +++ b/test/benchmark/src/menu/menu.bench.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { reactBenchmark } from '@mui/internal-benchmark/page'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; + +function OpenMenu({ count }: { count: number }) { + return ( + + {Array.from({ length: count }, (_, index) => ( + Item {index} + ))} + + ); +} + +reactBenchmark('Menu open', () => ); diff --git a/test/benchmark/src/smoke/smoke.bench.tsx b/test/benchmark/src/smoke/smoke.bench.tsx new file mode 100644 index 00000000000000..8b026c8a89d378 --- /dev/null +++ b/test/benchmark/src/smoke/smoke.bench.tsx @@ -0,0 +1,279 @@ +import * as React from 'react'; +import { reactBenchmark } from '@mui/internal-benchmark/page'; +import Accordion from '@mui/material/Accordion'; +import AccordionDetails from '@mui/material/AccordionDetails'; +import AccordionSummary from '@mui/material/AccordionSummary'; +import Alert from '@mui/material/Alert'; +import Autocomplete from '@mui/material/Autocomplete'; +import Avatar from '@mui/material/Avatar'; +import Badge from '@mui/material/Badge'; +import Breadcrumbs from '@mui/material/Breadcrumbs'; +import Button from '@mui/material/Button'; +import ButtonGroup from '@mui/material/ButtonGroup'; +import Card from '@mui/material/Card'; +import CardActions from '@mui/material/CardActions'; +import CardContent from '@mui/material/CardContent'; +import Checkbox from '@mui/material/Checkbox'; +import Chip from '@mui/material/Chip'; +import CircularProgress from '@mui/material/CircularProgress'; +import Divider from '@mui/material/Divider'; +import Fab from '@mui/material/Fab'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import IconButton from '@mui/material/IconButton'; +import LinearProgress from '@mui/material/LinearProgress'; +import Link from '@mui/material/Link'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemText from '@mui/material/ListItemText'; +import MenuItem from '@mui/material/MenuItem'; +import MenuList from '@mui/material/MenuList'; +import Pagination from '@mui/material/Pagination'; +import Paper from '@mui/material/Paper'; +import Radio from '@mui/material/Radio'; +import RadioGroup from '@mui/material/RadioGroup'; +import Rating from '@mui/material/Rating'; +import Select from '@mui/material/Select'; +import Slider from '@mui/material/Slider'; +import Step from '@mui/material/Step'; +import StepLabel from '@mui/material/StepLabel'; +import Stepper from '@mui/material/Stepper'; +import Switch from '@mui/material/Switch'; +import Tab from '@mui/material/Tab'; +import Tabs from '@mui/material/Tabs'; +import TextField from '@mui/material/TextField'; +import ToggleButton from '@mui/material/ToggleButton'; +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; +import Tooltip from '@mui/material/Tooltip'; +import Typography from '@mui/material/Typography'; + +// One small benchmark per component, all in one file: the runner loads the file's page once per +// round and samples each of them there, which keeps the page loads few and each mount small. + +// The smoke benchmarks are there to catch big regressions. A round of the whole file is long, so +// they get few rounds: they alarm past 10%, which those rounds can settle, rather than the default +// 5%, which they mostly can't. +const SMOKE_OPTIONS = { sampleSize: 30, alarm: { error: 0.1 } }; + +// The real cursor persists across samples and can come to rest over a tooltip trigger, opening a +// tooltip mid-sample, which only some samples would then pay for. Blocked on the container, so +// React writes no inline style onto every trigger inside the path being timed. +const INERT_TO_POINTER: React.CSSProperties = { pointerEvents: 'none' }; + +const OPTIONS = ['First', 'Second', 'Third', 'Fourth', 'Fifth']; +const STEPS = ['Account', 'Details', 'Review']; + +/** + * A smoke benchmark mounting `count` instances of a component, each as `renderInstance` renders it, + * inside a container styled with `containerStyle`. The indexes are created here, once per page + * load, so a sample only renders them. + */ +function smokeBenchmark( + name: string, + count: number, + renderInstance: (index: number) => React.ReactNode, + containerStyle?: React.CSSProperties, +) { + const indexes = Array.from({ length: count }, (_, index) => index); + reactBenchmark( + `Smoke ยท ${name}`, + () =>
    {indexes.map(renderInstance)}
    , + SMOKE_OPTIONS, + ); +} + +smokeBenchmark('Accordion', 40, (index) => ( + + Section {index} + Details {index} + +)); + +smokeBenchmark('Alert', 100, (index) => ( + + Message {index} + +)); + +smokeBenchmark('Autocomplete', 20, (index) => ( + } + /> +)); + +smokeBenchmark('Avatar', 500, (index) => ( + {String.fromCharCode(65 + (index % 26))} +)); + +smokeBenchmark('Badge', 200, (index) => ( + + Inbox + +)); + +smokeBenchmark('Breadcrumbs', 100, (index) => ( + + Home + Section + Page {index} + +)); + +smokeBenchmark('Button', 200, (index) => ( + +)); + +smokeBenchmark('ButtonGroup', 60, (index) => ( + + + + + +)); + +smokeBenchmark('Card', 100, (index) => ( + + + Card {index} + Some content + + + + + +)); + +smokeBenchmark('Checkbox', 60, (index) => ( + } label={`Option ${index}`} /> +)); + +smokeBenchmark('Chip', 250, (index) => ); + +smokeBenchmark('CircularProgress', 200, (index) => ); + +smokeBenchmark('Divider', 800, (index) => ); + +smokeBenchmark('Fab', 200, (index) => ( + + + + +)); + +smokeBenchmark('IconButton', 250, (index) => ( + + {index % 10} + +)); + +smokeBenchmark('LinearProgress', 300, (index) => ( + +)); + +smokeBenchmark('Link', 400, (index) => ( + + Link {index} + +)); + +smokeBenchmark('List', 20, (index) => ( + + {OPTIONS.map((option) => ( + + + + + + ))} + +)); + +smokeBenchmark('MenuList', 20, (index) => ( + + {OPTIONS.map((option) => ( + {option} + ))} + +)); + +smokeBenchmark('Pagination', 20, (index) => ); + +smokeBenchmark('Paper', 500, (index) => ( + + Paper {index} + +)); + +smokeBenchmark('Radio', 20, (index) => ( + + {OPTIONS.slice(0, 3).map((option) => ( + } label={option} /> + ))} + +)); + +smokeBenchmark('Rating', 40, (index) => ); + +smokeBenchmark('Select', 20, (index) => ( + +)); + +smokeBenchmark('Slider', 80, (index) => ); + +smokeBenchmark('Stepper', 40, (index) => ( + + {STEPS.map((step) => ( + + {step} + + ))} + +)); + +smokeBenchmark('Switch', 100, (index) => ); + +smokeBenchmark('Tabs', 30, (index) => ( + + + + + +)); + +smokeBenchmark('TextField', 60, (index) => ); + +smokeBenchmark('ToggleButtonGroup', 40, (index) => ( + + {OPTIONS.slice(0, 3).map((option) => ( + + {option} + + ))} + +)); + +smokeBenchmark( + 'Tooltip', + 70, + (index) => ( + + {index % 10} + + ), + INERT_TO_POINTER, +); + +smokeBenchmark('Typography', 800, (index) => ( + + Text {index} + +)); diff --git a/test/benchmark/src/textField/textField.bench.tsx b/test/benchmark/src/textField/textField.bench.tsx new file mode 100644 index 00000000000000..864569c4df90e4 --- /dev/null +++ b/test/benchmark/src/textField/textField.bench.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import { reactBenchmark } from '@mui/internal-benchmark/page'; +import TextField from '@mui/material/TextField'; + +function Form({ count }: { count: number }) { + return ( +
    + {Array.from({ length: count }, (_, index) => ( + + ))} +
    + ); +} + +reactBenchmark('TextField mount', () =>
    ); diff --git a/test/benchmark/src/tooltip/tooltip.bench.tsx b/test/benchmark/src/tooltip/tooltip.bench.tsx new file mode 100644 index 00000000000000..557f7a72777aba --- /dev/null +++ b/test/benchmark/src/tooltip/tooltip.bench.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; +import { reactBenchmark } from '@mui/internal-benchmark/page'; +import IconButton from '@mui/material/IconButton'; +import Tooltip from '@mui/material/Tooltip'; + +// A page with an icon action on every row, each explaining itself in a tooltip. +function Actions({ count }: { count: number }) { + return ( +
    + {Array.from({ length: count }, (_, index) => ( + + {index % 10} + + ))} +
    + ); +} + +reactBenchmark('Tooltip mount', () => ); diff --git a/test/benchmark/tsconfig.json b/test/benchmark/tsconfig.json new file mode 100644 index 00000000000000..523cd3c541d174 --- /dev/null +++ b/test/benchmark/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "noEmit": true + }, + "include": ["src/**/*"], + "exclude": ["src/__bench__"] +} diff --git a/test/benchmark/vite.config.mjs b/test/benchmark/vite.config.mjs new file mode 100644 index 00000000000000..8f03b329ef01f7 --- /dev/null +++ b/test/benchmark/vite.config.mjs @@ -0,0 +1,4 @@ +import { defineConfig } from 'vite'; +import { benchmarkPlugin } from '@mui/internal-benchmark/vitePlugin'; + +export default defineConfig({ plugins: [benchmarkPlugin()] });