如何降低NextJS+Mantine站点的CLS以优化性能?
问题
使用NextJS搭配Mantine框架开发站点,采用createStyles添加样式,尽可能复用Mantine组件。部署到Vercel后,除CLS(累积布局偏移)外其余性能指标表现良好,但CLS数值极差,首次加载时能明显感受到内容偏移。
Header组件示例代码
import { useState } from 'react'; import { createStyles, Container, Anchor, Group, ActionIcon, useMantineColorScheme } from '@mantine/core'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; import Sun from '@/public/svg/sun.svg'; import MoonStars from '@/public/svg/moonStars.svg'; const useStyles = createStyles((theme) => ({ inner: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', }, burger: { [theme.fn.largerThan('sm')]: { display: 'none', } }, links: { fontWeight: 700, textTransform: 'uppercase', textDecoration: 'none', fontSize: theme.fontSizes.md, }, mainLink: { color: theme.colorScheme === 'dark' ? theme.colors.dark[1] : theme.colors.gray[6], borderBottom: `2px solid transparent`, transition: 'border-color 100ms ease, color 100ms ease', '&:hover': { color: theme.colorScheme === 'dark' ? theme.white : theme.black, textDecoration: 'none', }, }, mainLinkActive: { color: theme.colorScheme === 'dark' ? theme.white : theme.black, borderBottomColor: theme.colors[theme.primaryColor][theme.colorScheme === 'dark' ? 5 : 6], }, sideText: { color: theme.colorScheme === 'dark' ? theme.white : theme.black, } })); export default function Component() { const { colorScheme, toggleColorScheme } = useMantineColorScheme(); const { classes, cx } = useStyles(); let pathname = usePathname(); const [active, setActive] = useState(() => { if (pathname.split('/')[1] === '') return 0; if (pathname.split('/')[1] === 'blog') return 1; }); let mainLinks = [ { "link": "/", "label": "About" }, { "link": "/blog", "label": "Blog" }, ] const mainItems = mainLinks.map((item, index) => ( <Anchor component={Link} href={item.link} key={item.label} py='xl' px='md' className={cx(classes.mainLink, { [classes.mainLinkActive]: index === active }, classes.links)} onClick={() => setActive(index)} > {item.label} </Anchor> )); const changeColorScheme = () => { toggleColorScheme(); localStorage.setItem('color-scheme', colorScheme === 'dark' ? 'light' : 'dark'); } return ( <Container pos='relative' mb='2rem'> <Container pos='sticky' className={classes.inner}> <div className={classes.links}> <Group spacing={0} > {mainItems} </Group> </div> <Group position="center" my="xl"> <ActionIcon onClick={() => changeColorScheme()} size="lg" sx={(theme) => ({ backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0], color: theme.colorScheme === 'dark' ? theme.colors.yellow[4] : theme.colors.blue[6], })}> {colorScheme === 'dark' ? <Sun /> : <MoonStars />} </ActionIcon> </Group> {/* <Burger opened={opened} onClick={toggle} className={classes.burger} size="sm" /> */} </Container> </Container> ); }
猜测原因:Mantine将组件转换为JS导致过多代码加载,引发动态内容偏移,考虑改用原生HTML组件,但需要确认具体原因。
原因分析
- 客户端样式注入延迟:Mantine的
createStyles和组件样式默认在客户端JS执行后才注入,首屏加载时浏览器先渲染无样式DOM,待JS加载完成后才应用样式,直接导致布局偏移。 - Hydration内容不匹配:用
useState初始化导航激活状态时,客户端hydration阶段的计算结果可能与服务端初始渲染内容不一致,导致导航链接的活跃样式(如下边框)动态变化,触发布局偏移。 - 容器尺寸未预定义:Header中的
Container组件未设置固定/最小高度,样式加载后元素的padding、margin生效,导致容器高度变化,带动下方内容偏移。 - SVG图标无固定尺寸:主题切换的SVG图标未声明宽高,首次渲染时图标加载完成后尺寸变化,引发局部布局偏移。
优化解决方法
1. 启用Mantine服务端样式注入
在Next.js App Router的根布局中配置Mantine SSR,确保样式在服务端就注入到HTML,避免客户端样式延迟:
import { MantineProvider, ColorSchemeScript } from '@mantine/core'; import './globals.css'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <head> <ColorSchemeScript defaultColorScheme="light" /> {/* 其他meta标签 */} </head> <body> <MantineProvider withGlobalStyles withNormalizeCSS> {children} </MantineProvider> </body> </html> ); }
2. 修复导航激活状态的Hydration差异
将激活状态的计算逻辑改为在客户端挂载后执行,避免服务端与客户端渲染内容不一致:
import { useEffect, useState } from 'react'; // ... 其他代码 const [active, setActive] = useState(0); useEffect(() => { const currentActive = pathname.split('/')[1] === '' ? 0 : pathname.split('/')[1] === 'blog' ? 1 : 0; setActive(currentActive); }, [pathname]);
3. 为Header容器设置固定高度
在useStyles的inner类中添加最小高度,避免样式加载后容器高度变化:
inner: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', minHeight: '64px', // 根据实际内容调整 },
4. 为SVG图标指定固定尺寸
直接在SVG组件上声明宽高:
{colorScheme === 'dark' ? <Sun width={24} height={24} /> : <MoonStars width={24} height={24} /> }
5. 拆分客户端与服务端逻辑
将Header中不需要交互的部分改为服务端组件,减少客户端JS加载量,避免不必要的动态渲染:
- 将导航链接的渲染逻辑提取为服务端组件,仅保留主题切换的客户端逻辑。
6. 使用Next.js Image优化SVG加载
改用next/image组件渲染SVG,提前声明尺寸:
import Image from 'next/image'; // ... 其他代码 {colorScheme === 'dark' ? <Image src={Sun} width={24} height={24} alt="Sun icon" /> : <Image src={MoonStars} width={24} height={24} alt="Moon icon" /> }
内容的提问来源于stack exchange,提问作者Krushnal Patel
相关产品推荐
相关产品推荐

