perf(server): compress the web app's static files and HTML shells

Self-hosted Docker installs served the SPA's CSS, JS and HTML shells without any Content-Encoding. Hono's compress() now wraps only the web routes: it is registered after every API, MCP and upload route, so their streams are never buffered or re-encoded. The Vercel app keeps relying on its CDN, which already compresses.
This commit is contained in:
Amruth Pillai
2026-09-29 22:04:03 +02:00
parent a9c4b84d38
commit e74403e12f
2 changed files with 33 additions and 0 deletions
+27
View File
@@ -1,3 +1,4 @@
import { gunzipSync } from "node:zlib";
import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
@@ -205,6 +206,32 @@ describe("createApp", () => {
expect(mocks.handleWebApp).toHaveBeenCalledWith(request);
expect(mocks.serveWebDistStatic).not.toHaveBeenCalled();
});
it("compresses the web app's HTML but never API streams or the Vercel app", async () => {
const { createApp } = await import("./app");
const html = `<!doctype html>${"<p>Reactive Resume</p>".repeat(200)}`;
const htmlResponse = () =>
new Response(html, {
headers: { "Content-Type": "text/html; charset=UTF-8", "Cache-Control": "private, no-store", Vary: "Cookie" },
});
const stream = () => new Response("data: x\n\n".repeat(500), { headers: { "Content-Type": "application/json" } });
mocks.handleWebApp.mockImplementation(async () => htmlResponse());
mocks.handleRpc.mockImplementation(async () => stream());
mocks.handleMcp.mockImplementation(async () => stream());
const headers = { "Accept-Encoding": "br, gzip" };
const page = await createApp().request("http://localhost:3000/", { headers });
const rpc = await createApp().request("http://localhost:3000/api/rpc/agent/chat", { headers });
const mcp = await createApp().request("http://localhost:3000/mcp", { headers });
const vercelPage = await createApp({ serveStatic: false }).request("http://localhost:3000/", { headers });
expect(page.headers.get("content-encoding")).toBe("gzip");
expect(page.headers.get("vary")).toBe("Cookie, Accept-Encoding");
expect(page.headers.get("cache-control")).toBe("private, no-store");
expect(gunzipSync(Buffer.from(await page.arrayBuffer())).toString()).toBe(html);
for (const response of [rpc, mcp, vercelPage]) expect(response.headers.get("content-encoding")).toBeNull();
expect(vercelPage.headers.get("vary")).toBe("Cookie");
});
});
it.each(["/auth/consent", "/auth/consent/", "/auth/login"])("prevents framing or caching %s", async (path) => {
+6
View File
@@ -3,6 +3,7 @@ import type { Context } from "hono";
import { isIP } from "node:net";
import { getConnInfo } from "@hono/node-server/conninfo";
import { Hono } from "hono";
import { compress } from "hono/compress";
import { prepareStagedBody, withStagedBody } from "@reactive-resume/api/features/storage/transport";
import { handleMcp } from "../mcp/handler";
import { handleOpenApi } from "../openapi/handler";
@@ -81,6 +82,11 @@ export function createApp(options: AppOptions = {}) {
app.on(["GET", "HEAD"], "/sitemap.xml", (c) => handleSitemap({ head: c.req.method === "HEAD" }));
app.on(["GET", "HEAD"], "/llms.txt", (c) => handleLlms({ head: c.req.method === "HEAD" }));
// Compresses only the web app's files and HTML shells: every route registered above answers before reaching
// it, so API, MCP, and upload streams are never buffered or re-encoded. Where a CDN serves the static files
// (Vercel), it also compresses at its edge.
if (options.serveStatic !== false) app.use("/*", compress());
// Must precede the static middleware: serveStatic resolves "/" to dist/index.html and would
// return it verbatim, skipping the OpenGraph/Twitter/canonical/JSON-LD injection in handleWebApp.
app.on(["GET", "HEAD"], "/", (c) => handleWebApp(c.req.raw));