Skip redundant root mutations during initial load

This commit is contained in:
Peter White
2026-09-02 11:44:20 +02:00
parent 5f88f4e680
commit db9fbab15e
4 changed files with 60 additions and 22 deletions
@@ -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 <html> 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);
}
}
@@ -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);
}
}
}
@@ -8,6 +8,12 @@ import { useEffect } from 'react';
export function TableOfContentsScript() {
useEffect(() => {
const root = document.documentElement;
// Writing the <html> 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
@@ -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 <html> 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;