From bf9de2903d2ea574a3d9596eb928dfab216cf807 Mon Sep 17 00:00:00 2001 From: Mika Cohen Date: Thu, 21 May 2026 15:44:43 -0600 Subject: [PATCH 1/2] Avoid repeated browser chrome theme syncs Skip DOM theme work when the requested theme and system-dark state are already applied. Several components subscribe with useTheme on startup; without this guard they each re-read computed styles and update chrome color state during first render. 10k Chrome first-load click profile, fresh copied fixture, 5 runs, median: | metric | previous | after | delta | |---|---:|---:|---:| | initial render | 3630.273 ms | 3269.955 ms | -360.318 ms (-9.9%) | | click settle | 9687.877 ms | 9654.482 ms | -33.395 ms (-0.3%) | | total | 13357.292 ms | 12924.437 ms | -432.855 ms (-3.2%) | | FunctionCall | 1195.416 ms | 1050.971 ms | -144.445 ms (-12.1%) | | RunTask | 2682.802 ms | 2298.999 ms | -383.803 ms (-14.3%) | | UpdateLayoutTree | 685.519 ms | 564.078 ms | -121.441 ms (-17.7%) | Verification: bun fmt; bun lint; bun typecheck; bun run test. --- apps/web/src/hooks/useTheme.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/web/src/hooks/useTheme.ts b/apps/web/src/hooks/useTheme.ts index cd254c97548..d8fa60414a0 100644 --- a/apps/web/src/hooks/useTheme.ts +++ b/apps/web/src/hooks/useTheme.ts @@ -18,6 +18,7 @@ const DYNAMIC_THEME_COLOR_SELECTOR = `meta[name="${THEME_COLOR_META_NAME}"][data let listeners: Array<() => void> = []; let lastSnapshot: ThemeSnapshot | null = null; let lastDesktopTheme: Theme | null = null; +let lastAppliedTheme: ThemeSnapshot | null = null; function emitChange() { for (const listener of listeners) listener(); @@ -89,11 +90,18 @@ export function syncBrowserChromeTheme() { function applyTheme(theme: Theme, suppressTransitions = false) { if (typeof document === "undefined" || typeof window === "undefined") return; + const systemDark = getSystemDark(); + if (lastAppliedTheme?.theme === theme && lastAppliedTheme.systemDark === systemDark) { + syncDesktopTheme(theme); + return; + } + if (suppressTransitions) { document.documentElement.classList.add("no-transitions"); } - const isDark = theme === "dark" || (theme === "system" && getSystemDark()); + const isDark = theme === "dark" || (theme === "system" && systemDark); document.documentElement.classList.toggle("dark", isDark); + lastAppliedTheme = { theme, systemDark }; syncBrowserChromeTheme(); syncDesktopTheme(theme); if (suppressTransitions) { From cc880130a7c088e7b562f1242d9e5f63f6b9175d Mon Sep 17 00:00:00 2001 From: Mika Cohen Date: Fri, 22 May 2026 15:17:11 -0600 Subject: [PATCH 2/2] Guard theme sync media queries --- .oxfmtrc.json | 1 + apps/web/src/hooks/useTheme.test.ts | 69 +++++++++++++++++++++++++++++ apps/web/src/hooks/useTheme.ts | 14 +++--- 3 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 apps/web/src/hooks/useTheme.test.ts diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 3d65d9c93bb..19934fdff42 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -3,6 +3,7 @@ "ignorePatterns": [ ".reference", ".plans", + ".cache", "dist", "dist-electron", "node_modules", diff --git a/apps/web/src/hooks/useTheme.test.ts b/apps/web/src/hooks/useTheme.test.ts new file mode 100644 index 00000000000..b003f01a1e9 --- /dev/null +++ b/apps/web/src/hooks/useTheme.test.ts @@ -0,0 +1,69 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +function installThemeDom(theme: string | null, matchMedia?: Window["matchMedia"]) { + const element = { + name: "", + setAttribute: vi.fn(), + }; + const documentElement = { + classList: { + add: vi.fn(), + remove: vi.fn(), + toggle: vi.fn(), + }, + offsetHeight: 0, + style: {}, + }; + const body = { + style: {}, + }; + + vi.stubGlobal("localStorage", { + getItem: vi.fn(() => theme), + setItem: vi.fn(), + }); + vi.stubGlobal("window", { + matchMedia, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + }); + vi.stubGlobal("document", { + body, + createElement: vi.fn(() => element), + documentElement, + head: { + append: vi.fn(), + }, + querySelector: vi.fn(() => null), + }); + vi.stubGlobal( + "getComputedStyle", + vi.fn(() => ({ backgroundColor: "rgb(1, 2, 3)" })), + ); +} + +describe("useTheme module initialization", () => { + afterEach(() => { + vi.resetModules(); + vi.unstubAllGlobals(); + }); + + it("does not read matchMedia for explicit themes", async () => { + const matchMedia = vi.fn(() => ({ + matches: true, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + })) as unknown as Window["matchMedia"]; + installThemeDom("dark", matchMedia); + + await import("./useTheme"); + + expect(matchMedia).not.toHaveBeenCalled(); + }); + + it("does not require matchMedia when an explicit theme is stored", async () => { + installThemeDom("light"); + + await expect(import("./useTheme")).resolves.toBeDefined(); + }); +}); diff --git a/apps/web/src/hooks/useTheme.ts b/apps/web/src/hooks/useTheme.ts index d8fa60414a0..ed76c734a41 100644 --- a/apps/web/src/hooks/useTheme.ts +++ b/apps/web/src/hooks/useTheme.ts @@ -29,7 +29,11 @@ function hasThemeStorage() { } function getSystemDark() { - return typeof window !== "undefined" && window.matchMedia(MEDIA_QUERY).matches; + return ( + typeof window !== "undefined" && + typeof window.matchMedia === "function" && + window.matchMedia(MEDIA_QUERY).matches + ); } function getStored(): Theme { @@ -90,7 +94,7 @@ export function syncBrowserChromeTheme() { function applyTheme(theme: Theme, suppressTransitions = false) { if (typeof document === "undefined" || typeof window === "undefined") return; - const systemDark = getSystemDark(); + const systemDark = theme === "system" ? getSystemDark() : false; if (lastAppliedTheme?.theme === theme && lastAppliedTheme.systemDark === systemDark) { syncDesktopTheme(theme); return; @@ -156,12 +160,12 @@ function subscribe(listener: () => void): () => void { listeners.push(listener); // Listen for system preference changes - const mq = window.matchMedia(MEDIA_QUERY); + const mq = typeof window.matchMedia === "function" ? window.matchMedia(MEDIA_QUERY) : null; const handleChange = () => { if (getStored() === "system") applyTheme("system", true); emitChange(); }; - mq.addEventListener("change", handleChange); + mq?.addEventListener("change", handleChange); // Listen for storage changes from other tabs const handleStorage = (e: StorageEvent) => { @@ -174,7 +178,7 @@ function subscribe(listener: () => void): () => void { return () => { listeners = listeners.filter((l) => l !== listener); - mq.removeEventListener("change", handleChange); + mq?.removeEventListener("change", handleChange); window.removeEventListener("storage", handleStorage); }; }