perf(ui): rewrite useBreakpoint with singleton useSyncExternalStore

Replace per-instance useState + matchMedia pattern with a module-level
singleton that shares 6 matchMedia listeners across all callers.
Reduces listener count from 18×N to a fixed 6.

Signed-off-by: Johan Persson <johanopersson@gmail.com>
This commit is contained in:
Johan Persson
2026-02-26 16:45:46 +01:00
parent 154492c05f
commit b39c2d3893
+81 -30
View File
@@ -13,7 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useMediaQuery } from './useMediaQuery';
import { useMemo } from 'react';
import { useSyncExternalStore } from 'use-sync-external-store/shim';
import type { Breakpoint } from '../types';
export const breakpoints: { name: string; id: Breakpoint; value: number }[] = [
@@ -25,40 +26,90 @@ export const breakpoints: { name: string; id: Breakpoint; value: number }[] = [
{ name: 'Extra Large', id: 'xl', value: 1536 },
];
/** @public */
export const useBreakpoint = () => {
// Call all media queries at the top level
const matches = breakpoints.map(breakpoint => {
return useMediaQuery(`(min-width: ${breakpoint.value}px)`);
});
const breakpointIndex = new Map<Breakpoint, number>(
breakpoints.map((bp, i) => [bp.id, i]),
);
// Pre-calculate all the up/down values we need
const upMatches = new Map(
breakpoints.map(bp => [bp.id, useMediaQuery(`(min-width: ${bp.value}px)`)]),
);
function bpIndex(key: Breakpoint): number {
return breakpointIndex.get(key) ?? 0;
}
const downMatches = new Map(
breakpoints.map(bp => [
bp.id,
useMediaQuery(`(max-width: ${bp.value - 1}px)`),
]),
);
const IS_SERVER =
typeof window === 'undefined' || typeof window.matchMedia === 'undefined';
let breakpoint: Breakpoint = breakpoints[0].id;
for (let i = matches.length - 1; i >= 0; i--) {
if (matches[i]) {
breakpoint = breakpoints[i].id;
break;
function computeBreakpoint(): Breakpoint {
if (IS_SERVER) {
return 'initial';
}
for (let i = breakpoints.length - 1; i >= 0; i--) {
if (window.matchMedia(`(min-width: ${breakpoints[i].value}px)`).matches) {
return breakpoints[i].id;
}
}
return 'initial';
}
return {
breakpoint,
up: (key: Breakpoint): boolean => {
return upMatches.get(key) ?? false;
},
down: (key: Breakpoint): boolean => {
return downMatches.get(key) ?? false;
},
// --- Singleton store ---
// Created lazily on first subscribe/getSnapshot call.
let current: Breakpoint | undefined;
let listeners: Set<() => void> | undefined;
let initialized = false;
function ensureInitialized(): void {
if (initialized || IS_SERVER) {
return;
}
initialized = true;
current = computeBreakpoint();
listeners = new Set();
for (const bp of breakpoints) {
const mql = window.matchMedia(`(min-width: ${bp.value}px)`);
const onChange = () => {
const next = computeBreakpoint();
if (next !== current) {
current = next;
for (const cb of listeners!) {
cb();
}
}
};
mql.addEventListener('change', onChange);
}
}
function subscribe(callback: () => void): () => void {
ensureInitialized();
listeners!.add(callback);
return () => {
listeners!.delete(callback);
};
}
function getSnapshot(): Breakpoint {
ensureInitialized();
return current!;
}
function getServerSnapshot(): Breakpoint {
return 'initial';
}
/** @public */
export const useBreakpoint = () => {
const breakpoint = useSyncExternalStore(
subscribe,
getSnapshot,
getServerSnapshot,
);
return useMemo(
() => ({
breakpoint,
up: (key: Breakpoint): boolean => bpIndex(breakpoint) >= bpIndex(key),
down: (key: Breakpoint): boolean => bpIndex(breakpoint) < bpIndex(key),
}),
[breakpoint],
);
};