Optimize MatchTimer by skipping useless rerenders
Some checks are pending
E2E Tests / e2e (push) Waiting to run
Tests and checks on push / run-checks-and-tests (push) Waiting to run
Updates translation progress / update-translation-progress-issue (push) Waiting to run

Realistically not a problem but trying to systematically get rid of
all these useless rerenders no matter how small they are in impact
This commit is contained in:
Kalle
2026-05-11 21:10:46 +03:00
parent 24736c70d6
commit b4bdd9d0df
2 changed files with 69 additions and 21 deletions

View File

@@ -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<MatchProps, "match" | "bracket">) {
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<MatchProps, "match" | "bracket">) {
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 (
<MatchTimerInner
startedAt={match.startedAt}
gamesCompleted={
(match.opponent1?.score ?? 0) + (match.opponent2?.score ?? 0)
}
bestOf={bestOf}
/>
);
}
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,
});

View File

@@ -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<typeof setTimeout>;
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;
}