From 625879b0617721dcebd45e39b7989141dcc78ad7 Mon Sep 17 00:00:00 2001 From: radishzzz Date: Thu, 8 May 2025 16:57:14 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20perf:=20add=20global=20content?= =?UTF-8?q?=20cache=20to=20significantly=20improve=20build=20speed,=20fix?= =?UTF-8?q?=20slow=20build=20times=20on=20[...tags=5Ftag]=20pages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/[...tags_tag].astro | 16 +--- src/utils/cache.ts | 17 ++++ src/utils/content.ts | 143 ++++++++++++++++++++++++++-------- 3 files changed, 130 insertions(+), 46 deletions(-) create mode 100644 src/utils/cache.ts diff --git a/src/pages/[...tags_tag].astro b/src/pages/[...tags_tag].astro index 046fd55..b36e7b1 100644 --- a/src/pages/[...tags_tag].astro +++ b/src/pages/[...tags_tag].astro @@ -1,9 +1,9 @@ --- import PostList from '@/components/PostList.astro' -import { allLocales, defaultLocale, moreLocales } from '@/config' +import { defaultLocale, moreLocales } from '@/config' import { getTagPath } from '@/i18n/path' import Layout from '@/layouts/Layout.astro' -import { getAllTags, getPostsByTag } from '@/utils/content' +import { getAllTags, getPostsByTag, getTagSupportedLangs } from '@/utils/content' export async function getStaticPaths() { type PathItem = { @@ -39,17 +39,7 @@ export async function getStaticPaths() { const { tag, lang } = Astro.props const posts = await getPostsByTag(tag, lang) const allTags = await getAllTags(lang) - -// Check if tag has posts in each language, return language code if exists, null if not -const tagSupportedLangs = await Promise.all( - allLocales.map(async (locale) => { - const postsInLang = await getPostsByTag(tag, locale) - return postsInLang.length > 0 ? locale : null - }), -) - -// Filter to get supported languages -const supportedLangs = tagSupportedLangs.filter(Boolean) as string[] +const supportedLangs = await getTagSupportedLangs(tag) --- diff --git a/src/utils/cache.ts b/src/utils/cache.ts new file mode 100644 index 0000000..06ec00a --- /dev/null +++ b/src/utils/cache.ts @@ -0,0 +1,17 @@ +/** + * Memoization decorator - provides in-memory caching for async functions + * + * @param fn The original async function to be memoized + * @returns A function wrapper with caching capability + */ +export function memoize(fn: (...args: any[]) => Promise) { + const cache = new Map>() + + return async (...args: any[]): Promise => { + const key = JSON.stringify(args) || 'default' + if (!cache.has(key)) { + cache.set(key, fn(...args)) + } + return cache.get(key)! + } +} diff --git a/src/utils/content.ts b/src/utils/content.ts index 49f13f9..2f52214 100644 --- a/src/utils/content.ts +++ b/src/utils/content.ts @@ -1,7 +1,26 @@ import type { CollectionEntry } from 'astro:content' import { defaultLocale } from '@/config' +import { memoize } from '@/utils/cache' import { getCollection, render } from 'astro:content' +/** + * Core Functions + * - addMetaToPost + * - getPosts + * - checkPostSlugDuplication + * + * Post Filtering + * - getRegularPosts + * - getPinnedPosts + * - getPostsByYear + * + * Tag Related + * - getPostsGroupByTags + * - getAllTags + * - getPostsByTag + * - getTagSupportedLangs + */ + // Type definitions export type Post = CollectionEntry<'posts'> & { remarkPluginFrontmatter: { @@ -9,18 +28,32 @@ export type Post = CollectionEntry<'posts'> & { } } -// Add metadata including reading time to post +/** + * Add metadata including reading time to a post + * @param post The post to enhance with metadata + * @returns Enhanced post with reading time information + */ +const metaCache = new Map() async function addMetaToPost(post: CollectionEntry<'posts'>): Promise { - const { remarkPluginFrontmatter } = await render(post) - return { ...post, remarkPluginFrontmatter: remarkPluginFrontmatter as { minutes: number } } + const cacheKey = `${post.id}-${post.data.lang || 'universal'}` + + if (!metaCache.has(cacheKey)) { + const { remarkPluginFrontmatter } = await render(post) + metaCache.set(cacheKey, remarkPluginFrontmatter as { minutes: number }) + } + + return { + ...post, + remarkPluginFrontmatter: metaCache.get(cacheKey)!, + } } /** * Find duplicate post slugs within the same language - * @param posts Array of blog posts + * @param posts Array of blog posts to check * @returns Array of descriptive error messages for duplicate slugs */ -export async function checkPostSlugDuplication(posts: CollectionEntry<'posts'>[]): Promise { +async function _checkPostSlugDuplication(posts: CollectionEntry<'posts'>[]): Promise { const slugMap = new Map>() const duplicates: string[] = [] @@ -48,13 +81,15 @@ export async function checkPostSlugDuplication(posts: CollectionEntry<'posts'>[] return duplicates } +// Export memoized version +export const checkPostSlugDuplication = memoize(_checkPostSlugDuplication) /** * Get all posts (including pinned ones, excluding drafts in production) - * @param lang Language code, optional, defaults to site's default language + * @param lang The language code to filter by, defaults to site's default language * @returns Posts filtered by language, enhanced with metadata, sorted by date */ -export async function getPosts(lang?: string) { +async function _getPosts(lang?: string) { const currentLang = lang || defaultLocale const filteredPosts = await getCollection( @@ -72,35 +107,41 @@ export async function getPosts(lang?: string) { b.data.published.valueOf() - a.data.published.valueOf(), ) } +// Export memoized version +export const getPosts = memoize(_getPosts) /** * Get all non-pinned posts - * @param lang Language code, optional, defaults to site's default language - * @returns Regular posts (not pinned), already filtered by language and drafts + * @param lang The language code to filter by, defaults to site's default language + * @returns Regular posts (non-pinned), filtered by language */ -export async function getRegularPosts(lang?: string) { +async function _getRegularPosts(lang?: string) { const posts = await getPosts(lang) return posts.filter(post => !post.data.pin) } +// Export memoized version +export const getRegularPosts = memoize(_getRegularPosts) /** - * Get pinned posts and sort by pin priority - * @param lang Language code, optional, defaults to site's default language + * Get pinned posts sorted by pin priority + * @param lang The language code to filter by, defaults to site's default language * @returns Pinned posts sorted by pin value in descending order */ -export async function getPinnedPosts(lang?: string) { +async function _getPinnedPosts(lang?: string) { const posts = await getPosts(lang) return posts .filter(post => post.data.pin && post.data.pin > 0) .sort((a, b) => (b.data.pin || 0) - (a.data.pin || 0)) } +// Export memoized version +export const getPinnedPosts = memoize(_getPinnedPosts) /** * Group posts by year and sort within each year - * @param lang Language code, optional, defaults to site's default language - * @returns Map of posts grouped by year (descending), with posts in each year sorted by date (descending) + * @param lang The language code to filter by, defaults to site's default language + * @returns Map of posts grouped by year (descending), sorted by date within each year */ -export async function getPostsByYear(lang?: string): Promise> { +async function _getPostsByYear(lang?: string): Promise> { const posts = await getRegularPosts(lang) const yearMap = new Map() @@ -122,26 +163,15 @@ export async function getPostsByYear(lang?: string): Promise return new Map([...yearMap.entries()].sort((a, b) => b[0] - a[0])) } - -/** - * Get all tags sorted by post count - * @param lang Language code, optional, defaults to site's default language - * @returns Array of tags sorted by popularity (most posts first) - */ -export async function getAllTags(lang?: string) { - const tagMap = await getPostsGroupByTags(lang) - const tagsWithCount = Array.from(tagMap.entries()) - - tagsWithCount.sort((a, b) => b[1].length - a[1].length) - return tagsWithCount.map(([tag]) => tag) -} +// Export memoized version +export const getPostsByYear = memoize(_getPostsByYear) /** * Group posts by their tags - * @param lang Language code, optional, defaults to site's default language + * @param lang The language code to filter by, defaults to site's default language * @returns Map where keys are tag names and values are arrays of posts with that tag */ -export async function getPostsGroupByTags(lang?: string) { +async function _getPostsGroupByTags(lang?: string) { const posts = await getPosts(lang) const tagMap = new Map() @@ -156,14 +186,61 @@ export async function getPostsGroupByTags(lang?: string) { return tagMap } +// Export memoized version +export const getPostsGroupByTags = memoize(_getPostsGroupByTags) + +/** + * Get all tags sorted by post count + * @param lang The language code to filter by, defaults to site's default language + * @returns Array of tags sorted by popularity (most posts first) + */ +async function _getAllTags(lang?: string) { + const tagMap = await getPostsGroupByTags(lang) + const tagsWithCount = Array.from(tagMap.entries()) + + tagsWithCount.sort((a, b) => b[1].length - a[1].length) + return tagsWithCount.map(([tag]) => tag) +} +// Export memoized version +export const getAllTags = memoize(_getAllTags) /** * Get all posts that contain a specific tag * @param tag The tag name to filter posts by - * @param lang Language code, optional, defaults to site's default language + * @param lang The language code to filter by, defaults to site's default language * @returns Array of posts that contain the specified tag */ -export async function getPostsByTag(tag: string, lang?: string) { +async function _getPostsByTag(tag: string, lang?: string) { const tagMap = await getPostsGroupByTags(lang) return tagMap.get(tag) || [] } +// Export memoized version +export const getPostsByTag = memoize(_getPostsByTag) + +/** + * Check which languages support a specific tag + * @param tag The tag name to check language support for + * @returns Array of language codes that support the specified tag + */ +async function _getTagSupportedLangs(tag: string) { + const posts = await getCollection( + 'posts', + ({ data }) => !data.draft, + ) + const supportedLangs = [] + const { allLocales } = await import('@/config') + + for (const locale of allLocales) { + const hasPostsWithTag = posts.some(post => + post.data.tags?.includes(tag) + && (post.data.lang === locale || post.data.lang === ''), + ) + if (hasPostsWithTag) { + supportedLangs.push(locale) + } + } + + return supportedLangs +} +// Export memoized version +export const getTagSupportedLangs = memoize(_getTagSupportedLangs)