perf(server): response优化为gzip压缩

添加了响应内容压缩中间件,支持对可压缩类型的响应进行gzip压缩,减少传输体积提升性能,同时添加了对应的测试用例
This commit is contained in:
xiaojunnuo
2026-08-27 22:09:18 +08:00
parent d87821ee02
commit f38522ca0b
3 changed files with 95 additions and 0 deletions
@@ -22,6 +22,7 @@ import * as upload from "@midwayjs/upload";
import { setLogger } from "@certd/acme-client";
import { HiddenMiddleware } from "./middleware/hidden.js";
import { shouldSetDefaultNoCache } from "./configuration-cache.js";
import { compressResponse } from "./middleware/compression.js";
// import * as swagger from '@midwayjs/swagger';
//@ts-ignore
// process.env.UV_THREADPOOL_SIZE = 2
@@ -90,6 +91,7 @@ export class MainConfiguration {
origin: "*",
})
);
this.app.use(compressResponse);
//
// this.app.use(async (ctx, next) => {
// // 只在返回 'index.html' 的时候设置 maxAge
@@ -0,0 +1,25 @@
import assert from "node:assert/strict";
import { shouldCompress } from "./compression.js";
function createContext(overrides: Record<string, unknown> = {}) {
const headers: Record<string, string> = { "Accept-Encoding": "gzip" };
return {
method: "GET",
status: 200,
body: { data: "response" },
request: { get: (name: string) => headers[name] },
response: { get: (name: string) => headers[name] },
...overrides,
} as any;
}
describe("compression middleware", () => {
it("compresses gzip-capable API responses", () => {
assert.equal(shouldCompress(createContext()), true);
});
it("skips responses without gzip support or downloads", () => {
assert.equal(shouldCompress(createContext({ request: { get: () => "br" } })), false);
assert.equal(shouldCompress(createContext({ response: { get: (name: string) => (name === "Content-Disposition" ? "attachment" : undefined) } })), false);
});
});
@@ -0,0 +1,68 @@
import { gzip } from "node:zlib";
import { promisify } from "node:util";
import { IMidwayKoaContext, NextFunction } from "@midwayjs/koa";
const gzipAsync = promisify(gzip);
const MIN_COMPRESS_SIZE = 1024;
function acceptsGzip(header: string | undefined): boolean {
if (!header) {
return false;
}
const encodings = header.split(",").map(value => value.trim().split(";"));
const gzipEncoding = encodings.find(([encoding]) => encoding === "gzip") ?? encodings.find(([encoding]) => encoding === "*");
if (!gzipEncoding) {
return false;
}
const quality = gzipEncoding[1]?.trim().replace(/^q=/, "");
return quality !== "0";
}
function isCompressibleContentType(contentType: string | undefined): boolean {
if (!contentType) {
return true;
}
const type = contentType.split(";", 1)[0].trim().toLowerCase();
return type.startsWith("text/") || type.includes("json") || type.includes("javascript") || type.includes("xml") || type.includes("svg");
}
export function shouldCompress(ctx: Pick<IMidwayKoaContext, "method" | "status" | "response" | "request" | "body">): boolean {
if (ctx.method === "HEAD" || ctx.status === 204 || ctx.status === 304) {
return false;
}
if (ctx.response.get("Content-Encoding") || ctx.response.get("Content-Disposition")) {
return false;
}
if (!acceptsGzip(ctx.request.get("Accept-Encoding"))) {
return false;
}
if (!isCompressibleContentType(ctx.response.get("Content-Type"))) {
return false;
}
if (Buffer.isBuffer(ctx.body) && !ctx.response.get("Content-Type")) {
return false;
}
return typeof ctx.body === "string" || Buffer.isBuffer(ctx.body) || (ctx.body !== null && typeof ctx.body === "object");
}
export async function compressResponse(ctx: IMidwayKoaContext, next: NextFunction): Promise<void> {
await next();
if (!shouldCompress(ctx)) {
return;
}
const contentType = ctx.response.get("Content-Type");
const body = Buffer.isBuffer(ctx.body) ? ctx.body : Buffer.from(typeof ctx.body === "string" ? ctx.body : JSON.stringify(ctx.body));
if (body.length < MIN_COMPRESS_SIZE) {
return;
}
const compressed = await gzipAsync(body);
ctx.body = compressed;
if (contentType) {
ctx.response.set("Content-Type", contentType);
}
ctx.response.set("Content-Encoding", "gzip");
ctx.response.set("Vary", "Accept-Encoding");
ctx.response.remove("Content-Length");
}