perf(packages): 优化 /api/packages 7000+ 条响应性能

- buildPackages 缓存全量结果(rawCache/ignored/threshold 不变时复用)

- gzip 压缩大响应(Node 内置 zlib,减少传输体积 60-80%)
This commit is contained in:
yinjun.chen1
2026-09-28 11:04:21 +08:00
parent 76f1ad42b2
commit b250ecbf55
2 changed files with 55 additions and 22 deletions
+14 -2
View File
@@ -28,6 +28,9 @@ export function isDemoMode() {
let rawCache = null
let rawCacheAt = null
// 包列表构建缓存(rawCache/ignored/threshold 不变时复用,避免每次请求全量 map 7000+ 条)
let packagesCache = null
// —— 异步扫描状态(后台重扫时供前端轮询进度) ——
export const scanState = {
scanning: false, done: false, packages: 0, bytes: 0, tgzBytes: 0, diskBytes: null,
@@ -227,6 +230,7 @@ export function hasScanCache() {
export function invalidateScan() {
rawCache = null
rawCacheAt = null
packagesCache = null // 清空包列表构建缓存
clearPkgCache() // storage 目录切换 / 清理动作后废弃增量缓存,避免旧目录条目残留
}
@@ -235,6 +239,7 @@ export function getScannedAt() {
}
// 构建包列表(统一状态计算 + 排序 + 忽略标记 + 来源筛选)
// 缓存全量列表:rawCache/ignored/threshold 不变时复用,避免每次请求全量 map 7000+ 条
export function buildPackages(keyword = '', origin = 'all') {
const threshold = Number(getSetting('version_threshold', DEFAULT_VERSION_THRESHOLD))
const ignoredRows = getIgnoredRows()
@@ -242,7 +247,11 @@ export function buildPackages(keyword = '', origin = 'all') {
const ignoredAtMap = Object.fromEntries(ignoredRows.map(r => [r.pkg_name, (r.ignored_at || '').slice(0, 10)]))
const raw = getRawPackages() || []
let list = raw.map(p => {
// 缓存 key:raw 长度 + ignored 内容 hash + threshold(任一变化即失效)
const ignoredHash = ignoredRows.map(r => `${r.pkg_name}:${r.ignored_at || ''}`).join('|')
const cacheKey = `${raw.length}_${ignoredHash}_${threshold}`
if (!packagesCache || packagesCache.key !== cacheKey) {
const list = raw.map(p => {
let status
if (p.status === 'broken') status = 'broken'
else if (ignoredSet.has(p.name)) status = 'ignored'
@@ -263,6 +272,9 @@ export function buildPackages(keyword = '', origin = 'all') {
latest: p.latest || null,
}
})
packagesCache = { key: cacheKey, list }
}
let list = packagesCache.list
// 检索:不区分大小写子串,匹配包名(版本号匹配由前端本地完成,避免列表携带明细)
const kw = keyword.trim().toLowerCase()
@@ -276,7 +288,7 @@ export function buildPackages(keyword = '', origin = 'all') {
// 排序:预警置顶 → 正常 → 忽略 → 异常;同状态按版本数降序
const rank = { warn: 0, ok: 1, ignored: 2, broken: 3 }
list.sort((a, b) => {
list = [...list].sort((a, b) => {
if (rank[a.status] !== rank[b.status]) return rank[a.status] - rank[b.status]
return b.versions - a.versions
})
+21
View File
@@ -20,6 +20,7 @@ process.env.UV_THREADPOOL_SIZE = process.env.UV_THREADPOOL_SIZE || '8'
import express from 'express'
import path from 'node:path'
import fs from 'node:fs'
import zlib from 'node:zlib'
import { fileURLToPath } from 'node:url'
import { resolveStorageDir } from './scanner.js'
import { isDemoMode, scanState, runScanAsync } from './scan-manager.js'
@@ -40,6 +41,26 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
const app = express()
app.use(express.json({ limit: '1mb' }))
// gzip 压缩:大响应(如 /api/packages 7000+ 条)减少传输体积 60-80%,Node 内置 zlib 无需新依赖
app.use((req, res, next) => {
const accept = req.headers['accept-encoding'] || ''
if (!accept.includes('gzip')) return next()
const originalJson = res.json.bind(res)
res.json = (data) => {
try {
const json = JSON.stringify(data)
if (json.length < 1024) return originalJson(data) // 小响应不压缩,避免开销
const compressed = zlib.gzipSync(json)
res.setHeader('Content-Encoding', 'gzip')
res.setHeader('Content-Type', 'application/json')
res.send(compressed)
} catch {
originalJson(data)
}
}
next()
})
const PORT = Number(process.env.PORT || 3000)
// —— 路由装配:登录/登出/主题公开,其余 /api/* 需鉴权 ——