You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js+Material UI响应式抽屉:页面切换Logo重绘问题求助

Next.js + Material UI 侧边栏Logo页面切换时短暂消失问题

问题描述

在Next.js项目中使用Material UI响应式抽屉,点击侧边栏链接切换页面时,顶部Logo会短暂消失后重新显示,但下方的kitten图片无此问题。相关实现代码如下:

import ListAltIcon from "@mui/icons-material/ListAlt";
import SupervisorAccountIcon from "@mui/icons-material/SupervisorAccount";
import Box from "@mui/material/Box";
import Divider from "@mui/material/Divider";
import Drawer from "@mui/material/Drawer";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import Toolbar from "@mui/material/Toolbar";

import Image from "next/image";
import logo from "../../../assets/logo.svg";
import kitten from "../../../assets/300.jpeg";

import { pages } from "../../../constants";

import StyledLink from "./sidebar.styled";

interface Props {
  handleDrawerToggle?: () => void;
  mobileOpen: boolean;
  drawerWidth: number;
}

const SidebarInner = () => (
  <div>
    <Toolbar>
      <Image
        src={logo}
        alt=" logo"
        width="120"
        height="64"
        className="logo-image"
        style={{ margin: "auto" }}
        priority={true}
      />
    </Toolbar>
    <Divider />
    <List sx={{ textDecoration: "none" }}>
      {Object.values(pages).map(({ name, url }, index) => (
        <ListItem key={url} disablePadding>
          <StyledLink href={url}>
            <ListItemButton>
              <ListItemIcon>
                {index % 2 === 0 ? <ListAltIcon /> : <SupervisorAccountIcon />}
              </ListItemIcon>
              <ListItemText primary={name} />
            </ListItemButton>
          </StyledLink>
        </ListItem>
      ))}
    </List>
    <Image src={kitten} alt="test image" style={{ margin: "auto" }} />
  </div>
);

export default function Sidebar(props: Props) {
  const { drawerWidth, handleDrawerToggle, mobileOpen } = props;

  return (
    <Box sx={{ display: "flex" }}>
      <Box
        component="nav"
        sx={{ width: { sm: drawerWidth }, flexShrink: { sm: 0 } }}
        aria-label="mailbox folders"
      >
        <Drawer
          variant="temporary"
          open={mobileOpen}
          onClose={handleDrawerToggle}
          ModalProps={{
            keepMounted: true,
          }}
          sx={{
            display: { xs: "block", sm: "none" },
            "& .MuiDrawer-paper": {
              boxSizing: "border-box",
              width: drawerWidth,
            },
          }}
        >
          <SidebarInner />
        </Drawer>
        <Drawer
          variant="permanent"
          sx={{
            display: { xs: "none", sm: "block" },
            "& .MuiDrawer-paper": {
              boxSizing: "border-box",
              width: drawerWidth,
            },
          }}
          open
        >
          <SidebarInner />
        </Drawer>
      </Box>
    </Box>
  );
}

问题分析

1. Logo与kitten图片表现差异的原因

  • 宽高属性类型不匹配:Logo的<Image>组件使用了字符串类型的width="120"和height="64",而Next.js的Image组件要求这两个属性为数字类型。类型不匹配会触发组件内部额外的布局计算,在页面切换时导致Logo重新渲染甚至重新加载;而kitten图片未指定宽高,采用了组件默认的自适应逻辑,没有触发额外的重绘逻辑。
  • 图片格式处理差异:Logo是SVG矢量图,Next.js对SVG的处理逻辑与JPEG位图不同。SVG作为代码式资源,在组件重新渲染时可能被重新解析,而JPEG会被缓存为二进制资源,页面切换时直接读取缓存,不会出现消失现象。
  • 重复渲染问题:SidebarInner被两个Drawer组件(移动端临时抽屉、桌面端永久抽屉)同时引用。页面切换时,响应式布局的状态变化可能导致两个抽屉的显示状态切换,触发SidebarInner两次渲染,Logo因此被重复初始化,而kitten图片因缓存机制未受影响。

修复方案

方案1:修正Image组件宽高属性类型

将Logo的width和height改为数字类型,符合Next.js Image组件的要求,减少不必要的布局计算:

<Image
  src={logo}
  alt="logo"
  width={120} // 改为数字类型
  height={64} // 改为数字类型
  className="logo-image"
  style={{ margin: "auto" }}
  priority={true}
/>

方案2:合并Drawer渲染逻辑,避免重复渲染SidebarInner

将两个抽屉合并为一个,根据屏幕尺寸动态切换variant,确保SidebarInner只渲染一次:

import { useMediaQuery, Theme } from "@mui/material/styles";

export default function Sidebar(props: Props) {
  const { drawerWidth, handleDrawerToggle, mobileOpen } = props;
  const isMobile = useMediaQuery((theme: Theme) => theme.breakpoints.down("sm"));

  return (
    <Box sx={{ display: "flex" }}>
      <Box
        component="nav"
        sx={{ width: { sm: drawerWidth }, flexShrink: { sm: 0 } }}
        aria-label="mailbox folders"
      >
        <Drawer
          variant={isMobile ? "temporary" : "permanent"}
          open={isMobile ? mobileOpen : true}
          onClose={handleDrawerToggle}
          ModalProps={{
            keepMounted: true,
          }}
          sx={{
            "& .MuiDrawer-paper": {
              boxSizing: "border-box",
              width: drawerWidth,
            },
          }}
        >
          <SidebarInner />
        </Drawer>
      </Box>
    </Box>
  );
}

方案3:用React.memo包裹SidebarInner,阻止不必要重渲染

通过React.memo缓存SidebarInner组件,仅当依赖变化时才重新渲染:

import React from "react";

const SidebarInner = React.memo(() => (
  // 原SidebarInner内容保持不变
));

内容的提问来源于stack exchange,提问作者Pikk

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 20:48:19