Next.js应用首次滚动卡顿 react-slick引发导航栏性能问题
问题说明
将原有React应用重构为Next.js项目后,滚动到指定位置触发下拉过渡效果的导航栏在页面首次加载时,滚动操作会出现数毫秒的卡顿冻结,导航栏下拉效果首次触发完成后功能恢复正常。该问题在纯React项目中未出现,经排查为react-slick轮播组件与导航栏滚动逻辑冲突导致。
根因分析
- 导航栏重复绑定了2个scroll事件监听器,且未做性能优化,滚动时高频触发状态更新,和
react-slick初始化时的尺寸计算、重排操作抢占主线程,直接导致首次滚动卡顿 - 导航栏状态更新逻辑存在bug:滚动高度超过112px时执行
setNewNav(!newNav),滚动过程中会反复切换导航栏显示状态,触发大量无意义重渲染 - Carousel组件在渲染函数主流程中直接调用父组件传入的状态更新回调,每次组件重渲染都会触发父组件重渲染,和滚动事件的状态更新叠加形成渲染阻塞
- Next.js服务端渲染的特性下,
react-slick在客户端hydration阶段需要重新计算所有轮播项布局,初始化开销远大于纯客户端React项目,和首屏滚动逻辑叠加就会出现首次冻结
修复方案
- 合并重复的scroll监听,添加节流,标记监听为
passive提升滚动性能,修正状态更新逻辑,仅在滚动阈值跨越时触发导航栏状态切换 - 修复Carousel组件的反模式调用,将父组件回调移入副作用钩子,禁止在渲染主流程中直接触发父组件状态更新
- 对轮播组件使用Next.js动态导入,关闭服务端渲染,延后组件初始化时机,避免和首屏导航栏交互抢占主线程
- 优化
react-slick配置,使用按需懒加载减少初始化开销,给轮播图片添加原生懒加载属性
修正后代码示例
导航栏组件核心逻辑修正
const NavComponent = () => { const isDesktopOrLaptop = useMediaQuery({ query: '(min-width: 1224px)' }) const isMobile = useMediaQuery({ query: '(max-width: 768px)' }) const [open, setOpen] = useState(false); const [scroll, setScroll] = useState(0); const [newNav, setNewNav] = useState(false) const [show, setShow] = useState(false); // 滚动节流,对齐浏览器16ms帧刷新间隔 const throttle = (func, delay = 16) => { let timer = null; return (...args) => { if (timer) return; timer = setTimeout(() => { func.apply(this, args); timer = null; }, delay) } } React.useEffect(() => { const handleScroll = throttle(() => { if (window === undefined) return; const scrollY = window.scrollY; // 合并两个滚动监听逻辑,减少重复触发 setScroll(scrollY > 112 ? scrollY : 0); // 修正原逻辑中反复取反的bug,仅在状态变化时更新 setNewNav(scrollY > 112); }) // 标记passive告诉浏览器监听不会阻止默认滚动行为,大幅提升滚动性能 window.addEventListener('scroll', handleScroll, { passive: true }); return () => window.removeEventListener('scroll', handleScroll); }, []); const navClass = newNav ? "menu2_show" : "menu2" const handleClose = () => setShow(prev => !prev); const handleShow = () => setShow(prev => !prev); // 其余JSX结构保持原有逻辑即可,给下拉导航栏元素添加CSS属性 will-change: transform 提示浏览器提前创建合成层,减少重排开销 }
轮播组件修正
import React from "react"; import { useState, useEffect } from "react"; import Slider from "react-slick"; import Link from "next/link" function Carousel(props) { const [zoomed, setZoomed] = useState('backgroundSize:550') const [fading_3, setFading_3] = useState(false) const [indexValue, setIndexValue] = useState({}) const [imageIndex, setImageIndex] = useState(0); useEffect(() => { // 修正原逻辑中不必要的取反操作 setFading_3(true) }, []) useEffect(() => { const currentVal = props.array[imageIndex]; setIndexValue(currentVal); // 将父组件回调移入副作用,仅在轮播索引变化时触发 props.sendDataToParent(currentVal) }, [imageIndex, props.array, props.sendDataToParent]) const classes_4 = fading_3 ? 'zoomClass' : 'zoomClass hide' const settings = { focusOnSelect: true, infinite: true, lazyLoad: 'ondemand', // 替换原布尔值配置,按需加载图片降低初始化开销 speed: 600, slidesToShow: 3, centerMode: true, centerPadding: 0, beforeChange: (current, next) => setImageIndex(next), }; return ( <div className="App"> <Slider {...settings} className={classes_4}> {props.array.map((img, idx) => ( <div key={img._id} className={idx === imageIndex ? "slide activeSlide" : "slide"}> {idx === imageIndex ? <Link href="/"> <img loading="lazy" src={img.img} alt={img.alt || ''} /> </Link> : <img loading="lazy" src={img.img} alt={img.alt || ''} /> } </div> ))} </Slider> </div> ); } export default Carousel;
页面引入轮播组件方式修正
import dynamic from 'next/dynamic'; // 动态导入轮播组件,关闭服务端渲染,延后初始化时机避免抢占首屏主线程 const Carousel = dynamic(() => import('./Carousel'), { ssr: false, loading: () => <div className="carousel-placeholder"></div> })
内容的提问来源于stack exchange,提问作者József Kiss
相关产品推荐
相关产品推荐

