authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-07-28 15:17:30-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-28 15:17:30-07:00
log8829e2f9b960eb31dc62ddab256d5da82c5077a6
tree188bb6979c17fd526729b48c96b772b941c1e134
parent2deea7abb0f9e4df71ca10da06a8d7268a4b9d42
signature Signed by PGP key B5690EEEBB952194

perf(server): gzip large thread snapshots (#4788)


4 files changed, 156 insertions(+), 2 deletions(-)

apps/server/src/http.test.ts+53-2
...@@ -1,6 +1,11 @@...@@ -1,6 +1,11 @@
1import { describe, expect, it } from "vite-plus/test";1import { expect, it } from "@effect/vitest";
2import * as NodeZlib from "node:zlib";
3import * as Effect from "effect/Effect";
4import * as Stream from "effect/Stream";
5import { HttpServerResponse } from "effect/unstable/http";
6import { describe } from "vite-plus/test";
27
3import { isLoopbackHostname, resolveDevRedirectUrl } from "./http.ts";8import { compressHttpResponse, isLoopbackHostname, resolveDevRedirectUrl } from "./http.ts";
49
5describe("http dev routing", () => {10describe("http dev routing", () => {
6 it("treats localhost and loopback addresses as local", () => {11 it("treats localhost and loopback addresses as local", () => {
...@@ -25,3 +30,49 @@ describe("http dev routing", () => {...@@ -25,3 +30,49 @@ describe("http dev routing", () => {
25 );30 );
26 });31 });
27});32});
33
34describe("http compression", () => {
35 it.effect("gzips large JSON responses when the client accepts it", () =>
36 Effect.gen(function* () {
37 const body = `{"value":"${"compressible".repeat(1_000)}"}`;
38 const response = compressHttpResponse(
39 HttpServerResponse.text(body, { contentType: "application/json" }),
40 "br, gzip, deflate",
41 );
42
43 expect(response.headers["content-encoding"]).toBe("gzip");
44 expect(response.headers["content-length"]).toBeUndefined();
45 expect(response.headers.vary).toBe("Accept-Encoding");
46 expect(response.body._tag).toBe("Stream");
47 if (response.body._tag === "Stream") {
48 const chunks = yield* Stream.runCollect(Stream.orDie(response.body.stream));
49 expect(NodeZlib.gunzipSync(Buffer.concat(Array.from(chunks))).toString()).toBe(body);
50 }
51 }),
52 );
53
54 it("keeps the original body when gzip is declined", () => {
55 const response = compressHttpResponse(
56 HttpServerResponse.text("x".repeat(2_000), { contentType: "application/json" }),
57 "gzip;q=0, *;q=1",
58 );
59
60 expect(response.headers["content-encoding"]).toBeUndefined();
61 expect(response.headers["content-length"]).toBe("2000");
62 expect(response.headers.vary).toBe("Accept-Encoding");
63 });
64
65 it("preserves existing Vary semantics", () => {
66 const makeResponse = (vary: string) =>
67 compressHttpResponse(
68 HttpServerResponse.text("x".repeat(2_000), {
69 contentType: "application/json",
70 headers: { vary },
71 }),
72 undefined,
73 );
74
75 expect(makeResponse("*").headers.vary).toBe("*");
76 expect(makeResponse("Origin, accept-encoding").headers.vary).toBe("Origin, accept-encoding");
77 });
78});
apps/server/src/http.ts+72
...@@ -12,8 +12,10 @@ import * as FileSystem from "effect/FileSystem";...@@ -12,8 +12,10 @@ import * as FileSystem from "effect/FileSystem";
12import * as Layer from "effect/Layer";12import * as Layer from "effect/Layer";
13import * as Option from "effect/Option";13import * as Option from "effect/Option";
14import * as Path from "effect/Path";14import * as Path from "effect/Path";
15import * as Stream from "effect/Stream";
15import { cast } from "effect/Function";16import { cast } from "effect/Function";
16import {17import {
18 Headers,
17 HttpBody,19 HttpBody,
18 HttpClient,20 HttpClient,
19 HttpClientResponse,21 HttpClientResponse,
...@@ -42,6 +44,76 @@ import { browserApiCorsAllowedHeaders, browserApiCorsAllowedMethods } from "./ht...@@ -42,6 +44,76 @@ import { browserApiCorsAllowedHeaders, browserApiCorsAllowedMethods } from "./ht
42const OTLP_TRACES_PROXY_PATH = "/api/observability/v1/traces";44const OTLP_TRACES_PROXY_PATH = "/api/observability/v1/traces";
43const LOOPBACK_HOSTNAMES = new Set(["127.0.0.1", "::1", "localhost"]);45const LOOPBACK_HOSTNAMES = new Set(["127.0.0.1", "::1", "localhost"]);
44const DESKTOP_RENDERER_ORIGINS = ["t3code://app", "t3code-dev://app"];46const DESKTOP_RENDERER_ORIGINS = ["t3code://app", "t3code-dev://app"];
47const GZIP_MIN_BYTES = 1024;
48
49function acceptsGzip(value: string | undefined): boolean {
50 if (!value) return false;
51
52 const accepted = new Map(
53 value.split(",").map((entry) => {
54 const [coding = "", ...parameters] = entry.trim().toLowerCase().split(";");
55 const quality = parameters
56 .map((parameter) => parameter.trim().match(/^q=(.+)$/)?.[1])
57 .find((parameter) => parameter !== undefined);
58 return [coding, quality === undefined ? 1 : Number(quality)] as const;
59 }),
60 );
61 return (accepted.get("gzip") ?? accepted.get("*") ?? 0) > 0;
62}
63
64function varyByAcceptEncoding(value: string | undefined): string {
65 if (!value) return "Accept-Encoding";
66 const values = new Set(value.split(",").map((entry) => entry.trim().toLowerCase()));
67 return values.has("*") || values.has("accept-encoding") ? value : `${value}, Accept-Encoding`;
68}
69
70export function compressHttpResponse(
71 response: HttpServerResponse.HttpServerResponse,
72 acceptEncoding: string | undefined,
73): HttpServerResponse.HttpServerResponse {
74 const body = response.body;
75 if (
76 body._tag !== "Uint8Array" ||
77 body.contentLength < GZIP_MIN_BYTES ||
78 !body.contentType.startsWith("application/json") ||
79 response.headers["content-encoding"]
80 ) {
81 return response;
82 }
83
84 const variedResponse = HttpServerResponse.setHeader(
85 response,
86 "vary",
87 varyByAcceptEncoding(response.headers.vary),
88 );
89 if (!acceptsGzip(acceptEncoding)) return variedResponse;
90
91 const compressedBody = Stream.fromReadableStream({
92 evaluate: () => new Response(body.body).body!.pipeThrough(new CompressionStream("gzip")),
93 onError: (cause) => cause,
94 });
95 const headers = Headers.set(
96 Headers.remove(variedResponse.headers, "content-length"),
97 "content-encoding",
98 "gzip",
99 );
100 return HttpServerResponse.stream(compressedBody, {
101 status: response.status,
102 statusText: response.statusText,
103 headers,
104 cookies: response.cookies,
105 contentType: body.contentType,
106 });
107}
108
109export const httpCompressionLayer = HttpRouter.middleware(
110 (httpEffect) =>
111 Effect.gen(function* () {
112 const request = yield* HttpServerRequest.HttpServerRequest;
113 return compressHttpResponse(yield* httpEffect, request.headers["accept-encoding"]);
114 }),
115 { global: true },
116);
45117
46export const browserApiCorsLayer = Layer.unwrap(118export const browserApiCorsLayer = Layer.unwrap(
47 Effect.gen(function* () {119 Effect.gen(function* () {
apps/server/src/server.test.ts+29
...@@ -1287,6 +1287,35 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -1287,6 +1287,35 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
1287 }).pipe(Effect.provide(NodeHttpServer.layerTest)),1287 }).pipe(Effect.provide(NodeHttpServer.layerTest)),
1288 );1288 );
12891289
1290 it.effect("compresses large JSON responses through the composed routes", () =>
1291 Effect.gen(function* () {
1292 const descriptor = {
1293 ...testEnvironmentDescriptor,
1294 label: "Test environment".repeat(100),
1295 };
1296 yield* buildAppUnderTest({
1297 layers: {
1298 serverEnvironment: {
1299 getDescriptor: Effect.succeed(descriptor),
1300 },
1301 },
1302 });
1303
1304 const url = yield* getHttpServerUrl("/.well-known/t3/environment");
1305 const response = yield* fetchEffect(url, {
1306 headers: {
1307 "accept-encoding": "gzip",
1308 },
1309 });
1310 const body = yield* responseJsonEffect<typeof descriptor>(response);
1311
1312 assert.equal(response.status, 200);
1313 assert.equal(response.headers["content-encoding"], "gzip");
1314 assert.equal(response.headers.vary, "Accept-Encoding");
1315 assert.deepEqual(body, descriptor);
1316 }).pipe(Effect.provide(NodeHttpServer.layerTest)),
1317 );
1318
1290 it.effect("includes CORS headers on public environment descriptor responses", () =>1319 it.effect("includes CORS headers on public environment descriptor responses", () =>
1291 Effect.gen(function* () {1320 Effect.gen(function* () {
1292 yield* buildAppUnderTest();1321 yield* buildAppUnderTest();
apps/server/src/server.ts+2
...@@ -11,6 +11,7 @@ import {...@@ -11,6 +11,7 @@ import {
11 serverEnvironmentHttpApiLayer,11 serverEnvironmentHttpApiLayer,
12 staticAndDevRouteLayer,12 staticAndDevRouteLayer,
13 browserApiCorsLayer,13 browserApiCorsLayer,
14 httpCompressionLayer,
14} from "./http.ts";15} from "./http.ts";
15import { fixPath } from "./os-jank.ts";16import { fixPath } from "./os-jank.ts";
16import { websocketRpcRouteLayer } from "./ws.ts";17import { websocketRpcRouteLayer } from "./ws.ts";
...@@ -372,6 +373,7 @@ export const makeRoutesLayer = Layer.mergeAll(...@@ -372,6 +373,7 @@ export const makeRoutesLayer = Layer.mergeAll(
372 Layer.provide(PreviewAutomationBroker.layer),373 Layer.provide(PreviewAutomationBroker.layer),
373 Layer.provide(ServerSelfUpdate.layer),374 Layer.provide(ServerSelfUpdate.layer),
374 Layer.provide(browserApiCorsLayer),375 Layer.provide(browserApiCorsLayer),
376 Layer.provide(httpCompressionLayer),
375);377);
376378
377export const makeServerLayer = Layer.unwrap(379export const makeServerLayer = Layer.unwrap(