Skip to content

Commit

Permalink
Build performance optimizations for projects with large sidebars (#2252)
Browse files Browse the repository at this point in the history
Co-authored-by: Kevin <[email protected]>
Co-authored-by: Chris Swithinbank <[email protected]>
  • Loading branch information
3 people authored Nov 8, 2024
1 parent a4c8edd commit 6116db0
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 41 deletions.
18 changes: 18 additions & 0 deletions packages/starlight/__tests__/basics/navigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
22 changes: 22 additions & 0 deletions packages/starlight/__tests__/i18n-sidebar/i18n-sidebar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
93 changes: 58 additions & 35 deletions packages/starlight/utils/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -121,32 +123,21 @@ 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);
// Inject current locale into link.
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;
Expand All @@ -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. */
Expand Down Expand Up @@ -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
);
Expand Down Expand Up @@ -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. */
Expand All @@ -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. */
Expand All @@ -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;
Expand Down
10 changes: 4 additions & 6 deletions packages/starlight/utils/starlight-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 6116db0

Please sign in to comment.