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

+21 -13
View File
@@ -3,22 +3,30 @@
import { asc } from "drizzle-orm";
import { apiJson } from "@/lib/api";
import { db, WebsiteShopCategories } from "@/lib/db";
import { apiCacheKey, cacheSafe, redisCache } from "@/lib/redis-cache";
export async function GET(_req: Request) {
try {
const data = await db
.select({
id: WebsiteShopCategories.id,
name: WebsiteShopCategories.name,
description: WebsiteShopCategories.description,
icon: WebsiteShopCategories.icon,
order: WebsiteShopCategories.order,
})
.from(WebsiteShopCategories)
.orderBy(
asc(WebsiteShopCategories.order),
asc(WebsiteShopCategories.name),
);
const data = await redisCache(
apiCacheKey("shop:categories"),
60,
async () =>
cacheSafe(
await db
.select({
id: WebsiteShopCategories.id,
name: WebsiteShopCategories.name,
description: WebsiteShopCategories.description,
icon: WebsiteShopCategories.icon,
order: WebsiteShopCategories.order,
})
.from(WebsiteShopCategories)
.orderBy(
asc(WebsiteShopCategories.order),
asc(WebsiteShopCategories.name),
),
),
);
return apiJson({ data });
} catch {
+49 -39
View File
@@ -4,6 +4,7 @@
import { asc, count, eq } from "drizzle-orm";
import { apiError, apiJson, pagination, positiveBigInt } from "@/lib/api";
import { db, WebsiteShopArticles } from "@/lib/db";
import { apiCacheKey, cacheSafe, redisCache } from "@/lib/redis-cache";
export async function GET(req: Request) {
const sp = new URL(req.url).searchParams;
@@ -24,46 +25,55 @@ export async function GET(req: Request) {
? eq(WebsiteShopArticles.categoryId, categoryId)
: undefined;
const [totalRows, data] = await Promise.all([
db.select({ total: count() }).from(WebsiteShopArticles).where(where),
db
.select({
id: WebsiteShopArticles.id,
categoryId: WebsiteShopArticles.categoryId,
name: WebsiteShopArticles.name,
info: WebsiteShopArticles.info,
iconUrl: WebsiteShopArticles.iconUrl,
color: WebsiteShopArticles.color,
costs: WebsiteShopArticles.costs,
giveRank: WebsiteShopArticles.giveRank,
isGiftable: WebsiteShopArticles.isGiftable,
credits: WebsiteShopArticles.credits,
duckets: WebsiteShopArticles.duckets,
diamonds: WebsiteShopArticles.diamonds,
badges: WebsiteShopArticles.badges,
furniture: WebsiteShopArticles.furniture,
position: WebsiteShopArticles.position,
})
.from(WebsiteShopArticles)
.where(where)
.orderBy(
asc(WebsiteShopArticles.position),
asc(WebsiteShopArticles.name),
)
.limit(take)
.offset(skip),
]);
const total = totalRows[0]?.total ?? 0;
return apiJson({
data,
meta: {
page,
perPage,
total,
lastPage: Math.max(1, Math.ceil(total / perPage)),
const data = await redisCache(
apiCacheKey(`shop:${categoryId ?? "all"}:${page}:${perPage}`),
60,
async () => {
const [totalRows, rows] = await Promise.all([
db.select({ total: count() }).from(WebsiteShopArticles).where(where),
db
.select({
id: WebsiteShopArticles.id,
categoryId: WebsiteShopArticles.categoryId,
name: WebsiteShopArticles.name,
info: WebsiteShopArticles.info,
iconUrl: WebsiteShopArticles.iconUrl,
color: WebsiteShopArticles.color,
costs: WebsiteShopArticles.costs,
giveRank: WebsiteShopArticles.giveRank,
isGiftable: WebsiteShopArticles.isGiftable,
credits: WebsiteShopArticles.credits,
duckets: WebsiteShopArticles.duckets,
diamonds: WebsiteShopArticles.diamonds,
badges: WebsiteShopArticles.badges,
furniture: WebsiteShopArticles.furniture,
position: WebsiteShopArticles.position,
})
.from(WebsiteShopArticles)
.where(where)
.orderBy(
asc(WebsiteShopArticles.position),
asc(WebsiteShopArticles.name),
)
.limit(take)
.offset(skip),
]);
return cacheSafe({
data: rows,
meta: {
page,
perPage,
total: totalRows[0]?.total ?? 0,
lastPage: Math.max(
1,
Math.ceil((totalRows[0]?.total ?? 0) / perPage),
),
},
});
},
});
);
return apiJson(data);
} catch {
// DB unreachable — never 500; return an empty, well-formed payload.
return apiJson(