From b4bdd9d0df245d663def123215dacce28892108a Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Mon, 11 May 2026 21:10:46 +0300 Subject: [PATCH] Optimize MatchTimer by skipping useless rerenders Realistically not a problem but trying to systematically get rid of all these useless rerenders no matter how small they are in impact --- .../components/Bracket/Match.tsx | 49 ++++++++++++------- app/hooks/useAutoRerender.ts | 41 +++++++++++++++- 2 files changed, 69 insertions(+), 21 deletions(-) diff --git a/app/features/tournament-bracket/components/Bracket/Match.tsx b/app/features/tournament-bracket/components/Bracket/Match.tsx index a0c3df9a8..bbd4c2473 100644 --- a/app/features/tournament-bracket/components/Bracket/Match.tsx +++ b/app/features/tournament-bracket/components/Bracket/Match.tsx @@ -1,7 +1,6 @@ import clsx from "clsx"; -import { differenceInMinutes } from "date-fns"; import { Eye } from "lucide-react"; -import * as React from "react"; +import type * as React from "react"; import { Link } from "react-router"; import { Avatar } from "~/components/Avatar"; import { SendouButton } from "~/components/elements/Button"; @@ -13,6 +12,7 @@ import { useTournamentVods, } from "~/features/tournament/routes/to.$id"; import { matchEndedEarly } from "~/features/tournament-match/tournament-match-utils"; +import { useAutoRerender } from "~/hooks/useAutoRerender"; import { databaseTimestampToDate } from "~/utils/dates"; import type { Unpacked } from "~/utils/types"; import { @@ -419,24 +419,13 @@ function MatchVods({ vods }: MatchVodsProps) { } function MatchTimer({ match, bracket }: Pick) { - const [now, setNow] = React.useState(new Date()); const tournament = useTournament(); - React.useEffect(() => { - const interval = setInterval(() => { - setNow(new Date()); - }, 60000); - - return () => clearInterval(interval); - }, []); - if (tournament.isLeagueDivision) return null; - if (!match.startedAt) return null; const isOver = match.opponent1?.result === "win" || match.opponent2?.result === "win"; - if (isOver) return null; const isLocked = tournament.ctx.castedMatchesInfo?.lockedMatches?.some( @@ -446,17 +435,39 @@ function MatchTimer({ match, bracket }: Pick) { const round = bracket.data.round.find((r) => r.id === match.round_id); const bestOf = round?.maps?.count; - if (!bestOf) return null; - const elapsedMinutes = differenceInMinutes( - now, - databaseTimestampToDate(match.startedAt), + return ( + + ); +} + +interface MatchTimerInnerProps { + startedAt: number; + gamesCompleted: number; + bestOf: number; +} + +function MatchTimerInner({ + startedAt, + gamesCompleted, + bestOf, +}: MatchTimerInnerProps) { + const startedAtDate = databaseTimestampToDate(startedAt); + const now = useAutoRerender("minute", { alignTo: startedAtDate }); + + const elapsedMinutes = Math.floor( + (now.getTime() - startedAtDate.getTime()) / 60_000, ); const status = Deadline.matchStatus({ elapsedMinutes, - gamesCompleted: - (match.opponent1?.score ?? 0) + (match.opponent2?.score ?? 0), + gamesCompleted, maxGamesCount: bestOf, }); diff --git a/app/hooks/useAutoRerender.ts b/app/hooks/useAutoRerender.ts index ab33991f4..aaacaba5a 100644 --- a/app/hooks/useAutoRerender.ts +++ b/app/hooks/useAutoRerender.ts @@ -1,15 +1,52 @@ import * as React from "react"; +interface AutoRerenderOptions { + alignTo: Date; +} + /** * Forces the component to rerender periodically. Returns the current `Date` at * the time of the latest tick — callers should consume this value (e.g. pass * it to date-fns) so React Compiler can see the state is observable and won't * memoize the rerender away. + * + * When `every` is `"minute"`, ticks are aligned to the `alignTo` reference + * time via a self-rescheduling `setTimeout`, so the component rerenders at + * exactly `alignTo + N*60s` rather than at arbitrary offsets from mount. */ -export function useAutoRerender(every?: "second" | "ten seconds"): Date { +export function useAutoRerender(): Date; +export function useAutoRerender(every: "second" | "ten seconds"): Date; +export function useAutoRerender( + every: "minute", + options: AutoRerenderOptions, +): Date; +export function useAutoRerender( + every?: "second" | "ten seconds" | "minute", + options?: AutoRerenderOptions, +): Date { const [now, setNow] = React.useState(() => new Date()); + const alignToMs = options?.alignTo.getTime(); React.useEffect(() => { + if (every === "minute") { + let timeout: ReturnType; + + const scheduleNext = () => { + const elapsed = Date.now() - alignToMs!; + const remainder = ((elapsed % 60_000) + 60_000) % 60_000; + timeout = setTimeout(() => { + setNow(new Date()); + scheduleNext(); + }, 60_000 - remainder); + }; + + scheduleNext(); + + return () => { + clearTimeout(timeout); + }; + } + const intervalTime = !every || every === "second" ? 1000 : 10000; const interval = setInterval(() => { @@ -19,7 +56,7 @@ export function useAutoRerender(every?: "second" | "ten seconds"): Date { return () => { clearInterval(interval); }; - }, [every]); + }, [every, alignToMs]); return now; }