Add performance optimizations and component refactors
CI / check (push) Successful in 34s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m23s

- Cache read-heavy public API routes via redisCache (leaderboard, values,
  shop, articles, photos, guilds, teams, staff, users, home, radio, badges)
- Add single-flight and bounded-memory cache layer with unit tests
- Parallelize independent DB queries on search, rares, shop, staff, polls
  and profile pages
- Push radio points leaderboard aggregation to SQL with a LIMIT
- Split studio-client and import-furni-client into focused modules
- Clean up next.config.ts
This commit is contained in:
openhands committed 2026-08-17 22:02:21 +02:00
1 parent 54f2bc5e0c
commit f285a7cd98
42 files changed
+1616 -1205

No files matched your search

+29 -16
View File
@@ -1,6 +1,7 @@
import { eq } from "drizzle-orm";
import { apiJson } from "@/lib/api";
import { db, WebsiteArticles } from "@/lib/db";
import { apiCacheKey, cacheSafe, redisCache } from "@/lib/redis-cache";
/**
* GET /api/articles/:slug — single website_article by slug, including the
@@ -13,26 +14,38 @@ export async function GET(
const { slug } = await params;
try {
const [article] = await db
.select({
id: WebsiteArticles.id,
title: WebsiteArticles.title,
slug: WebsiteArticles.slug,
shortStory: WebsiteArticles.shortStory,
fullStory: WebsiteArticles.fullStory,
image: WebsiteArticles.image,
createdAt: WebsiteArticles.createdAt,
updatedAt: WebsiteArticles.updatedAt,
})
.from(WebsiteArticles)
.where(eq(WebsiteArticles.slug, slug))
.limit(1);
const data = await redisCache(
apiCacheKey(`articles:${slug}`),
60,
async () => {
const [article] = await db
.select({
id: WebsiteArticles.id,
title: WebsiteArticles.title,
slug: WebsiteArticles.slug,
shortStory: WebsiteArticles.shortStory,
fullStory: WebsiteArticles.fullStory,
image: WebsiteArticles.image,
createdAt: WebsiteArticles.createdAt,
updatedAt: WebsiteArticles.updatedAt,
})
.from(WebsiteArticles)
.where(eq(WebsiteArticles.slug, slug))
.limit(1);
if (!article) {
if (!article) {
return null;
}
return cacheSafe({ data: article });
},
);
if (data === null) {
return apiJson({ error: "Article not found" }, { status: 404 });
}
return apiJson({ data: article });
return apiJson(data);
} catch {
// DB unavailable — treat as not found rather than a 500.
return apiJson({ error: "Article not found" }, { status: 200 });
+36 -26
View File
@@ -1,6 +1,7 @@
import { count, desc } from "drizzle-orm";
import { apiJson, pagination } from "@/lib/api";
import { db, WebsiteArticles } from "@/lib/db";
import { apiCacheKey, cacheSafe, redisCache } from "@/lib/redis-cache";
/**
* GET /api/articles — paginated list of website_articles, newest first.
@@ -12,33 +13,42 @@ export async function GET(req: Request) {
const { page, perPage, skip, take } = pagination(sp);
try {
const [totalRows, articles] = await Promise.all([
db.select({ total: count() }).from(WebsiteArticles),
db
.select({
id: WebsiteArticles.id,
title: WebsiteArticles.title,
slug: WebsiteArticles.slug,
shortStory: WebsiteArticles.shortStory,
image: WebsiteArticles.image,
createdAt: WebsiteArticles.createdAt,
})
.from(WebsiteArticles)
.orderBy(desc(WebsiteArticles.createdAt))
.limit(take)
.offset(skip),
]);
const total = totalRows[0]?.total ?? 0;
return apiJson({
data: articles,
meta: {
page,
perPage,
total,
lastPage: Math.max(1, Math.ceil(total / perPage)),
const data = await redisCache(
apiCacheKey(`articles:${page}:${perPage}`),
60,
async () => {
const [totalRows, articles] = await Promise.all([
db.select({ total: count() }).from(WebsiteArticles),
db
.select({
id: WebsiteArticles.id,
title: WebsiteArticles.title,
slug: WebsiteArticles.slug,
shortStory: WebsiteArticles.shortStory,
image: WebsiteArticles.image,
createdAt: WebsiteArticles.createdAt,
})
.from(WebsiteArticles)
.orderBy(desc(WebsiteArticles.createdAt))
.limit(take)
.offset(skip),
]);
return cacheSafe({
data: articles,
meta: {
page,
perPage,
total: totalRows[0]?.total ?? 0,
lastPage: Math.max(
1,
Math.ceil((totalRows[0]?.total ?? 0) / perPage),
),
},
});
},
});
);
return apiJson(data);
} catch {
// DB unavailable — return an empty payload instead of a 500.
return apiJson(