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

如何让React阅读进度条滚动更流畅?

文章阅读进度条滚动卡顿问题解决

问题描述

实现了文章阅读进度展示功能,但滚动时进度条卡顿严重,存在明显滞后。当前通过useEffect监听滚动事件计算页面高度占比,将占比作为props传递给styled组件设置宽度,功能可用但卡顿问题突出。

原实现代码

export const SingleArticle = () => {
  const [readProgress, setReadProgress] = useState(0)

  useEffect(() => {
    const calcReadProgress = () => {
      const { scrollHeight, clientHeight } = document.documentElement

      const height = scrollHeight - clientHeight

      setReadProgress((document.documentElement.scrollTop / height) * 100)
    }

    window.addEventListener('scroll', calcReadProgress)

    return () => window.removeEventListener('scroll', calcReadProgress)
  }, [])

  return (
       <ReadProgressBar progress={readProgress} />
  )
}

type StyledProps = {
  progress: number
}

export const ReadProgressBar = styled.div<StyledProps>`
  width: 100%;
  height: 10px;
  background-color: ${({ theme }) => theme.colors.darkBlue};
  position: fixed;
  top: 0;
  left: 0;
  z-index: 10;
  &::after {
     content: '';
     width: ${({ progress }) => (progress ? `${progress}%` : '0%')};
     height: 10px;
     background-color: ${({ theme }) => theme.colors.purple};
     position: absolute;
     top: 0px;
     left: 0px;
  }
`

优化方案

通过优化滚动事件处理逻辑、减少不必要的重渲染,解决卡顿问题,优化后的代码如下:

import { useState, useEffect, useRef } from 'react';
import styled from 'styled-components';

export const SingleArticle = () => {
  const [readProgress, setReadProgress] = useState(0);
  const isUpdating = useRef(false);
  const currentProgress = useRef(0);

  useEffect(() => {
    const calcReadProgress = () => {
      if (isUpdating.current) return;
      
      isUpdating.current = true;
      requestAnimationFrame(() => {
        const { scrollHeight, clientHeight, scrollTop } = document.documentElement;
        const height = scrollHeight - clientHeight;
        
        if (height <= 0) {
          currentProgress.current = 0;
          setReadProgress(0);
          isUpdating.current = false;
          return;
        }
        
        const newProgress = (scrollTop / height) * 100;
        // 仅当进度变化超过阈值时更新,减少重渲染
        if (Math.abs(newProgress - currentProgress.current) > 0.1) {
          currentProgress.current = newProgress;
          setReadProgress(newProgress);
        }
        
        isUpdating.current = false;
      });
    };

    // 被动监听滚动事件,提升浏览器滚动性能
    window.addEventListener('scroll', calcReadProgress, { passive: true });

    return () => window.removeEventListener('scroll', calcReadProgress);
  }, []);

  return (
    <ReadProgressBar progress={readProgress} />
  );
};

type StyledProps = {
  progress: number;
};

export const ReadProgressBar = styled.div<StyledProps>`
  width: 100%;
  height: 10px;
  background-color: ${({ theme }) => theme.colors.darkBlue};
  position: fixed;
  top: 0;
  left: 0;
  z-index: 10;
  &::after {
    content: '';
    width: ${({ progress }) => `${progress}%`};
    height: 10px;
    background-color: ${({ theme }) => theme.colors.purple};
    position: absolute;
    top: 0;
    left: 0;
    // 轻量过渡动画提升视觉顺滑度
    transition: width 0.1s ease-out;
  }
`;

关键优化点说明

  • requestAnimationFrame同步更新:将进度计算和DOM更新绑定到浏览器的重绘周期,避免主线程阻塞,让进度条更新与滚动动作同步。
  • 更新锁避免重复计算:用isUpdating标记当前是否正在处理进度更新,防止同一帧内多次触发计算,减少冗余操作。
  • 阈值过滤减少重渲染:只有当进度变化超过0.1%时才更新组件状态,避免微小变化导致的频繁重渲染。
  • 被动滚动监听:设置{ passive: true }告知浏览器不会阻止默认滚动行为,让浏览器可以优化滚动性能。
  • 边界情况处理:当页面内容高度小于视口高度时,直接将进度设为0,避免异常值出现。
  • 轻量过渡动画:给进度条宽度变化添加短时长过渡,让视觉效果更顺滑,同时不会引入额外卡顿。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 04:20:55