🚀 perf: add global content cache to significantly improve build speed, fix slow build times on [...tags_tag] pages

This commit is contained in:
radishzzz 2025-05-08 16:57:14 +01:00
parent 5b9c2d562a
commit 625879b061
3 changed files with 130 additions and 46 deletions

View file

@ -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)
---
<Layout supportedLangs={supportedLangs}>

17
src/utils/cache.ts Normal file
View file

@ -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<T>(fn: (...args: any[]) => Promise<T>) {
const cache = new Map<string, Promise<T>>()
return async (...args: any[]): Promise<T> => {
const key = JSON.stringify(args) || 'default'
if (!cache.has(key)) {
cache.set(key, fn(...args))
}
return cache.get(key)!
}
}

View file

@ -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<string, { minutes: number }>()
async function addMetaToPost(post: CollectionEntry<'posts'>): Promise<Post> {
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<string[]> {
async function _checkPostSlugDuplication(posts: CollectionEntry<'posts'>[]): Promise<string[]> {
const slugMap = new Map<string, Set<string>>()
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<Map<number, Post[]>> {
async function _getPostsByYear(lang?: string): Promise<Map<number, Post[]>> {
const posts = await getRegularPosts(lang)
const yearMap = new Map<number, Post[]>()
@ -122,26 +163,15 @@ export async function getPostsByYear(lang?: string): Promise<Map<number, Post[]>
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<string, Post[]>()
@ -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)