Skip to content

Consider gzip compression for JSON API responses (/api/bootstrap, /api/resync) #140

Description

@LarsLaskowski

Severity: Trivial / nice-to-have (explicitly a judgement call — closing as not_planned is a legitimate outcome)
Category: Performance (network)
Affected files: src/server/index.ts, package.json. Line numbers refer to main @ 3ac64da.

Problem

/api/bootstrap returns up to serverMaxSyncLines (≤ 500) full LogLine objects — with rawLine and message both carrying the line text, plus metadata, that is roughly 150–250 KB of JSON per page load. /api/resync in reset mode is the same size, and it fires on every reconnect and on tab-visibility resumes. Nothing is compressed today. Log text compresses extremely well (repetitive timestamps, logger names, duplicated raw/message fields): gzip typically brings this to ~10–15 % of the original size.

On a wired LAN this is irrelevant — which is why this issue is a consider, not a defect. It becomes noticeable for the documented reverse-proxy/remote-access setup and for Wi-Fi dashboards on busy networks.

Decision to make first

The project convention is to keep dependencies minimal. There are three reasonable outcomes; pick one and either implement or close:

  1. Do nothing / close as not planned — valid if the maintainer considers LAN-only the primary target. If a reverse proxy is already in place for auth, it can also do compression (gzip on; in nginx / encode gzip in Caddy); a README note recommending that is a zero-dependency alternative.
  2. Add the compression middleware (the standard Express solution, small and stable).
  3. No new dependency: use Node's built-in node:zlib in a tiny hand-rolled middleware for the two JSON endpoints. More code to own; only worth it if avoiding the dependency matters more than the ~30 lines.

Option 2 is the recommended implementation if compression is wanted; option 1's README note is the recommended minimum.

Suggested implementation (option 2)

  1. npm install compression (+ @types/compression as a dev dependency).
  2. In src/server/index.ts, mount it only for the API router and never for the SSE stream — compressing text/event-stream buffers events and breaks live delivery:
import compression from 'compression';

app.use(
  '/api',
  compression({
    filter: (request, response) =>
      request.path !== '/stream' && compression.filter(request, response)
  }),
  apiLimiter,
  createApiRouter({})
);

Notes for the implementer:

  • Inside a router mounted at /api, request.path is '/stream' for /api/stream — verify with a test rather than assuming (if it turns out to be the full path in this position, match accordingly).
  • Keep express.static uncompressed or include it consciously — the JS bundle (~tens of KB) also benefits, but etag-based 304s already keep repeat loads cheap; compressing only /api is the smaller, safer change.
  • Default threshold (1 KB) is fine; small responses like /api/health stay uncompressed automatically.
  1. If instead option 1 is chosen: add a short README note in the reverse-proxy section recommending enabling gzip/brotli at the proxy for /api/bootstrap and /api/resync, and explicitly not for /api/stream.

Acceptance criteria (if implemented as option 2)

  • GET /api/bootstrap with Accept-Encoding: gzip → response has Content-Encoding: gzip and decodes to the same JSON as before.
  • GET /api/streamno Content-Encoding, first SSE event arrives immediately (add/extend a route test asserting the header is absent).
  • npm test, npm run typecheck, and npm run build pass; README.md documents the behavior briefly.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions