mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-02 08:07:40 -05:00
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
This commit is contained in:
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user