Material-UI 性能优化 #

概述 #

性能优化是构建高质量应用的重要环节。本章节介绍 MUI 应用的各种性能优化技巧。

包大小优化 #

Tree Shaking #

MUI 支持 Tree Shaking,只打包使用的组件:

jsx
import { Button, TextField } from '@mui/material';

按需导入图标 #

jsx
import DeleteIcon from '@mui/icons-material/Delete';
import AddIcon from '@mui/icons-material/Add';

使用 Babel 插件 #

javascript
// babel.config.js
module.exports = {
  plugins: [
    [
      'babel-plugin-import',
      {
        libraryName: '@mui/material',
        libraryDirectory: '',
        camel2DashComponentName: false,
      },
      'core',
    ],
    [
      'babel-plugin-import',
      {
        libraryName: '@mui/icons-material',
        libraryDirectory: '',
        camel2DashComponentName: false,
      },
      'icons',
    ],
  ],
};

渲染优化 #

使用 React.memo #

jsx
import { memo } from 'react';

const UserCard = memo(function UserCard({ user }) {
  return (
    <Card>
      <CardContent>
        <Typography>{user.name}</Typography>
      </CardContent>
    </Card>
  );
});

避免内联样式 #

jsx
const styles = {
  container: {
    p: 2,
    bgcolor: 'primary.main',
  },
};

function MyComponent() {
  return <Box sx={styles.container}>内容</Box>;
}

使用 useMemo #

jsx
import { useMemo } from 'react';

function UserList({ users, filter }) {
  const filteredUsers = useMemo(() => {
    return users.filter((user) => user.name.includes(filter));
  }, [users, filter]);

  return (
    <List>
      {filteredUsers.map((user) => (
        <ListItem key={user.id}>{user.name}</ListItem>
      ))}
    </List>
  );
}

使用 useCallback #

jsx
import { useCallback } from 'react';

function UserList({ users }) {
  const handleClick = useCallback((userId) => {
    console.log('User clicked:', userId);
  }, []);

  return (
    <List>
      {users.map((user) => (
        <ListItemButton key={user.id} onClick={() => handleClick(user.id)}>
          {user.name}
        </ListItemButton>
      ))}
    </List>
  );
}

样式性能 #

使用 styled 代替 sx #

对于频繁渲染的组件,使用 styled 更高效:

jsx
import { styled } from '@mui/material/styles';

const StyledCard = styled(Card)(({ theme }) => ({
  padding: theme.spacing(2),
  borderRadius: theme.shape.borderRadius * 2,
}));

function MyComponent() {
  return <StyledCard>内容</StyledCard>;
}

缓存样式 #

jsx
import { styled } from '@mui/material/styles';

const useStyles = styled('div')(({ theme }) => ({
  padding: theme.spacing(2),
  backgroundColor: theme.palette.background.paper,
}));

列表优化 #

虚拟化长列表 #

jsx
import { FixedSizeList } from 'react-window';

function VirtualizedList({ items }) {
  const Row = ({ index, style }) => (
    <ListItem style={style}>
      <ListItemText primary={items[index].name} />
    </ListItem>
  );

  return (
    <FixedSizeList
      height={400}
      width="100%"
      itemCount={items.length}
      itemSize={46}
    >
      {Row}
    </FixedSizeList>
  );
}

分页加载 #

jsx
import { useState } from 'react';

function PaginatedList({ items }) {
  const [page, setPage] = useState(0);
  const itemsPerPage = 10;

  const displayedItems = items.slice(
    page * itemsPerPage,
    (page + 1) * itemsPerPage
  );

  return (
    <>
      <List>
        {displayedItems.map((item) => (
          <ListItem key={item.id}>{item.name}</ListItem>
        ))}
      </List>
      <Button onClick={() => setPage(page + 1)}>加载更多</Button>
    </>
  );
}

图片优化 #

懒加载图片 #

jsx
import { useState, useEffect, useRef } from 'react';

