diff --git a/.changeset/reduce-client-bundle.md b/.changeset/reduce-client-bundle.md index b34978d5b..b0e41572a 100644 --- a/.changeset/reduce-client-bundle.md +++ b/.changeset/reduce-client-bundle.md @@ -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. diff --git a/packages/gitbook/next.config.mjs b/packages/gitbook/next.config.mjs index 0a4fc9d78..992281a11 100644 --- a/packages/gitbook/next.config.mjs +++ b/packages/gitbook/next.config.mjs @@ -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: { diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json index 7cbc03ff7..8afd96e79 100644 --- a/packages/gitbook/package.json +++ b/packages/gitbook/package.json @@ -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/" diff --git a/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/site-index/route.ts b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/site-index/route.ts index 780c80207..7eeffbb1f 100644 Binary files a/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/site-index/route.ts and b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/site-index/route.ts differ diff --git a/packages/gitbook/src/app/utils.ts b/packages/gitbook/src/app/utils.ts index e219e0b1d..ca0c944fc 100644 --- a/packages/gitbook/src/app/utils.ts +++ b/packages/gitbook/src/app/utils.ts @@ -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. diff --git a/packages/gitbook/src/components/PageBody/PageCoverImage.tsx b/packages/gitbook/src/components/PageBody/PageCoverImage.tsx index bfa70e8e0..ae2d18c4b 100644 --- a/packages/gitbook/src/components/PageBody/PageCoverImage.tsx +++ b/packages/gitbook/src/components/PageBody/PageCoverImage.tsx @@ -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 ( +
+
+
+ ); + } return (
diff --git a/packages/gitbook/src/components/PageBody/useCoverPosition.ts b/packages/gitbook/src/components/PageBody/useCoverPosition.ts index 63de39f77..8814c2a92 100644 --- a/packages/gitbook/src/components/PageBody/useCoverPosition.ts +++ b/packages/gitbook/src/components/PageBody/useCoverPosition.ts @@ -28,6 +28,7 @@ interface Images { export function useCoverPosition(imgs: Images, y: number) { const containerRef = useRef(null); const [loadedDimensions, setLoadedDimensions] = useState(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, }; } diff --git a/packages/gitbook/src/components/Search/useLocalSearchResults.tsx b/packages/gitbook/src/components/Search/useLocalSearchResults.tsx index 5d95cc77c..2dc7e30e8 100644 --- a/packages/gitbook/src/components/Search/useLocalSearchResults.tsx +++ b/packages/gitbook/src/components/Search/useLocalSearchResults.tsx @@ -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 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(); diff --git a/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx b/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx index ec3edcc64..26983d017 100644 --- a/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx +++ b/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx @@ -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 ( {scripts.length > 0 - ? scripts.map(({ script }) => ( -