revert some changes

This commit is contained in:
Nolann Biron
2026-07-16 09:15:25 +02:00
parent 9ab6cfb878
commit f8fd5e2bb4
11 changed files with 43 additions and 78 deletions
+1 -1
View File
@@ -2,4 +2,4 @@
"gitbook": patch
---
Reduce the JavaScript and CSS loaded on published site pages: the search index and its UI now load only when search is opened, the admin toolbar and OpenAPI/ContentKit styles are no longer shipped to every visitor, and the client bundle targets modern browsers.
Reduce the JavaScript and CSS loaded on published site pages: the search index and its UI now load only when search is opened, and the admin toolbar and OpenAPI/ContentKit styles are no longer shipped to every visitor.
-10
View File
@@ -37,16 +37,6 @@ const nextConfig = {
optimisticClientCache: false,
// Disable splitting the RSC in like 5 chunks
prefetchInlining: true,
// Tree-shake barrel imports from these packages so only the used entrypoints ship
// in the client bundle (notably `motion`, which is otherwise pulled in wholesale).
optimizePackageImports: [
'motion',
'@gitbook/icons',
'react-aria',
'react-aria-components',
'react-stately',
],
},
env: {
+3 -1
View File
@@ -138,7 +138,9 @@
"e2e-browserless": "bun test ./tests/",
"typecheck": "tsc --noEmit"
},
"browserslist": ["chrome >= 93, edge >= 93, firefox >= 92, safari >= 15.4, not dead"],
"browserslist": [
">0.3%, chrome >= 64, edge >= 79, firefox >= 67, opera >= 51, safari >= 12 and not dead"
],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
+2 -21
View File
@@ -1,5 +1,4 @@
import { getVisitorAuthClaims, getVisitorAuthClaimsFromToken } from '@/lib/adaptive';
import { cache } from '@/lib/cache';
import { type SiteURLData, fetchSiteContextByURLLookup, getBaseContext } from '@/lib/context';
import { getDynamicCustomizationSettings } from '@/lib/customization';
import type { SiteAPIToken } from '@gitbook/api';
@@ -27,16 +26,6 @@ export type RouteParams = RouteLayoutParams & {
* Get the static context when rendering statically a site.
*/
export async function getStaticSiteContext(params: RouteLayoutParams) {
// Only the fields the context depends on — dropping pagePath so the layout, page and their
// metadata/viewport generators all share a single cached execution per request.
return fetchStaticSiteContext({
mode: params.mode,
siteURL: params.siteURL,
siteData: params.siteData,
});
}
const fetchStaticSiteContext = cache(async (params: RouteLayoutParams) => {
const siteURL = getSiteURLFromParams(params);
const siteURLData = getSiteURLDataFromParams(params);
@@ -60,21 +49,13 @@ const fetchStaticSiteContext = cache(async (params: RouteLayoutParams) => {
context,
visitorAuthClaims: getVisitorAuthClaimsFromToken(decoded),
};
});
}
/**
* Get the site context when rendering dynamically.
* The context will depend on the request.
*/
export async function getDynamicSiteContext(params: RouteLayoutParams) {
return fetchDynamicSiteContext({
mode: params.mode,
siteURL: params.siteURL,
siteData: params.siteData,
});
}
const fetchDynamicSiteContext = cache(async (params: RouteLayoutParams) => {
const siteURL = getSiteURLFromParams(params);
const siteURLData = getSiteURLDataFromParams(params);
@@ -93,7 +74,7 @@ const fetchDynamicSiteContext = cache(async (params: RouteLayoutParams) => {
context,
visitorAuthClaims: getVisitorAuthClaims(siteURLData),
};
});
}
/**
* Get the decoded page path from the params.
@@ -29,10 +29,15 @@ interface PageCoverImageProps {
export function PageCoverImage(props: PageCoverImageProps) {
const { imgs, y, height, mask } = props;
// The image is always rendered server-side (reserving space via aspect-ratio) so it stays
// discoverable by the preload scanner as the LCP element; the client probe only refines
// `objectPositionY` once real dimensions are known.
const { containerRef, objectPositionY } = useCoverPosition(imgs, y);
const { containerRef, objectPositionY, isLoading } = useCoverPosition(imgs, y);
if (isLoading) {
return (
<div className="h-full w-full overflow-hidden" ref={containerRef}>
<div className="h-full w-full animate-pulse bg-gradient-to-br from-gray-100 to-gray-200 dark:from-gray-800 dark:to-gray-900" />
</div>
);
}
return (
<div className="h-full w-full overflow-hidden" ref={containerRef} style={{ height }}>
@@ -28,6 +28,7 @@ interface Images {
export function useCoverPosition(imgs: Images, y: number) {
const containerRef = useRef<HTMLDivElement>(null);
const [loadedDimensions, setLoadedDimensions] = useState<ImageSize | null>(null);
const [isLoading, setIsLoading] = useState(!imgs.light.size && !imgs.dark?.size);
const container = useResizeObserver({
// @ts-expect-error wrong types
@@ -43,6 +44,8 @@ export function useCoverPosition(imgs: Images, y: number) {
return; // Already have dimensions
}
setIsLoading(true);
// Load the original image (using src, not srcSet) to get true dimensions
// Use dark image if available, otherwise fall back to light
const imageToLoad = imgs.dark || imgs.light;
@@ -52,6 +55,11 @@ export function useCoverPosition(imgs: Images, y: number) {
width: img.naturalWidth,
height: img.naturalHeight,
});
setIsLoading(false);
};
img.onerror = () => {
// If image fails to load, use a fallback
setIsLoading(false);
};
img.src = imageToLoad.src;
}, [imgs.light, imgs.dark]);
@@ -96,5 +104,6 @@ export function useCoverPosition(imgs: Images, y: number) {
return {
containerRef,
objectPositionY,
isLoading: !imageDimensions || isLoading,
};
}
@@ -20,10 +20,7 @@ interface RawIndexPage {
icon?: string;
emoji?: string;
description?: string;
/** Inlined breadcrumbs (version 1 responses). */
breadcrumbs?: Breadcrumb[];
/** Indices into the response-level `crumbs` table (version 2 responses). */
breadcrumbRefs?: number[];
}
/** FlexSearch-compatible document type — satisfies DocumentData via explicit index signature */
@@ -123,21 +120,7 @@ async function getOrBuildIndexes(indexURL: string): Promise<Map<string, Document
throw new Error(`Failed to fetch search index: ${response.status}`);
}
const data: { version: number; pages: RawIndexPage[]; crumbs?: Breadcrumb[] } =
await response.json();
// Version 2 dedupes breadcrumbs into a shared `crumbs` table; resolve the per-page
// references back to inline breadcrumbs so the rest of the code is version-agnostic.
if (data.crumbs) {
const crumbs = data.crumbs;
for (const page of data.pages) {
if (page.breadcrumbRefs) {
page.breadcrumbs = page.breadcrumbRefs
.map((index) => crumbs[index])
.filter((crumb): crumb is Breadcrumb => crumb !== undefined);
}
}
}
const data: { version: 1; pages: RawIndexPage[] } = await response.json();
// Group pages by their `lang` value (empty string for pages without one)
const pagesByLang = new Map<string, RawIndexPage[]>();
@@ -1,7 +1,6 @@
import type { GitBookSiteContext } from '@/lib/context';
import { CustomizationDefaultThemeMode } from '@gitbook/api';
import type { Metadata, Viewport } from 'next';
import Script from 'next/script';
import React from 'react';
import * as ReactDOM from 'react-dom';
@@ -40,6 +39,12 @@ export async function SiteLayout(props: {
ReactDOM.preconnect(GITBOOK_ASSETS_URL);
}
scripts.forEach(({ script }) => {
ReactDOM.preload(script, {
as: 'script',
});
});
return (
<SiteLayoutClientContexts
contextId={context.contextId}
@@ -71,9 +76,7 @@ export async function SiteLayout(props: {
<LoadIntegrations />
{scripts.length > 0
? scripts.map(({ script }) => (
<Script key={script} src={script} strategy="afterInteractive" />
))
? scripts.map(({ script }) => <script key={script} async src={script} />)
: null}
{scripts.some((script) => script.cookies) || customization.privacyPolicy.url ? (
@@ -159,7 +159,7 @@ export async function generateSitePageViewport(context: GitBookSiteContext): Pro
}
export async function generateSitePageMetadata(props: SitePageProps): Promise<Metadata> {
const { context, pageTarget } = await getPageDataWithFallback({
const { context, pageTarget, pageMetaLinks } = await getPageDataWithFallback({
context: props.context,
pagePathParams: props.pageParams,
});
@@ -174,8 +174,6 @@ export async function generateSitePageMetadata(props: SitePageProps): Promise<Me
const { page, ancestors } = pageTarget;
const { customization, revision, linker, imageResizer } = context;
const pageMetaLinks = await resolvePageMetaLinks(context, page.id);
const canonical = (
pageMetaLinks?.canonical
? new URL(
@@ -248,7 +246,7 @@ export async function generateSitePageMetadata(props: SitePageProps): Promise<Me
* Fetches all the data required to render the site page.
*/
export async function getSitePageData(props: SitePageProps) {
const { context, pageTarget } = await getPageDataWithFallback({
const { context, pageTarget, pageMetaLinks } = await getPageDataWithFallback({
context: props.context,
pagePathParams: props.pageParams,
});
@@ -289,12 +287,7 @@ export async function getSitePageData(props: SitePageProps) {
const withSections = Boolean(visibleSections && visibleSections.list.length > 0);
// The page document and its meta links are independent; resolve them concurrently to
// avoid stacking two round trips in front of the LCP content.
const [document, pageMetaLinks] = await Promise.all([
getPageDocument(context, page),
resolvePageMetaLinks(context, page.id),
]);
const document = await getPageDocument(context, page);
const iconStyle = getCustomizationIconStyle(customization);
const iconSources = await getInlineIconSources(
getContentInlineIconSourceRequests({
@@ -326,6 +319,9 @@ async function getPageDataWithFallback(args: {
}) {
const { context: baseContext, pagePathParams } = args;
const { context, pageTarget } = await fetchPageData(baseContext, pagePathParams);
const pageMetaLinks = await (pageTarget?.page
? resolvePageMetaLinks(context, pageTarget.page.id)
: null);
return {
context: {
@@ -333,6 +329,7 @@ async function getPageDataWithFallback(args: {
page: pageTarget?.page,
},
pageTarget,
pageMetaLinks,
};
}
@@ -8,7 +8,6 @@ import { Icon, type IconName } from '@gitbook/icons';
import leven from 'leven';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useEffect, useState } from 'react';
import { preload } from 'react-dom';
import { useAI } from '../AI';
import { PreservePageLayout } from '../PageBody/PreservePageLayout';
import { useSetSearchState } from '../Search';
@@ -42,11 +41,6 @@ export function SitePageNotFound() {
const setSearchState = useSetSearchState();
const { assistants } = useAI();
// getRelatedPages (below) fetches the site index on mount; preload it so the request starts
// before hydration. Scoped to the 404 page — the common way to hit a broken link cold —
// rather than the previous every-page preload.
preload(siteIndexURL, { as: 'fetch', type: 'application/json' });
// Show the assistant input when a non-search assistant is available (i.e. not just ask-AI).
const assistant = assistants.find((candidate) => candidate.mode !== 'search') ?? null;
const inputLabel = tString(language, assistant ? 'search_or_ask' : 'search');
@@ -69,7 +63,7 @@ export function SitePageNotFound() {
}
// Otherwise, rank the site's pages against the path that 404'd. We reuse the search index
// (preloaded above and CDN-cached), so this adds no extra origin request — see getRelatedPages.
// (already preloaded and CDN-cached), so this adds no origin request — see getRelatedPages.
let active = true;
getRelatedPages(siteIndexURL, pathname ?? '', siteSpaceId).then(
(pages) => {
@@ -249,8 +243,9 @@ type IndexPage = {
* Return the pages whose path is closest to the one that 404'd.
*
* Rather than asking the server (which would mean an extra request per 404), this reuses the
* CDN-cached search index served at `~gitbook/site-index`. The ranking is a lighter, client-side
* cousin of `getSimilarPages` (which the Markdown 404 runs server-side from the full page tree).
* search index served at `~gitbook/site-index` — already preloaded and CDN-cached on every page —
* so it's a cache hit, not an origin request. The ranking is a lighter, client-side cousin of
* `getSimilarPages` (which the Markdown 404 runs server-side from the full page tree).
*/
async function getRelatedPages(
indexURL: string,