From db9fbab15e2856166331dda5ce2fc0283dca0cea Mon Sep 17 00:00:00 2001 From: Peter White Date: Wed, 2 Sep 2026 11:44:20 +0200 Subject: [PATCH] Skip redundant root mutations during initial load --- .../components/AIChat/useAIChatWidthStore.ts | 19 ++++++++- .../PageBody/PreservePageLayout.tsx | 41 ++++++++++++------- .../TableOfContents/TableOfContentsScript.tsx | 14 +++++-- .../primitives/NavigationLoader.tsx | 8 +++- 4 files changed, 60 insertions(+), 22 deletions(-) diff --git a/packages/gitbook/src/components/AIChat/useAIChatWidthStore.ts b/packages/gitbook/src/components/AIChat/useAIChatWidthStore.ts index a1a533ff5..f5d797049 100644 --- a/packages/gitbook/src/components/AIChat/useAIChatWidthStore.ts +++ b/packages/gitbook/src/components/AIChat/useAIChatWidthStore.ts @@ -62,8 +62,23 @@ export const useIsAIChatMaxWidth = () => // Hoisted so the synchronous persist rehydrate (during create() above) can call them before this point. function setWidthOnViewport(width: number) { - if (typeof document !== 'undefined') { - document.documentElement.style.setProperty('--ai-chat-width', `${capToViewport(width)}px`); + if (typeof document === 'undefined') { + return; + } + // Any change to the style attribute re-styles the whole document, so only write when + // the value differs from what is already in effect (the stylesheet default is the min width). + const style = document.documentElement.style; + const current = style.getPropertyValue('--ai-chat-width'); + const capped = capToViewport(width); + if (capped === AI_CHAT_MIN_WIDTH) { + if (current) { + style.removeProperty('--ai-chat-width'); + } + return; + } + const value = `${capped}px`; + if (current !== value) { + style.setProperty('--ai-chat-width', value); } } diff --git a/packages/gitbook/src/components/PageBody/PreservePageLayout.tsx b/packages/gitbook/src/components/PageBody/PreservePageLayout.tsx index e683e5a9c..5221ad591 100644 --- a/packages/gitbook/src/components/PageBody/PreservePageLayout.tsx +++ b/packages/gitbook/src/components/PageBody/PreservePageLayout.tsx @@ -24,22 +24,33 @@ export function PreservePageLayout(props: { wideLayout: boolean; pageHasToc: boo return; } - if (wideLayout) { - header.classList.add('layout-wide'); - header.classList.remove('layout-default'); - } else { - header.classList.remove('layout-wide'); - header.classList.add('layout-default'); - } - - if (pageHasToc) { - header.classList.add('page-has-toc'); - header.classList.remove('page-no-toc'); - } else { - header.classList.add('page-no-toc'); - header.classList.remove('page-has-toc'); - } + // The header is a `body:has()` subject, so touching its classes re-styles the whole + // document. Only correct it when a previous page left it disagreeing with this one, and + // stamp this page's layout on it as the page leaves, which is when the next one needs it. + syncLayoutClasses(header, { wideLayout, pageHasToc }, false); + return () => { + syncLayoutClasses(header, { wideLayout, pageHasToc }, true); + }; }, [wideLayout, pageHasToc]); return null; } + +function syncLayoutClasses( + header: Element, + layout: { wideLayout: boolean; pageHasToc: boolean }, + force: boolean +) { + const pairs: [wanted: string, other: string][] = [ + layout.wideLayout ? ['layout-wide', 'layout-default'] : ['layout-default', 'layout-wide'], + layout.pageHasToc ? ['page-has-toc', 'page-no-toc'] : ['page-no-toc', 'page-has-toc'], + ]; + for (const [wanted, other] of pairs) { + if (header.classList.contains(other)) { + header.classList.remove(other); + header.classList.add(wanted); + } else if (force && !header.classList.contains(wanted)) { + header.classList.add(wanted); + } + } +} diff --git a/packages/gitbook/src/components/TableOfContents/TableOfContentsScript.tsx b/packages/gitbook/src/components/TableOfContents/TableOfContentsScript.tsx index 1cc1d65f6..a1cf567e7 100644 --- a/packages/gitbook/src/components/TableOfContents/TableOfContentsScript.tsx +++ b/packages/gitbook/src/components/TableOfContents/TableOfContentsScript.tsx @@ -8,6 +8,12 @@ import { useEffect } from 'react'; export function TableOfContentsScript() { useEffect(() => { const root = document.documentElement; + // Writing the style attribute re-styles the whole document, so skip unchanged values. + const setVar = (name: string, value: string) => { + if (root.style.getPropertyValue(name) !== value) { + root.style.setProperty(name, value); + } + }; // Calculate and set TOC dimensions const updateTocLayout = () => { @@ -42,8 +48,8 @@ export function TableOfContentsScript() { } // Update height - root.style.setProperty('--toc-height', `${Math.max(height, 0)}px`); - root.style.setProperty('--toc-top-offset', `${Math.max(offset, 0)}px`); + setVar('--toc-height', `${Math.max(height, 0)}px`); + setVar('--toc-top-offset', `${Math.max(offset, 0)}px`); // Subtract visible pageCover (if any) if ( @@ -64,8 +70,8 @@ export function TableOfContentsScript() { } } - root.style.setProperty('--outline-height', `${Math.max(height, 0)}px`); - root.style.setProperty('--outline-top-offset', `${Math.max(offset, 0)}px`); + setVar('--outline-height', `${Math.max(height, 0)}px`); + setVar('--outline-top-offset', `${Math.max(offset, 0)}px`); }; // Initial update diff --git a/packages/gitbook/src/components/primitives/NavigationLoader.tsx b/packages/gitbook/src/components/primitives/NavigationLoader.tsx index 91671bad8..cdfe322e5 100644 --- a/packages/gitbook/src/components/primitives/NavigationLoader.tsx +++ b/packages/gitbook/src/components/primitives/NavigationLoader.tsx @@ -1,6 +1,6 @@ 'use client'; import { usePathname } from 'next/navigation'; -import { useEffect, useLayoutEffect } from 'react'; +import { useEffect, useLayoutEffect, useRef } from 'react'; import { useIsNavigating } from '../hooks'; import { tcls } from '@/lib/tailwind'; @@ -15,8 +15,14 @@ export const NavigationLoader = () => { }, []); // On route changes, add a transient class for the first paint of the new page. + // Skipped on the initial mount: toggling a class on re-styles the whole document. + const isInitialMount = useRef(true); useLayoutEffect(() => { void pathname; + if (isInitialMount.current) { + isInitialMount.current = false; + return; + } const root = document.documentElement; root.classList.add('route-change'); let raf2 = 0;