summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHiDeoo2024-11-08 15:45:29 +0100
committerGitHub2024-11-08 15:45:29 +0100
commit6116db03a4157c0f0caa210690ef0dcdd001a287 (patch)
tree22b464361541c383211edc6e42872eb56010b9ec
parenta4c8eddc53993068c0f60159fecc123013827ef0 (diff)
downloadIT.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.ts18
-rw-r--r--packages/starlight/__tests__/i18n-sidebar/i18n-sidebar.test.ts22
-rw-r--r--packages/starlight/utils/navigation.ts93
-rw-r--r--packages/starlight/utils/starlight-page.ts10
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,