Next.js Typescript项目Chrome浏览器惯性滚动卡顿问题求助
故障根因及修复方案
根因排查
- 滚动状态未持久化存储:你当前在组件内直接定义的
data对象属于普通函数变量,组件每次重渲染都会被重新初始化,滚动插值计算依赖的previous值会被频繁清零,导致每帧滚动偏移量计算跳变。Chrome对高频异常transform变更的容错率低于其他浏览器,直接表现为滚动卡顿。 - 未开启GPU合成层优化:虽然使用了
translate3d触发硬件加速,但未明确指定合成层相关属性,Chrome会默认将滚动容器和其他页面元素放在同一个渲染层,每帧滚动都要触发全页重绘,性能开销陡增。 - 高频回流阻塞渲染:
resize事件触发时会直接修改body高度,无防抖的前提下会高频触发页面回流,和requestAnimationFrame的滚动计算抢占渲染线程,Chrome的调度策略会优先处理回流,进一步加剧滚动卡顿。 - 缺少raf清理逻辑:组件卸载时没有终止
requestAnimationFrame的递归调用,额外的无效执行也会占用渲染资源。
修复代码示例
首页代码修改
import type { NextPage } from 'next'; import { useEffect, useRef } from 'react'; import Head from 'next/head' import styles from '../styles/Home.module.css' import Nav from '../components/nav'; import useMousePosition from '../hooks/useMousePosition'; import Projects from '../components/projects'; import Experience from '../components/experience'; import Contact from '../components/contact'; // 导入useWindowSize(原代码漏了导入) import useWindowSize from '../hooks/useWindowSize'; const Home: NextPage = () => { const size = useWindowSize(); const app = useRef<HTMLDivElement>(null); // 修正ref类型为HTMLDivElement const scrollContainer = useRef<HTMLDivElement>(null); // 用useRef持久化滚动状态,避免重渲染重置 const data = useRef({ ease: 0.1, current: 0, previous: 0, rounded: 0 }).current; // 存储raf id用于卸载时清理 const rafId = useRef<number>(0); useEffect(() => { rafId.current = requestAnimationFrame(skewScrolling); // 组件卸载时终止raf递归 return () => cancelAnimationFrame(rafId.current); }, []); const setBodyHeight = () => { if (scrollContainer.current) document.body.style.height = `${scrollContainer.current.getBoundingClientRect().height}px`; }; useEffect(() => { setBodyHeight(); }, [size.height]); const skewScrolling = () => { if (scrollContainer.current) { data.current = window.scrollY; data.previous += (data.current - data.previous) * data.ease; data.rounded = Math.round(data.previous * 100) / 100; scrollContainer.current.style.transform = `translate3d(0, -${data.rounded}px, 0)`; rafId.current = requestAnimationFrame(skewScrolling); } }; return ( <div ref={app} className={styles.container}> <Head> <title>Abhijith Kannan</title> <meta name="description" content="Generated by create next app" /> <link rel="icon" href="/favicon.ico" /> </Head> <Nav /> {/* 新增willChange等属性强制开启合成层优化 */} <div ref={scrollContainer} style={{ zIndex: 10, willChange: 'transform', backfaceVisibility: 'hidden' }}> <div className={styles.background}> <div className={styles.intro}> <img className={styles.backgroundImg} src="/images/back.svg" alt="background" /> <div className={styles.introContainer}> <div className={styles.text}> <h2 className={styles.h2}>Hi everyone,</h2> <h1 className={styles.h1}>I am <span className={styles.span}>Electrical Engineering</span> and <span className={styles.span}> Frontend Developer</span></h1> </div> <div></div> </div> <div> </div> </div> </div> <Projects /> <Experience /> <Contact /> <footer className={styles.footer}>Developed by Abhijith Kannan</footer> </div> </div > ) } export default Home
useWindowSize自定义Hook修改(新增防抖)
import { useState, useEffect } from "react"; // 简易防抖工具函数 function debounce<T extends (...args: any[]) => any>(fn: T, delay: number): T { let timer: NodeJS.Timeout; return ((...args: any[]) => { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }) as T; } export default function useWindowSize() { function getSize() { if (typeof window !== 'undefined') { return { width: window.innerWidth, height: window.innerHeight }; } else { return { width: 0, height: 0 }; } } const [windowSize, setWindowSize] = useState(getSize); useEffect(() => { // 给resize事件回调加100ms防抖,避免频繁触发回流 const handleResize = debounce(() => { setWindowSize(getSize()); }, 100); window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, []); return windowSize; }
内容的提问来源于stack exchange,提问作者ForgottenTale
相关产品推荐
相关产品推荐

