Created
March 14, 2025 21:42
-
-
Save NoahCardoza/4897e2d49de5998f032df289df4844e5 to your computer and use it in GitHub Desktop.
tss/makeStyles vs MUI sx prop
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useState, useEffect } from 'react'; | |
import { createRoot } from 'react-dom/client'; | |
import { Box, Button, Typography, Paper } from '@mui/material'; | |
import { makeStyles } from 'tss-react/mui'; | |
import { Profiler } from 'react'; | |
import { sum } from 'lodash'; | |
// Number of components to render in each test | |
const COMPONENT_COUNT = 1000; | |
// Number of update cycles to run | |
const UPDATE_CYCLES = 500; | |
// Number of warm-up cycles before measuring | |
const WARMUP_CYCLES = 50; | |
// Performance metrics storage | |
const metrics = { | |
tss: { | |
renderTimes: [], | |
memoryUsage: [], | |
isWarmup: true, | |
}, | |
sx: { | |
renderTimes: [], | |
memoryUsage: [], | |
isWarmup: true, | |
} | |
}; | |
// TSS make styles implementation | |
const useStyles = makeStyles()((theme) => ({ | |
root: { | |
display: 'flex', | |
flexDirection: 'column', | |
padding: theme.spacing(2), | |
margin: theme.spacing(1), | |
backgroundColor: theme.palette.background.paper, | |
borderRadius: theme.shape.borderRadius, | |
boxShadow: theme.shadows[1], | |
'&:hover': { | |
boxShadow: theme.shadows[3], | |
}, | |
}, | |
title: { | |
color: theme.palette.primary.main, | |
fontWeight: 'bold', | |
marginBottom: theme.spacing(1), | |
}, | |
content: { | |
color: theme.palette.text.secondary, | |
}, | |
})); | |
// Component using TSS make styles | |
const TSSStyledComponent = ({ index, updateCounter }) => { | |
const { classes } = useStyles(); | |
return ( | |
<Paper className={classes.root}> | |
<Typography className={classes.title}> | |
TSS Item #{index} (Update: {updateCounter}) | |
</Typography> | |
<Typography className={classes.content}> | |
This component uses TSS make styles for styling | |
</Typography> | |
</Paper> | |
); | |
}; | |
// Component using sx prop | |
const SXStyledComponent = ({ index, updateCounter }) => { | |
return ( | |
<Paper | |
sx={{ | |
display: 'flex', | |
flexDirection: 'column', | |
padding: 2, | |
margin: 1, | |
bgcolor: 'background.paper', | |
borderRadius: 'shape.borderRadius', | |
boxShadow: 1, | |
'&:hover': { | |
boxShadow: 3, | |
}, | |
}} | |
> | |
<Typography | |
sx={{ | |
color: 'primary.main', | |
fontWeight: 'bold', | |
mb: 1, | |
}} | |
> | |
SX Item #{index} (Update: {updateCounter}) | |
</Typography> | |
<Typography | |
sx={{ | |
color: 'text.secondary', | |
}} | |
> | |
This component uses the sx prop for styling | |
</Typography> | |
</Paper> | |
); | |
}; | |
// Test container for TSS components | |
const TSSComponentsTest = ({ updateCounter }) => { | |
return ( | |
<Profiler id="tss-components" onRender={onRenderCallback}> | |
<Box> | |
{Array.from({ length: COMPONENT_COUNT }).map((_, index) => ( | |
<TSSStyledComponent | |
key={index} | |
index={index} | |
updateCounter={updateCounter} | |
/> | |
))} | |
</Box> | |
</Profiler> | |
); | |
}; | |
// Test container for SX components | |
const SXComponentsTest = ({ updateCounter }) => { | |
return ( | |
<Profiler id="sx-components" onRender={onRenderCallback}> | |
<Box> | |
{Array.from({ length: COMPONENT_COUNT }).map((_, index) => ( | |
<SXStyledComponent | |
key={index} | |
index={index} | |
updateCounter={updateCounter} | |
/> | |
))} | |
</Box> | |
</Profiler> | |
); | |
}; | |
// Statistical helper functions | |
const calculateMedian = (values) => { | |
const sorted = [...values].sort((a, b) => a - b); | |
const middle = Math.floor(sorted.length / 2); | |
return sorted.length % 2 === 0 | |
? (sorted[middle - 1] + sorted[middle]) / 2 | |
: sorted[middle]; | |
}; | |
const calculateStdDev = (values, mean) => { | |
if (values.length <= 1) return 0; | |
const variance = values.reduce((acc, val) => acc + Math.pow(val - mean, 2), 0) / (values.length - 1); | |
return Math.sqrt(variance); | |
}; | |
// TSS make styles implementation and component definitions remain the same | |
// ...existing code... | |
// Modified Profiler callback to handle warm-up phase | |
function onRenderCallback( | |
id, | |
phase, | |
actualDuration, | |
baseDuration, | |
startTime, | |
commitTime | |
) { | |
// Skip recording metrics during warmup phase | |
if ((id === 'tss-components' && metrics.tss.isWarmup) || | |
(id === 'sx-components' && metrics.sx.isWarmup)) { | |
return; | |
} | |
if (id === 'tss-components') { | |
metrics.tss.renderTimes.push(actualDuration); | |
} else if (id === 'sx-components') { | |
metrics.sx.renderTimes.push(actualDuration); | |
} | |
// Collect memory usage if possible | |
if (window.performance && window.performance.memory) { | |
const memory = window.performance.memory.usedJSHeapSize / (1024 * 1024); | |
if (id === 'tss-components') { | |
metrics.tss.memoryUsage.push(memory); | |
} else if (id === 'sx-components') { | |
metrics.sx.memoryUsage.push(memory); | |
} | |
} | |
} | |
// Calculate and format results with added statistics | |
const calculateResults = (setResults) => { | |
// Average calculations | |
const tssAvgRenderTime = metrics.tss.renderTimes.reduce((a, b) => a + b, 0) / metrics.tss.renderTimes.length; | |
const sxAvgRenderTime = metrics.sx.renderTimes.reduce((a, b) => a + b, 0) / metrics.sx.renderTimes.length; | |
// Min/Max calculations | |
const tssMaxRenderTime = Math.max(...metrics.tss.renderTimes); | |
const sxMaxRenderTime = Math.max(...metrics.sx.renderTimes); | |
const tssMinRenderTime = Math.min(...metrics.tss.renderTimes); | |
const sxMinRenderTime = Math.min(...metrics.sx.renderTimes); | |
// Median calculations | |
const tssMedianRenderTime = calculateMedian(metrics.tss.renderTimes); | |
const sxMedianRenderTime = calculateMedian(metrics.sx.renderTimes); | |
// Standard deviation calculations | |
const tssStdDevRenderTime = calculateStdDev(metrics.tss.renderTimes, tssAvgRenderTime); | |
const sxStdDevRenderTime = calculateStdDev(metrics.sx.renderTimes, sxAvgRenderTime); | |
// Memory calculations | |
let tssAvgMemory = 0; | |
let sxAvgMemory = 0; | |
if (metrics.tss.memoryUsage.length > 0 && metrics.sx.memoryUsage.length > 0) { | |
tssAvgMemory = (sum(metrics.tss.memoryUsage) / metrics.tss.memoryUsage.length) | |
sxAvgMemory = (sum(metrics.sx.memoryUsage) / metrics.sx.memoryUsage.length) | |
} | |
// Percentage differences (fixed maxPercentDifference calculation) | |
const minPercentDifference = ((sxMinRenderTime - tssMinRenderTime) / sxMinRenderTime * 100).toFixed(2); | |
const averagePercentDifference = ((sxAvgRenderTime - tssAvgRenderTime) / sxAvgRenderTime * 100).toFixed(2); | |
const maxPercentDifference = ((sxMaxRenderTime - tssMaxRenderTime) / sxMaxRenderTime * 100).toFixed(2); | |
const medianPercentDifference = ((sxMedianRenderTime - tssMedianRenderTime) / sxMedianRenderTime * 100).toFixed(2); | |
const memoryPercentDifference = ((sxAvgMemory - tssAvgMemory) / sxAvgMemory * 100).toFixed(2); | |
setResults({ | |
tss: { | |
avgRenderTime: tssAvgRenderTime.toFixed(2), | |
maxRenderTime: tssMaxRenderTime.toFixed(2), | |
minRenderTime: tssMinRenderTime.toFixed(2), | |
medianRenderTime: tssMedianRenderTime.toFixed(2), | |
stdDevRenderTime: tssStdDevRenderTime.toFixed(2), | |
avgMemory: tssAvgMemory.toFixed(2), | |
}, | |
sx: { | |
avgRenderTime: sxAvgRenderTime.toFixed(2), | |
maxRenderTime: sxMaxRenderTime.toFixed(2), | |
minRenderTime: sxMinRenderTime.toFixed(2), | |
medianRenderTime: sxMedianRenderTime.toFixed(2), | |
stdDevRenderTime: sxStdDevRenderTime.toFixed(2), | |
avgMemory: sxAvgMemory.toFixed(2), | |
}, | |
comparison: { | |
minPercentDifference, | |
averagePercentDifference, | |
maxPercentDifference, | |
medianPercentDifference, | |
memoryPercentDifference, | |
} | |
}); | |
}; | |
// Benchmark App | |
const BenchmarkApp = () => { | |
const [testState, setTestState] = useState('idle'); | |
// States: idle, warmup-tss, warmup-sx, running-tss, running-sx, complete | |
const [updateCounter, setUpdateCounter] = useState(0); | |
const [results, setResults] = useState(null); | |
const [sxFirst] = useState(() => Math.random() >= 0.5); // Randomize test order | |
// Run update cycles | |
useEffect(() => { | |
let updateInterval; | |
let cycleCount = 0; | |
const isWarmupPhase = testState === 'warmup-tss' || testState === 'warmup-sx'; | |
const isRunningPhase = testState === 'running-tss' || testState === 'running-sx'; | |
if (isWarmupPhase || isRunningPhase) { | |
const maxCycles = isWarmupPhase ? WARMUP_CYCLES : UPDATE_CYCLES; | |
updateInterval = setInterval(() => { | |
setUpdateCounter(prev => prev + 1); | |
cycleCount++; | |
if (cycleCount >= maxCycles) { | |
clearInterval(updateInterval); | |
if (testState === 'warmup-tss') { | |
// Mark warmup complete and start collecting real metrics | |
metrics.tss.isWarmup = false; | |
setUpdateCounter(0); | |
setTestState('running-tss'); | |
} | |
else if (testState === 'warmup-sx') { | |
// Mark warmup complete and start collecting real metrics | |
metrics.sx.isWarmup = false; | |
setUpdateCounter(0); | |
setTestState('running-sx'); | |
} | |
else if (testState === 'running-tss') { | |
// Move to the next test | |
setTimeout(() => { | |
if (sxFirst) { | |
setTestState('complete'); | |
calculateResults(setResults); | |
} else { | |
metrics.sx.isWarmup = true; | |
setUpdateCounter(0); | |
setTestState('warmup-sx'); | |
} | |
}, 1000); | |
} | |
else if (testState === 'running-sx') { | |
setTimeout(() => { | |
if (sxFirst) { | |
metrics.tss.isWarmup = true; | |
setUpdateCounter(0); | |
setTestState('warmup-tss'); | |
} else { | |
setTestState('complete'); | |
calculateResults(setResults); | |
} | |
}, 1000); | |
} | |
} | |
}, 50); // Update every 50ms | |
} | |
return () => { | |
if (updateInterval) clearInterval(updateInterval); | |
}; | |
}, [testState]); | |
const startBenchmark = () => { | |
// Reset metrics | |
metrics.tss.renderTimes = []; | |
metrics.tss.memoryUsage = []; | |
metrics.sx.renderTimes = []; | |
metrics.sx.memoryUsage = []; | |
metrics.tss.isWarmup = true; | |
metrics.sx.isWarmup = true; | |
setUpdateCounter(0); | |
setResults(null); | |
// Start with either TSS or SX based on randomization | |
const firstTest = sxFirst ? 'warmup-sx' : 'warmup-tss'; | |
setTestState(firstTest); | |
}; | |
return ( | |
<Box sx={{ p: 4, maxWidth: 800, mx: 'auto' }}> | |
<Typography variant="h4" gutterBottom> | |
MUI Styling Benchmark: sx prop vs. TSS make styles | |
</Typography> | |
<Typography variant="body1" paragraph> | |
This benchmark renders {COMPONENT_COUNT} components and updates them {UPDATE_CYCLES} times to measure performance differences. | |
{sxFirst ? " Running SX first, then TSS." : " Running TSS first, then SX."} | |
</Typography> | |
{(testState === 'idle' || testState === 'complete') && ( | |
<Button | |
variant="contained" | |
color="primary" | |
onClick={startBenchmark} | |
sx={{ mb: 4 }} | |
> | |
Start Benchmark | |
</Button> | |
)} | |
{testState === 'warmup-tss' && ( | |
<> | |
<Typography variant="h6">Warming up TSS make styles...</Typography> | |
<Typography variant="body2">Warm-up cycle: {updateCounter}/{WARMUP_CYCLES}</Typography> | |
<TSSComponentsTest updateCounter={updateCounter} /> | |
</> | |
)} | |
{testState === 'running-tss' && ( | |
<> | |
<Typography variant="h6">Running TSS make styles test...</Typography> | |
<Typography variant="body2">Update cycle: {updateCounter}/{UPDATE_CYCLES}</Typography> | |
<TSSComponentsTest updateCounter={updateCounter} /> | |
</> | |
)} | |
{testState === 'warmup-sx' && ( | |
<> | |
<Typography variant="h6">Warming up SX prop...</Typography> | |
<Typography variant="body2">Warm-up cycle: {updateCounter}/{WARMUP_CYCLES}</Typography> | |
<SXComponentsTest updateCounter={updateCounter} /> | |
</> | |
)} | |
{testState === 'running-sx' && ( | |
<> | |
<Typography variant="h6">Running SX prop test...</Typography> | |
<Typography variant="body2">Update cycle: {updateCounter}/{UPDATE_CYCLES}</Typography> | |
<SXComponentsTest updateCounter={updateCounter} /> | |
</> | |
)} | |
{results && ( | |
<Paper sx={{ p: 3, mt: 3 }}> | |
<Typography variant="h5" gutterBottom>Results</Typography> | |
<Box sx={{ display: 'flex', gap: 4, mb: 3 }}> | |
<Box> | |
<Typography variant="h6">TSS make styles</Typography> | |
<Typography>Min Render Time: {results.tss.minRenderTime} ms</Typography> | |
<Typography>Median Render Time: {results.tss.medianRenderTime} ms</Typography> | |
<Typography>Avg Render Time: {results.tss.avgRenderTime} ms</Typography> | |
<Typography>Max Render Time: {results.tss.maxRenderTime} ms</Typography> | |
<Typography>Std Dev: {results.tss.stdDevRenderTime} ms</Typography> | |
<Typography>Avg Memory Usage: {results.tss.avgMemory} MB</Typography> | |
</Box> | |
<Box> | |
<Typography variant="h6">SX prop</Typography> | |
<Typography>Min Render Time: {results.sx.minRenderTime} ms</Typography> | |
<Typography>Median Render Time: {results.sx.medianRenderTime} ms</Typography> | |
<Typography>Avg Render Time: {results.sx.avgRenderTime} ms</Typography> | |
<Typography>Max Render Time: {results.sx.maxRenderTime} ms</Typography> | |
<Typography>Std Dev: {results.sx.stdDevRenderTime} ms</Typography> | |
<Typography>Avg Memory Usage: {results.sx.avgMemory} MB</Typography> | |
</Box> | |
</Box> | |
<Typography variant="h6">Comparing TSS/makeStyles vs MUI's SX</Typography> | |
<Typography> | |
Min: {results.comparison.minPercentDifference}% | |
</Typography> | |
<Typography> | |
Median: {results.comparison.medianPercentDifference}% | |
</Typography> | |
<Typography> | |
Average: {results.comparison.averagePercentDifference}% | |
</Typography> | |
<Typography> | |
Max: {results.comparison.maxPercentDifference}% | |
</Typography> | |
<Typography> | |
Memory: {results.comparison.memoryPercentDifference}% | |
</Typography> | |
<Typography variant="body2" color="textSecondary" sx={{ mt: 2 }}> | |
Note: Memory usage may not be accurate in all browsers. Positive percentages indicate TSS is faster/uses less memory. | |
</Typography> | |
</Paper> | |
)} | |
</Box> | |
); | |
}; | |
// Render the benchmark app | |
const root = createRoot(document.getElementById('app')); | |
root.render(<BenchmarkApp />); | |
// Export a function to run the benchmark programmatically | |
export const runBenchmark = () => { | |
const app = document.createElement('div'); | |
app.id = 'benchmark-root'; | |
document.body.appendChild(app); | |
const benchmarkRoot = createRoot(app); | |
benchmarkRoot.render(<BenchmarkApp />); | |
return { | |
cleanup: () => { | |
benchmarkRoot.unmount(); | |
app.remove(); | |
} | |
}; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Results from a incognito Brave Browser window with no extensions enabled.