Skip to content

Commit fa41af4

Browse files
rsbhclaude
andcommitted
perf: cache sorted page tree and precompute nav map
Tree sorting and nav computation ran on every /api/page request. Now cached after first call — getPageNav is O(1) map lookup. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 29b06e1 commit fa41af4

2 files changed

Lines changed: 27 additions & 12 deletions

File tree

packages/chronicle/src/lib/source.ts

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ function buildSyntheticMeta(): {
9797
}
9898

9999
let cachedSource: ReturnType<typeof loader> | null = null;
100+
let cachedTree: Root | null = null;
101+
let cachedNavMap: Map<string, PageNav> | null = null;
100102

101103
async function getSource() {
102104
if (cachedSource) return cachedSource;
@@ -112,6 +114,8 @@ export { getSource as source };
112114

113115
export function invalidate() {
114116
cachedSource = null;
117+
cachedTree = null;
118+
cachedNavMap = null;
115119
}
116120

117121
function getOrder(node: Node, orderMap: Map<string, number>): number | undefined {
@@ -146,8 +150,10 @@ function sortTreeByOrder(tree: Root, pages: { url: string; data: unknown }[]): R
146150
}
147151

148152
export async function getPageTree(): Promise<Root> {
153+
if (cachedTree) return cachedTree;
149154
const s = await getSource();
150-
return sortTreeByOrder(s.pageTree as Root, s.getPages());
155+
cachedTree = sortTreeByOrder(s.pageTree as Root, s.getPages());
156+
return cachedTree;
151157
}
152158

153159
export async function getPages() {
@@ -186,23 +192,32 @@ function titleFromUrl(url: string): string {
186192
.join(' ');
187193
}
188194

189-
export async function getPageNav(slug: string[], tree?: Root): Promise<PageNav> {
190-
const resolvedTree = tree ?? (await getPageTree());
191-
const pages = flattenTree(resolvedTree.children);
192-
const url = slug.length === 0 ? '/' : `/${slug.join('/')}`;
193-
const i = pages.findIndex(p => p.url === url);
194-
if (i < 0) return { prev: null, next: null };
195+
async function getNavMap(): Promise<Map<string, PageNav>> {
196+
if (cachedNavMap) return cachedNavMap;
197+
const tree = await getPageTree();
198+
const pages = flattenTree(tree.children);
195199
const toLink = (p: (typeof pages)[number]): PageNavLink => ({
196200
url: p.url,
197201
title:
198202
typeof p.name === 'string' && p.name.length > 0
199203
? p.name
200204
: titleFromUrl(p.url)
201205
});
202-
return {
203-
prev: i > 0 ? toLink(pages[i - 1]) : null,
204-
next: i < pages.length - 1 ? toLink(pages[i + 1]) : null
205-
};
206+
const navMap = new Map<string, PageNav>();
207+
for (let i = 0; i < pages.length; i++) {
208+
navMap.set(pages[i].url, {
209+
prev: i > 0 ? toLink(pages[i - 1]) : null,
210+
next: i < pages.length - 1 ? toLink(pages[i + 1]) : null
211+
});
212+
}
213+
cachedNavMap = navMap;
214+
return cachedNavMap;
215+
}
216+
217+
export async function getPageNav(slug: string[]): Promise<PageNav> {
218+
const navMap = await getNavMap();
219+
const url = slug.length === 0 ? '/' : `/${slug.join('/')}`;
220+
return navMap.get(url) ?? { prev: null, next: null };
206221
}
207222

208223
export function extractFrontmatter(page: { data: unknown }, fallbackTitle?: string): Frontmatter {

packages/chronicle/src/server/entry-server.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export default {
4545
getPageTree(),
4646
route.type === RouteType.DocsPage ? getPage(route.slug) : Promise.resolve(null),
4747
]);
48-
const nav = page ? await getPageNav(pageSlug, tree) : { prev: null, next: null };
48+
const nav = page ? await getPageNav(pageSlug) : { prev: null, next: null };
4949

5050
const relativePath = page ? getRelativePath(page) : null;
5151
const originalPath = page ? getOriginalPath(page) : null;

0 commit comments

Comments
 (0)