perf: memoize post queries and reduce JSON-LD bloat

This commit is contained in:
2026-03-15 17:31:23 +08:00
parent 1b495d2d2d
commit 5325a08bc3
4 changed files with 40 additions and 15 deletions

View File

@@ -145,8 +145,6 @@ export default async function BlogPostPage({ params }: Props) {
wordCount: wordCount, wordCount: wordCount,
readingTime: `${readingTime} min read`, readingTime: `${readingTime} min read`,
}), }),
articleBody: textContent.slice(0, 5000),
inLanguage: siteConfig.defaultLocale,
url: postUrl, url: postUrl,
}; };

View File

@@ -130,6 +130,8 @@ export default async function RootLayout({
<head> <head>
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" /> <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link rel="font" href="https://fonts.googleapis.com" />
<link rel="font" href="https://fonts.gstatic.com" />
</head> </head>
<body> <body>
<NextTopLoader <NextTopLoader

View File

@@ -1,11 +1,18 @@
import { allPosts, allPages, Post, Page } from 'contentlayer2/generated'; import { allPosts, allPages, Post, Page } from 'contentlayer2/generated';
let _sortedCache: Post[] | null = null;
let _relatedCache: Map<string, Post[]> = new Map();
let _neighborsCache: Map<string, { newer?: Post; older?: Post }> = new Map();
let _tagsCache: { tag: string; slug: string; count: number }[] | null = null;
export function getAllPostsSorted(): Post[] { export function getAllPostsSorted(): Post[] {
return [...allPosts].sort((a, b) => { if (_sortedCache) return _sortedCache;
_sortedCache = [...allPosts].sort((a, b) => {
const aDate = a.published_at ? new Date(a.published_at).getTime() : 0; const aDate = a.published_at ? new Date(a.published_at).getTime() : 0;
const bDate = b.published_at ? new Date(b.published_at).getTime() : 0; const bDate = b.published_at ? new Date(b.published_at).getTime() : 0;
return bDate - aDate; return bDate - aDate;
}); });
return _sortedCache;
} }
export function getPostBySlug(slug: string): Post | undefined { export function getPostBySlug(slug: string): Post | undefined {
@@ -38,24 +45,31 @@ export function getTagSlug(tag: string): string {
} }
export function getAllTagsWithCount(): { tag: string; slug: string; count: number }[] { export function getAllTagsWithCount(): { tag: string; slug: string; count: number }[] {
const map = new Map<string, number>(); if (_tagsCache) return _tagsCache;
const map = new Map<string, number>();
for (const post of allPosts) { for (const post of allPosts) {
if (!post.tags) continue; if (!post.tags) continue;
for (const tag of post.tags) { for (const postTag of post.tags) {
map.set(tag, (map.get(tag) ?? 0) + 1); map.set(postTag, (map.get(postTag) ?? 0) + 1);
} }
} }
return Array.from(map.entries()) _tagsCache = Array.from(map.entries())
.map(([tag, count]) => ({ tag, slug: getTagSlug(tag), count })) .map(([tag, count]) => ({ tag, slug: getTagSlug(tag), count }))
.sort((a, b) => { .sort((a, b) => {
if (b.count === a.count) return a.tag.localeCompare(b.tag); if (b.count === a.count) return a.tag.localeCompare(b.tag);
return b.count - a.count; return b.count - a.count;
}); });
return _tagsCache;
} }
export function getRelatedPosts(target: Post, limit = 3): Post[] { export function getRelatedPosts(target: Post, limit = 3): Post[] {
const cacheKey = `${target._id}-${limit}`;
if (_relatedCache.has(cacheKey)) {
return _relatedCache.get(cacheKey)!;
}
const targetTags = new Set(target.tags?.map((tag) => tag.toLowerCase()) ?? []); const targetTags = new Set(target.tags?.map((tag) => tag.toLowerCase()) ?? []);
const candidates = getAllPostsSorted().filter((post) => post._id !== target._id); const candidates = getAllPostsSorted().filter((post) => post._id !== target._id);
@@ -84,28 +98,39 @@ export function getRelatedPosts(target: Post, limit = 3): Post[] {
.slice(0, limit) .slice(0, limit)
.map((entry) => entry.post); .map((entry) => entry.post);
let result: Post[];
if (scored.length >= limit) { if (scored.length >= limit) {
return scored; result = scored;
} else {
const fallback = candidates.filter(
(post) => !scored.some((existing) => existing._id === post._id)
);
result = [...scored, ...fallback.slice(0, limit - scored.length)].slice(0, limit);
} }
const fallback = candidates.filter( _relatedCache.set(cacheKey, result);
(post) => !scored.some((existing) => existing._id === post._id) return result;
);
return [...scored, ...fallback.slice(0, limit - scored.length)].slice(0, limit);
} }
export function getPostNeighbors(target: Post): { export function getPostNeighbors(target: Post): {
newer?: Post; newer?: Post;
older?: Post; older?: Post;
} { } {
const cacheKey = target._id;
if (_neighborsCache.has(cacheKey)) {
return _neighborsCache.get(cacheKey)!;
}
const sorted = getAllPostsSorted(); const sorted = getAllPostsSorted();
const index = sorted.findIndex((post) => post._id === target._id); const index = sorted.findIndex((post) => post._id === target._id);
if (index === -1) return {}; if (index === -1) return {};
return { const result = {
newer: index > 0 ? sorted[index - 1] : undefined, newer: index > 0 ? sorted[index - 1] : undefined,
older: index < sorted.length - 1 ? sorted[index + 1] : undefined older: index < sorted.length - 1 ? sorted[index + 1] : undefined
}; };
_neighborsCache.set(cacheKey, result);
return result;
} }

2
next-env.d.ts vendored
View File

@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts"; import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.