diff options
author | HiDeoo | 2024-11-08 15:45:29 +0100 |
---|---|---|
committer | GitHub | 2024-11-08 15:45:29 +0100 |
commit | 6116db03a4157c0f0caa210690ef0dcdd001a287 (patch) | |
tree | 22b464361541c383211edc6e42872eb56010b9ec | |
parent | a4c8eddc53993068c0f60159fecc123013827ef0 (diff) | |
download | IT.starlight-6116db03a4157c0f0caa210690ef0dcdd001a287.tar.gz IT.starlight-6116db03a4157c0f0caa210690ef0dcdd001a287.tar.bz2 IT.starlight-6116db03a4157c0f0caa210690ef0dcdd001a287.zip |
Build performance optimizations for projects with large sidebars (#2252)
Co-authored-by: Kevin <46791833+kevinzunigacuellar@users.noreply.github.com>
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
-rw-r--r-- | packages/starlight/__tests__/basics/navigation.test.ts | 18 | ||||
-rw-r--r-- | packages/starlight/__tests__/i18n-sidebar/i18n-sidebar.test.ts | 22 | ||||
-rw-r--r-- | packages/starlight/utils/navigation.ts | 93 | ||||
-rw-r--r-- | packages/starlight/utils/starlight-page.ts | 10 |
4 files changed, 102 insertions, 41 deletions
diff --git a/packages/starlight/__tests__/basics/navigation.test.ts b/packages/starlight/__tests__/basics/navigation.test.ts index 50496b4e..90676507 100644 --- a/packages/starlight/__tests__/basics/navigation.test.ts +++ b/packages/starlight/__tests__/basics/navigation.test.ts @@ -105,6 +105,24 @@ describe('getSidebar', () => { const homeLink = sidebar.find((item) => item.type === 'link' && item.href === '/'); expect(homeLink?.label).toBe('Home Page'); }); + + test('uses cached intermediate sidebars', async () => { + // Reset the modules registry so that re-importing `utils/navigation.ts` re-evaluates the + // module and clears the cache of intermediate sidebars from previous tests in this file. + vi.resetModules(); + const navigation = await import('../../utils/navigation'); + const routing = await import('../../utils/routing'); + + const getLocaleRoutes = vi.spyOn(routing, 'getLocaleRoutes'); + + navigation.getSidebar('/', undefined); + navigation.getSidebar('/environmental-impact/', undefined); + navigation.getSidebar('/guides/authoring-content/', undefined); + + expect(getLocaleRoutes).toHaveBeenCalledOnce(); + + getLocaleRoutes.mockRestore(); + }); }); describe('flattenSidebar', () => { diff --git a/packages/starlight/__tests__/i18n-sidebar/i18n-sidebar.test.ts b/packages/starlight/__tests__/i18n-sidebar/i18n-sidebar.test.ts index 52b7ff7b..341a20ed 100644 --- a/packages/starlight/__tests__/i18n-sidebar/i18n-sidebar.test.ts +++ b/packages/starlight/__tests__/i18n-sidebar/i18n-sidebar.test.ts @@ -270,4 +270,26 @@ describe('getSidebar', () => { const entry = sidebar.find((item) => item.type === 'link' && item.href === '/fr/manual-setup'); expect(entry?.label).toBe('Fait maison'); }); + test('uses intermediate sidebars cached by locales', async () => { + // Reset the modules registry so that re-importing `utils/navigation.ts` re-evaluates the + // module and clears the cache of intermediate sidebars from previous tests in this file. + vi.resetModules(); + const navigation = await import('../../utils/navigation'); + const routing = await import('../../utils/routing'); + + const getLocaleRoutes = vi.spyOn(routing, 'getLocaleRoutes'); + + const paths = ['/', '/environmental-impact/', '/guides/authoring-content/']; + + for (const path of paths) { + navigation.getSidebar(path, undefined); + navigation.getSidebar(path, 'fr'); + } + + expect(getLocaleRoutes).toHaveBeenCalledTimes(2); + expect(getLocaleRoutes).toHaveBeenNthCalledWith(1, undefined); + expect(getLocaleRoutes).toHaveBeenNthCalledWith(2, 'fr'); + + getLocaleRoutes.mockRestore(); + }); }); diff --git a/packages/starlight/utils/navigation.ts b/packages/starlight/utils/navigation.ts index ec2f9ee7..6c38c667 100644 --- a/packages/starlight/utils/navigation.ts +++ b/packages/starlight/utils/navigation.ts @@ -20,6 +20,8 @@ import type { StarlightConfig } from './user-config'; const DirKey = Symbol('DirKey'); const SlugKey = Symbol('SlugKey'); +const neverPathFormatter = createPathFormatter({ trailingSlash: 'never' }); + export interface Link { type: 'link'; label: string; @@ -73,11 +75,11 @@ function configItemToEntry( routes: Route[] ): SidebarEntry { if ('link' in item) { - return linkFromSidebarLinkItem(item, locale, currentPathname); + return linkFromSidebarLinkItem(item, locale); } else if ('autogenerate' in item) { return groupFromAutogenerateConfig(item, locale, routes, currentPathname); } else if ('slug' in item) { - return linkFromInternalSidebarLinkItem(item, locale, currentPathname); + return linkFromInternalSidebarLinkItem(item, locale); } else { const label = pickLang(item.translations, localeToLang(locale)) || item.label; return { @@ -121,11 +123,7 @@ function groupFromAutogenerateConfig( const isAbsolute = (link: string) => /^https?:\/\//.test(link); /** Create a link entry from a manual link item in user config. */ -function linkFromSidebarLinkItem( - item: SidebarLinkItem, - locale: string | undefined, - currentPathname: string -) { +function linkFromSidebarLinkItem(item: SidebarLinkItem, locale: string | undefined) { let href = item.link; if (!isAbsolute(href)) { href = ensureLeadingSlash(href); @@ -133,20 +131,13 @@ function linkFromSidebarLinkItem( if (locale) href = '/' + locale + href; } const label = pickLang(item.translations, localeToLang(locale)) || item.label; - return makeSidebarLink( - href, - label, - currentPathname, - getSidebarBadge(item.badge, locale, label), - item.attrs - ); + return makeSidebarLink(href, label, getSidebarBadge(item.badge, locale, label), item.attrs); } /** Create a link entry from an automatic internal link item in user config. */ function linkFromInternalSidebarLinkItem( item: InternalSidebarLinkItem, - locale: string | undefined, - currentPathname: string + locale: string | undefined ) { // Astro passes root `index.[md|mdx]` entries with a slug of `index` const slug = item.slug === 'index' ? '' : item.slug; @@ -169,50 +160,39 @@ function linkFromInternalSidebarLinkItem( } const label = pickLang(item.translations, localeToLang(locale)) || item.label || entry.entry.data.title; - return makeSidebarLink( - entry.slug, - label, - currentPathname, - getSidebarBadge(item.badge, locale, label), - item.attrs - ); + return makeSidebarLink(entry.slug, label, getSidebarBadge(item.badge, locale, label), item.attrs); } /** Process sidebar link options to create a link entry. */ function makeSidebarLink( href: string, label: string, - currentPathname: string, badge?: Badge, attrs?: LinkHTMLAttributes ): Link { if (!isAbsolute(href)) { href = formatPath(href); } - const isCurrent = pathsMatch(encodeURI(href), currentPathname); - return makeLink({ label, href, isCurrent, badge, attrs }); + return makeLink({ label, href, badge, attrs }); } /** Create a link entry */ function makeLink({ - isCurrent = false, attrs = {}, badge = undefined, ...opts }: { label: string; href: string; - isCurrent?: boolean; badge?: Badge | undefined; attrs?: LinkHTMLAttributes | undefined; }): Link { - return { type: 'link', ...opts, badge, isCurrent, attrs }; + return { type: 'link', ...opts, badge, isCurrent: false, attrs }; } /** Test if two paths are equivalent even if formatted differently. */ function pathsMatch(pathA: string, pathB: string) { - const format = createPathFormatter({ trailingSlash: 'never' }); - return format(pathA) === format(pathB); + return neverPathFormatter(pathA) === neverPathFormatter(pathB); } /** Get the segments leading to a page. */ @@ -268,11 +248,10 @@ function treeify(routes: Route[], baseDir: string): Dir { } /** Create a link entry for a given content collection entry. */ -function linkFromRoute(route: Route, currentPathname: string): Link { +function linkFromRoute(route: Route): Link { return makeSidebarLink( slugToPathname(route.slug), route.entry.data.sidebar.label || route.entry.data.title, - currentPathname, route.entry.data.sidebar.badge, route.entry.data.sidebar.attrs ); @@ -333,7 +312,7 @@ function dirToItem( ): SidebarEntry { return isDir(dirOrRoute) ? groupFromDir(dirOrRoute, fullPath, dirName, currentPathname, locale, collapsed) - : linkFromRoute(dirOrRoute, currentPathname); + : linkFromRoute(dirOrRoute); } /** Create a sidebar entry for a given content directory. */ @@ -348,9 +327,25 @@ function sidebarFromDir( ); } +/** + * Intermediate sidebar represents sidebar entries generated from the user config for a specific + * locale and do not contain any information about the current page. + * These representations are cached per locale to avoid regenerating them for each page. + * When generating the final sidebar for a page, the intermediate sidebar is cloned and the current + * page is marked as such. + * + * @see getSidebarFromIntermediateSidebar + */ +const intermediateSidebars = new Map<string | undefined, SidebarEntry[]>(); + /** Get the sidebar for the current page using the global config. */ export function getSidebar(pathname: string, locale: string | undefined): SidebarEntry[] { - return getSidebarFromConfig(config.sidebar, pathname, locale); + let intermediateSidebar = intermediateSidebars.get(locale); + if (!intermediateSidebar) { + intermediateSidebar = getSidebarFromConfig(config.sidebar, pathname, locale); + intermediateSidebars.set(locale, intermediateSidebar); + } + return getSidebarFromIntermediateSidebar(intermediateSidebar, pathname); } /** Get the sidebar for the current page using the specified sidebar config. */ @@ -368,6 +363,34 @@ export function getSidebarFromConfig( } } +/** Transform an intermediate sidebar into a sidebar for the current page. */ +function getSidebarFromIntermediateSidebar( + intermediateSidebar: SidebarEntry[], + pathname: string +): SidebarEntry[] { + const sidebar = structuredClone(intermediateSidebar); + setIntermediateSidebarCurrentEntry(sidebar, pathname); + return sidebar; +} + +/** Marks the current page as such in an intermediate sidebar. */ +function setIntermediateSidebarCurrentEntry( + intermediateSidebar: SidebarEntry[], + pathname: string +): boolean { + for (const entry of intermediateSidebar) { + if (entry.type === 'link' && pathsMatch(encodeURI(entry.href), pathname)) { + entry.isCurrent = true; + return true; + } + + if (entry.type === 'group' && setIntermediateSidebarCurrentEntry(entry.entries, pathname)) { + return true; + } + } + return false; +} + /** Generates a deterministic string based on the content of the passed sidebar. */ export function getSidebarHash(sidebar: SidebarEntry[]): string { let hash = 0; diff --git a/packages/starlight/utils/starlight-page.ts b/packages/starlight/utils/starlight-page.ts index a25a000f..42013e2a 100644 --- a/packages/starlight/utils/starlight-page.ts +++ b/packages/starlight/utils/starlight-page.ts @@ -12,7 +12,7 @@ import { } from './route-data'; import type { StarlightDocsEntry } from './routing'; import { slugToLocaleData, urlToSlug } from './slugs'; -import { getPrevNextLinks, getSidebarFromConfig } from './navigation'; +import { getPrevNextLinks, getSidebar, getSidebarFromConfig } from './navigation'; import { docsSchema } from '../schema'; import type { Prettify, RemoveIndexSignature } from './types'; import { DeprecatedLabelsPropProxy } from './i18n'; @@ -115,11 +115,9 @@ export async function generateStarlightPageRouteData({ const pageFrontmatter = await getStarlightPageFrontmatter(frontmatter); const id = `${stripLeadingAndTrailingSlashes(slug)}.md`; const localeData = slugToLocaleData(slug); - const sidebar = getSidebarFromConfig( - props.sidebar ? validateSidebarProp(props.sidebar) : config.sidebar, - url.pathname, - localeData.locale - ); + const sidebar = props.sidebar + ? getSidebarFromConfig(validateSidebarProp(props.sidebar), url.pathname, localeData.locale) + : getSidebar(url.pathname, localeData.locale); const headings = props.headings ?? []; const pageDocsEntry: StarlightPageDocsEntry = { id, |