perf(common): stop useTimeAgo re-rendering every second, add re-render profiler
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.
This commit is contained in:
3 files changed
+211
-1
No files matched your search
@@ -213,6 +213,34 @@ payload to a stub hook that is byte-identical for profiling and production build
|
||||
that probe returns a false negative for both. Confirm the Profiler tab with the real
|
||||
extension.
|
||||
|
||||
## Where the re-render churn actually comes from
|
||||
|
||||
`scripts/benchmarks/measure-rerenders.mts` counts React commits on a board nobody is
|
||||
touching, and attributes each commit to the shallowest component that re-rendered — the
|
||||
point where the update entered the tree.
|
||||
|
||||
**An idle board commits 73–96 times per minute**, and in 60 s `@mantine/core/Box`
|
||||
re-renders over 30,000 times. That is the churn behind the 650 MB peak: it is transient
|
||||
allocation, not retention.
|
||||
|
||||
Roughly 80% of those commits enter at the *same place*: an anonymous provider at depth 2
|
||||
whose props are `[value, children]` and whose value is `{appDir}` — Next.js's internal
|
||||
`HeadManagerContext`. Its value is a fresh object literal on every App Router render, so
|
||||
each one re-renders the entire application below it. The remainder enter around
|
||||
`__next_metadata_boundary__` / `<title>`, which points the same way.
|
||||
|
||||
So the dominant cost is the **App Router root re-rendering about once a second**, not any
|
||||
individual widget. No polling `router.refresh()` exists in application code, so the
|
||||
trigger is inside Next's own router/metadata handling and is not fixed by anything in
|
||||
this PR. That is the next thing to chase, and it is worth more than any further
|
||||
component-level tuning: everything else is downstream of it.
|
||||
|
||||
`useTimeAgo` was calling `setState` every second even when the rendered label was
|
||||
unchanged ("3 minutes ago" holds for a minute), so it now compares before setting. This
|
||||
is a correctness fix — idle commits stayed within their 73–96 range afterwards, because
|
||||
the app-root churn dominates and this hook is not what drives it. Recorded as *not* a
|
||||
measured win.
|
||||
|
||||
## Measured but not pursued
|
||||
|
||||
| lever | measured cost | why not pursued |
|
||||
|
||||
Reference in new issue
Block a user