function LazyImage({ src, alt, ...props }) {
  const [isLoaded, setIsLoaded] = useState(false);
  const imgRef = useRef();

  useEffect(() => {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          setIsLoaded(true);
          observer.disconnect();
        }
      });
    });

    if (imgRef.current) {
      observer.observe(imgRef.current);
    }

    return () => observer.disconnect();
  }, []);

  return (
    <Box ref={imgRef}>
      {isLoaded ? (
        <Box component="img" src={src} alt={alt} {...props} />
      ) : (
        <Skeleton variant="rectangular" width="100%" height={200} />
      )}
    </Box>
  );
}

响应式图片 #

jsx
function ResponsiveImage({ src, alt }) {
  return (
    <Box
      component="img"
      src={src}
      alt={alt}
      sx={{
        width: '100%',
        height: 'auto',
        maxWidth: { xs: '100%', md: '50%' },
      }}
    />
  );
}

代码分割 #

路由级分割 #

jsx
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { CircularProgress } from '@mui/material';

const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));

function App() {
  return (
    <BrowserRouter>
      <Suspense
        fallback={
          <Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}>
            <CircularProgress />
          </Box>
        }
      >
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/contact" element={<Contact />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

组件级分割 #

jsx
import { lazy, Suspense } from 'react';

const HeavyChart = lazy(() => import('./HeavyChart'));

function Dashboard() {
  return (
    <Box>
      <Typography variant="h4">仪表板</Typography>
      <Suspense fallback={<Skeleton variant="rectangular" height={300} />}>
        <HeavyChart />
      </Suspense>
    </Box>
  );
}

主题优化 #

避免重复创建主题 #

jsx
import { useMemo } from 'react';
import { createTheme, ThemeProvider } from '@mui/material/styles';

function App() {
  const theme = useMemo(
    () =>
      createTheme({
        palette: {
          primary: { main: '#1976d2' },
        },
      }),
    []
  );

  return (
    <ThemeProvider theme={theme}>
      <MyApp />
    </ThemeProvider>
  );
}

使用 CSS 变量 #

jsx
const theme = createTheme({
  cssVariables: true,
});

状态管理优化 #

避免不必要的状态更新 #

jsx
import { useState, useCallback } from 'react';

function SearchInput({ onSearch }) {
  const [value, setValue] = useState('');

  const debouncedSearch = useCallback(
    debounce((searchValue) => {
      onSearch(searchValue);
    }, 300),
    [onSearch]
  );

  const handleChange = (e) => {
    const newValue = e.target.value;
    setValue(newValue);
    debouncedSearch(newValue);
  };

  return (
    <TextField
      value={value}
      onChange={handleChange}
      placeholder="搜索..."
    />
  );
}

使用 Context 优化 #

jsx
import { createContext, useContext, useMemo } from 'react';

const UserContext = createContext();

function UserProvider({ children }) {
  const [user, setUser] = useState(null);

  const value = useMemo(() => ({ user, setUser }), [user]);

  return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}

function useUser() {
  return useContext(UserContext);
}

性能监控 #

React DevTools Profiler #

jsx
import { Profiler } from 'react';

function onRenderCallback(
  id,
  phase,
  actualDuration,
  baseDuration,
  startTime,
  commitTime
) {
  console.log(`${id} ${phase} took ${actualDuration}ms`);
}

function App() {
  return (
    <Profiler id="App" onRender={onRenderCallback}>
      <MyComponent />
    </Profiler>
  );
}

使用 useTransition #

jsx
import { useState, useTransition } from 'react';

function SearchResults() {
  const [isPending, startTransition] = useTransition();
  const [results, setResults] = useState([]);

  const handleSearch = (query) => {
    startTransition(() => {
      const filtered = heavyFiltering(query);
      setResults(filtered);
    });
  };

  return (
    <Box>
      <TextField onChange={(e) => handleSearch(e.target.value)} />
      {isPending && <CircularProgress />}
      <List>
        {results.map((item) => (
          <ListItem key={item.id}>{item.name}</ListItem>
        ))}
      </List>
    </Box>
  );
}

最佳实践总结 #

优化项 方法
包大小 Tree Shaking、按需导入
渲染 memo、useMemo、useCallback
样式 styled 代替 sx、缓存样式
列表 虚拟化、分页
图片 懒加载、响应式
代码 代码分割、懒加载
主题 避免重复创建、CSS 变量

下一步 #

继续学习 最佳实践,了解企业级开发的规范和技巧!

最后更新:2026-03-28