measure-rerenders.mts counts React commits on an untouched board and attributes each
to the shallowest component that re-rendered, i.e. where the update entered the tree.
What it found: an idle board commits 73-96 times per minute, re-rendering
@mantine/core/Box over 30,000 times in 60s. That is the churn behind the 650 MB
browser peak — transient allocation rather than retention. About 80% of those commits
enter at the same anonymous depth-2 provider, props [value, children], value {appDir}:
Next.js's internal HeadManagerContext, whose value is a fresh object literal on every
App Router render, so each render re-renders the whole app beneath it. The rest enter
at __next_metadata_boundary__ / <title>, pointing the same way.
The trigger is inside Next's router/metadata handling — application code has no
polling router.refresh() — so nothing here fixes it. It is the next thing to chase and
it dominates everything downstream of it.
useTimeAgo called setState on a 1s interval even when the label was unchanged ("3
minutes ago" holds for a full minute), re-rendering its subtree ~60x/minute per
consumer for an identical string. It now compares before setting. Recorded as a
correctness fix and NOT a measured win: idle commits stayed inside their 73-96 range
afterwards, because the app-root churn dominates.
pnpm test 1794/1795 (pre-existing pi-hole Testcontainers flake); typecheck clean
across 37 packages; no new lint errors.
52 lines
1.6 KiB
TypeScript
52 lines
1.6 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import dayjs from "dayjs";
|
|
import relativeTime from "dayjs/plugin/relativeTime";
|
|
|
|
dayjs.extend(relativeTime);
|
|
|
|
const calculateTimeAgo = (timestamp: Date) => {
|
|
return dayjs().to(timestamp);
|
|
};
|
|
|
|
export const useTimeAgo = (timestamp: Date, updateFrequency = 1000) => {
|
|
const [timeAgo, setTimeAgo] = useState(() => calculateTimeAgo(timestamp));
|
|
|
|
useEffect(() => {
|
|
setTimeAgo(calculateTimeAgo(timestamp));
|
|
}, [timestamp]);
|
|
|
|
useEffect(() => {
|
|
// Only re-render when the rendered text actually changes. The label is coarse
|
|
// ("3 minutes ago" holds for a whole minute) but the timer ticks every second,
|
|
// so setting state unconditionally re-rendered the subtree ~60x/minute per
|
|
// consumer for an identical string.
|
|
const intervalId = setInterval(() => {
|
|
const next = calculateTimeAgo(timestamp);
|
|
setTimeAgo((previous) => (previous === next ? previous : next));
|
|
}, updateFrequency);
|
|
|
|
return () => clearInterval(intervalId); // clear interval on hook unmount
|
|
}, [timestamp, updateFrequency]);
|
|
|
|
return timeAgo;
|
|
};
|
|
|
|
export const useIntegrationConnected = (updatedAt: Date, { timeout = 30000 }) => {
|
|
const [connected, setConnected] = useState(Math.abs(dayjs(updatedAt).diff()) < timeout);
|
|
|
|
useEffect(() => {
|
|
setConnected(Math.abs(dayjs(updatedAt).diff()) < timeout);
|
|
|
|
const delayUntilTimeout = timeout - Math.abs(dayjs(updatedAt).diff());
|
|
const timeoutRef = setTimeout(() => {
|
|
setConnected(false);
|
|
}, delayUntilTimeout);
|
|
|
|
return () => clearTimeout(timeoutRef);
|
|
}, [updatedAt, timeout]);
|
|
|
|
return connected;
|
|
};
|