实现一个高性能倒计时:从踩坑到最佳实践

前言

本次来分享一个在基于react18中是实现一个"按钮触发倒计时"的完整思路和优化实践。

使用定时器的弊端

setInterval 或者 setTimeout 定时器实现倒计时是一个比较常方案,但是有一个无法避免的 "坑",在浏览器中,如果用户切换到其它的tab页签后,定时器就会不准确

使用 rAF 实现

requestAnimationFrame有以下几个好处:

  • rAF 与浏览器渲染节奏同步,动画/计时更平滑
  • rAF 提供每帧时间戳,易做精度与边界控制

核心思路(Hooks + rAF)

  • 使用 useRef 存放"可变但不触发渲染"的数据

    • rafIdRef:当前动画帧 id
    • endTimeRef:结束时间戳(毫秒)
    • lastSecondRef:上一次已渲染的秒数
  • 仅在秒数变化时 setState(用 lastSecondRef 去重),因为 rAF 每秒可触发 60 次渲染,过多占用cpu

  • 启动新倒计时前,取消旧的 rAF,避免并发

  • 组件卸载时取消 rAF,防泄漏

  • Math.ceil(diff / 1000) 处理边界更稳。Math.ceil 在接近 0 的边界,ceil 更符合"剩余秒数"的用户心智,不会提前跳到更小的值

核心代码

scss 复制代码
import React, { useRef, useState, useEffect } from 'react'

function App() {
  const [count, setCount] = useState(5)

  const rafIdRef = useRef(null)
  const endTimeRef = useRef(0)
  const lastSecondRef = useRef(null)

  const startCountdown = (durationSec) => {
    // 1) 防并发:先取消已有动画帧
    if (rafIdRef.current !== null) {
      cancelAnimationFrame(rafIdRef.current)
      rafIdRef.current = null
    }

    // 2) 初始化时间数据
    const start = performance.now()
    endTimeRef.current = start + durationSec * 1000
    lastSecondRef.current = null

    // 3) rAF 驱动:只在秒数变化时 setState
    const step = (ts) => {
      const diff = endTimeRef.current - ts
      if (diff > 0) {
        const sec = Math.ceil(diff / 1000)
        // 关键优化,避免重复过渡的 setState
        if (sec !== lastSecondRef.current) {
          lastSecondRef.current = sec
          setCount(sec)
        }
        rafIdRef.current = requestAnimationFrame(step)
      } else {
        setCount(0)
        rafIdRef.current = null
      }
    }
    rafIdRef.current = requestAnimationFrame(step)
  }

  // 4) 卸载清理,防止泄漏
  useEffect(() => {
    return () => {
      if (rafIdRef.current !== null) {
        cancelAnimationFrame(rafIdRef.current)
      }
    }
  }, [])

  return (
    <div>
      <p>剩余秒数:{count}</p>
      <button onClick={() => startCountdown(5)}>开始 5 秒倒计时</button>
    </div>
  )
}

export default App

结语

如果这篇文章帮到了你,欢迎点赞👍和关注⭐️。

文章如有错误之处,希望在评论区指正🙏🙏。

相关推荐
ZJU_统一阿萨姆11 小时前
【DSH】如何将 DeepSeek Harness 嵌入生产环境?万字解构 DeepSeek Agent Harness 的运行机制与安全边界
前端·javascript·安全
用户9210802628611 小时前
4. 前端地图大量点位实时更新优化:后端推增量,前端做 diff
前端
SoaringHeart11 小时前
Flutter最佳实践:Web项目中文字体首次加载乱码问题解决
前端·flutter
西安小哥12 小时前
破局与重生:大厂前端如何借力 AI 转型“超级全栈“
前端·人工智能
sunly_13 小时前
TypeScript总结:16、面向对象速查
前端·javascript·typescript
张元清13 小时前
React useInterval Hook:没有过期闭包的 setInterval (2026)
javascript·react.js
MXN_小南学前端13 小时前
React超长文本域中实现“返回顶部”浮动按钮
前端·javascript·react.js
学习嵌入式的小周13 小时前
Notepad++8.8.7下载安装(附安装包)
前端·notepad++
gs8014013 小时前
解构 Cordis:面向“时空可组合性”的 TypeScript 元框架深度剖析
前端·javascript·typescript
Highcharts.js13 小时前
Highcharts大数据渲染模块Boost实战与参数最佳实践表
javascript·数据可视化·boost·highcharts·大数据渲染·加速配置·参数表