Skip to content

Commit 54a918a

Browse files
authored
feat(template): disable adapter-node precompress to shrink tarball (#125)
## Summary - Flip `adapter()` → `adapter({ precompress: false })` in `template/svelte.config.js`. - Drops 84 precompressed siblings (`*.br` + `*.gz`) from both shipped images. - Each `dist*/` artifact: **2.0M → 1.5M** (–25%, ~520K saving per image, ~1.04 MB total). - Closes #120. ## Why Per issue #120, the precompressed siblings cost ~520K per image but buy negligible runtime CPU savings for the canonical local-first deployment (`npx @forgeplan/web start` on `127.0.0.1`, single user, no CDN). After PR #117 minified the server bundle, precompressed client assets were the largest fixed cost left in the experimental tarball. Decision (PRD-031 + EVID-036): chose **Option A** from #120 (disable at adapter level) over B (build.mjs strip — strictly more code for same runtime behaviour) and C (keep `.br`, drop `.gz` — half the win for ~zero additional safety). ## Trade-off (R-1, documented in PRD-031) Sirv serves assets uncompressed when no precompressed sibling exists. Verified via direct `Accept-Encoding: br, gzip` probe against a live `npx ... start` server in a scratch dir: ``` HTTP/1.1 200 OK Content-Length: 3047 ``` (Same response with and without `Accept-Encoding`. No `Content-Encoding` header in either case.) For loopback `127.0.0.1` deployments the cost is negligible. Mitigation paths if a real complaint surfaces: revert this flag, or wire on-the-fly compression in `template/src/hooks.server.ts`. ## Test plan - [x] `npm run build` — both images emit (`dist/ ready (1.40M)`, `dist-nightly/ ready (1.40M)`). - [x] `find dist dist-nightly -name '*.br' -o -name '*.gz' | wc -l` → 0 (was 84). - [x] `du -sh dist dist-nightly` → 1.5M each (was 2.0M each, well under PRD-014 SC-1 3M cap). - [x] `npm run smoke` → `ALL IMAGES PASS`. - [x] Manual scratch-dir probe: `init --image nightly` → `start` → `GET /` returns HTTP 200; `GET /_app/immutable/entry/app.*.js` returns HTTP 200 with correct Content-Length. - [ ] CI matrix (ubuntu/macos/windows) — pending. Refs: PRD-031, EVID-036, PRD-014, PRD-030, #117.
2 parents f37817a + 8358761 commit 54a918a

5 files changed

Lines changed: 345 additions & 1 deletion
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
---
2+
depth: standard
3+
id: EVID-036
4+
kind: evidence
5+
last_modified_at: 2026-05-09T11:38:56.840360+00:00
6+
last_modified_by: claude-code/2.1.138
7+
links:
8+
- target: PRD-031
9+
relation: informs
10+
status: active
11+
title: Drop precompressed assets — build/smoke/probe measurements
12+
---
13+
14+
# EVID-036: Drop precompressed assets — build/smoke/probe measurements
15+
16+
| Field | Value |
17+
|-------|-------|
18+
| Status | Draft |
19+
| Created | 2026-05-09 |
20+
| Valid Until | 2026-08-09 |
21+
| Target | PRD-031 |
22+
23+
## Structured Fields
24+
25+
evidence_type: measurement
26+
verdict: supports
27+
congruence_level: 3
28+
29+
## Measurement
30+
31+
Built `@forgeplan/web` after flipping `template/svelte.config.js` from
32+
`adapter()` to `adapter({ precompress: false })`. Measured against
33+
PRD-031 SC-1 / SC-2 / SC-3 / SC-4 / SC-5 directly on the post-build
34+
`dist/` and `dist-nightly/` directories and on a live server spawned
35+
from a scratch `init`. All commands executed in
36+
`/Users/nikitafedorov/Desktop/Projects/forgeplan-web` on
37+
`darwin 25.4.0` against the same `develop` branch the change was made on.
38+
39+
Commands:
40+
41+
```bash
42+
# SC-1: precompressed sibling count
43+
find dist dist-nightly -name '*.br' -o -name '*.gz' | wc -l
44+
45+
# SC-2: per-image size
46+
du -sh dist dist-nightly
47+
48+
# SC-3 + SC-4: live server probe
49+
SCRATCH=$(mktemp -d) && cd "$SCRATCH" && mkdir -p .forgeplan
50+
node /…/bin/forgeplan-web.mjs init -y --image nightly
51+
PORT=15998 node .forgeplan-web/index.js &
52+
curl -sI http://127.0.0.1:15998/_app/immutable/entry/app.DvKYkSiM.js
53+
curl -sI -H 'Accept-Encoding: br, gzip' \
54+
http://127.0.0.1:15998/_app/immutable/entry/app.DvKYkSiM.js
55+
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:15998/
56+
57+
# SC-5: smoke
58+
npm run smoke
59+
```
60+
61+
## Result
62+
63+
| SC | Metric | Before | After | Pass? |
64+
|-----|--------|--------|-------|-------|
65+
| SC-1 | precompressed siblings (`*.br` + `*.gz` under both `dist*/client/`) | 84 | **0** ||
66+
| SC-2 | `du -sh dist` | 2.0M | **1.5M** (-25%) | ✅ (< 3M cap) |
67+
| SC-2 | `du -sh dist-nightly` | 2.0M | **1.5M** (-25%) | ✅ (< 3M cap) |
68+
| SC-3 | `GET /` HTTP status (live server, scratch dir) | 200 | **200** ||
69+
| SC-4 | `/api/health` returns `ok` (smoke run) | ok | **ok** (project=shim) ||
70+
| SC-5 | `npm run smoke` exit code | 0 | **0** (`ALL IMAGES PASS`) ||
71+
72+
`scripts/build.mjs` post-build report after the change:
73+
74+
```
75+
[build] dist/ ready (1.40M, image=stable, features=0)
76+
[build] dist-nightly/ ready (1.40M, image=nightly, features=0)
77+
[build] done. images built: stable, nightly
78+
```
79+
80+
Asset-compression probe (3047-byte `app.DvKYkSiM.js`):
81+
82+
```
83+
--- without Accept-Encoding ---
84+
HTTP/1.1 200 OK
85+
Content-Length: 3047
86+
--- with Accept-Encoding: br, gzip ---
87+
HTTP/1.1 200 OK
88+
Content-Length: 3047
89+
```
90+
91+
→ Sirv (adapter-node's static layer) serves uncompressed bytes when
92+
precompressed siblings are absent. There is **no** `Content-Encoding`
93+
header in either response. PRD-031 R-1 ("uncompressed first-load wire
94+
traffic") is therefore real, not just theoretical — but it lands on a
95+
loopback `127.0.0.1` connection in the canonical deployment, where the
96+
cost is negligible.
97+
98+
## Interpretation
99+
100+
Option A from issue #120 (disable `precompress` at adapter level) is
101+
implemented and verified end-to-end. All five PRD-031 success criteria
102+
pass on the first build. Tarball saving is ~520K per image (~1.04 MB
103+
total across both images) — exactly the figure forecast in the issue
104+
body. Live server still serves `GET /` HTTP 200 from a freshly
105+
initialised scratch dir; smoke matrix passes with `ALL IMAGES PASS`.
106+
107+
The compression-probe result confirms the documented trade-off (R-1):
108+
sirv does NOT compress on-the-fly; clients receive uncompressed bytes.
109+
For local-first deployments (`127.0.0.1`, single user) this is
110+
negligible. If a future user reports slow first-load over a slow
111+
network, the mitigation path is documented in PRD-031 (flip
112+
`precompress` back to `true`, or wire an on-the-fly compression
113+
middleware in `template/src/hooks.server.ts`).
114+
115+
This evidence supports activating PRD-031 with `R_eff > 0`.
116+
117+
## Congruence Level Justification
118+
119+
CL3 (same context, penalty 0.0): the measurement was executed against
120+
the exact build artifacts (`dist/`, `dist-nightly/`) and the exact bin
121+
script (`bin/forgeplan-web.mjs init … && node .forgeplan-web/index.js`)
122+
that PRD-031 governs, on the same `develop` branch the change was
123+
landed on. No proxy environment, no benchmark surrogate, no cached
124+
result — direct measurement on the surface the PRD describes.
125+
126+
## Related Artifacts
127+
128+
| Artifact | Relation |
129+
|----------|----------|
130+
| PRD-031 | informs |
131+
132+

.forgeplan/prds/PRD-014-reduce-dist-size-by-bundling-server-with-esbuild.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,4 @@ Then используется именно /custom/path/forgeplan, а не де
228228

229229
> **Next step**: создать RFC-013 с архитектурой бандла + эджкейсами.
230230
231+

.forgeplan/prds/PRD-030-feature-flag-and-image-system-promote-experimental-bundle-to-stable-default.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,3 +300,4 @@ And neither directory contains a `node_modules/` subdirectory
300300

301301

302302

303+
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
---
2+
depth: standard
3+
id: PRD-031
4+
kind: prd
5+
last_modified_at: 2026-05-09T11:34:37.535120+00:00
6+
last_modified_by: claude-code/2.1.138
7+
links:
8+
- target: PRD-014
9+
relation: based_on
10+
- target: PRD-030
11+
relation: based_on
12+
status: active
13+
title: Drop precompressed client assets from tarball
14+
---
15+
16+
---
17+
id: PRD-031
18+
title: "Drop precompressed client assets from tarball"
19+
status: Draft
20+
author: fedorovvvv
21+
created: 2026-05-09
22+
updated: 2026-05-09
23+
priority: P2
24+
depth: standard
25+
domain: general
26+
projectType: cli_tool
27+
stepsCompleted: []
28+
---
29+
30+
# PRD-031: Drop precompressed client assets from tarball
31+
32+
## Progress
33+
34+
```
35+
Phase 0 ████████████████████████ 1/1 (100%)
36+
─────────────────────────────────────────────────
37+
TOTAL 1/1 (100%)
38+
```
39+
40+
---
41+
42+
## Executive Summary
43+
44+
### Vision
45+
46+
Ship a smaller `@forgeplan/web` tarball by removing 84 precompressed
47+
`*.br` / `*.gz` client siblings emitted by `@sveltejs/adapter-node`
48+
saving ~520K per `dist*/` artifact (~29% of the experimental tarball)
49+
without measurably degrading the local-first user experience.
50+
51+
### Problem
52+
53+
`@sveltejs/adapter-node` defaults to `precompress: true`, so every
54+
`dist*/client/` carries 21 `.br` + 21 `.gz` files alongside the
55+
originals (~520K). The tarball ships **two** images (`dist/` for
56+
`stable`, `dist-nightly/` for `nightly`), so users pay the cost twice
57+
on every `npm install` / `npx` invocation.
58+
59+
The runtime CPU savings precompressed assets buy (sirv serving the
60+
cached `.br`/`.gz` directly) are negligible for the canonical
61+
deployment shape: `npx @forgeplan/web start` on `127.0.0.1` for one
62+
local user, behind no CDN. Every user pays the install cost; almost
63+
nobody benefits from the runtime saving.
64+
65+
**Impact**: ~520K × 2 dists = ~1.04 MB of dead weight on every
66+
install. After PR #117 minified the server bundle (1.4 MB → 683 KB),
67+
precompressed client assets are now the largest fixed cost in
68+
`dist-nightly/`.
69+
70+
### Target Users
71+
72+
| Persona | Описание | Ключевая боль |
73+
|---------|----------|---------------|
74+
| Local-first user | Runs `npx @forgeplan/web start` on `127.0.0.1` | Pays install cost (tarball download + extraction) for runtime savings they will never measure |
75+
| CI runner | Installs `@forgeplan/web` in throwaway environments | Slower cold installs because of inflated tarball |
76+
77+
### Differentiators
78+
79+
- Single-flag adapter change (Option A from issue #120) — minimal
80+
blast radius, fully reversible by flipping `precompress` back to
81+
`true`.
82+
- Aligned with `npx @forgeplan/web` philosophy: the package is
83+
optimized for fast first-run on a developer machine, not for
84+
high-throughput public-facing serving.
85+
86+
---
87+
88+
## Success Criteria
89+
90+
| ID | Criterion | Metric | Current | Target | Timeframe | How to Measure |
91+
|----|-----------|--------|---------|--------|-----------|----------------|
92+
| SC-1 | Precompressed siblings absent from both shipped images | count of `*.br` + `*.gz` under `dist/client/``dist-nightly/client/` after `npm run build` | 84 | 0 | This PR | `find dist dist-nightly -name '*.br' -o -name '*.gz' \| wc -l` |
93+
| SC-2 | Each tarball image stays under PRD-014 SC-1 cap | `du -sh dist*/` | 2.0M each | ≤ 1.5M each (target) / ≤ 3M (cap) | This PR | `du -sh dist dist-nightly` |
94+
| SC-3 | `start` still serves usable HTML | HTTP status of `GET /` after `npx @forgeplan/web start` | 200 | 200 | This PR | scratch-dir manual probe (see EVID) |
95+
| SC-4 | `/api/health` still returns `ok` | endpoint response | `{ ok: true }` | `{ ok: true }` | This PR | manual probe |
96+
| SC-5 | No regression in `npm run smoke` | smoke exit code | 0 | 0 | This PR | `npm run smoke` |
97+
98+
---
99+
100+
## Product Scope
101+
102+
### MVP (In-Scope)
103+
104+
- Pass `precompress: false` to `adapter()` in `template/svelte.config.js`.
105+
- Re-build both images (`dist/` + `dist-nightly/`) and verify the
106+
precompressed siblings are gone.
107+
- Smoke test the bin script against a scratch dir end-to-end.
108+
- Document the trade-off (no precompressed cache; sirv now serves
109+
uncompressed or compresses on-the-fly per `Accept-Encoding`).
110+
111+
### Out of Scope
112+
113+
- Lowering `DIST_*_MAX_BYTES` cap in `scripts/build.mjs` — separate
114+
decision (PRD candidate).
115+
- RFC-013 graduation / legacy `dist/` removal — owned by PRD-014.
116+
- Adding a custom sirv compression middleware to recover the runtime
117+
saving — only if a measured regression appears.
118+
119+
### Growth Vision
120+
121+
- If real users complain about uncompressed first-load wire size
122+
over slow networks, revisit by either (a) flipping `precompress`
123+
back on, or (b) wiring an on-the-fly compression middleware in
124+
`template/src/hooks.server.ts`.
125+
126+
---
127+
128+
## User Journeys
129+
130+
### Journey 1: Local-first user installs and starts
131+
132+
**Цель пользователя**: Spin up the Forgeplan web viewer in a working repo.
133+
134+
| Шаг | Действие пользователя | Ответ системы | Заметки |
135+
|-----|----------------------|---------------|---------|
136+
| 1 | `npx @forgeplan/web init -y` | Copies `dist/` (~1.5 MB target) into `.forgeplan-web/` | Smaller download than today |
137+
| 2 | `npx @forgeplan/web start` | Spawns node, listens on `127.0.0.1:5174` | No precompressed cache hit on assets |
138+
| 3 | Opens `http://127.0.0.1:5174` | Browser receives HTML; CSS/JS load | sirv serves assets uncompressed or via on-the-fly gzip per `Accept-Encoding` (TBD by smoke) |
139+
140+
**Результат**: Same UX, smaller install footprint.
141+
142+
### Journey 2: CI runner does throwaway install
143+
144+
**Цель пользователя**: Validate Forgeplan integration in CI.
145+
146+
| Шаг | Действие пользователя | Ответ системы | Заметки |
147+
|-----|----------------------|---------------|---------|
148+
| 1 | `npm i -g @forgeplan/web@latest` | Pulls smaller tarball, faster cold install | Saves ~1 MB per cold install across both images |
149+
| 2 | `forgeplan-web --version` | Prints version | Unaffected by this change |
150+
151+
**Результат**: Faster CI cold start, no functional change.
152+
153+
---
154+
155+
## Functional Requirements
156+
157+
| ID | Category | Priority | Requirement | Journey |
158+
|----|----------|----------|-------------|---------|
159+
| FR-001 | Build | Must | Build pipeline can emit `dist*/client/` directories with no `*.br` and no `*.gz` siblings | Journey 1, Journey 2 |
160+
| FR-002 | Runtime | Must | `start` command can serve an HTTP 200 on `GET /` and `/api/health` from a freshly initialised `.forgeplan-web/` that contains no precompressed siblings | Journey 1 |
161+
| FR-003 | Runtime | Should | Browser can render a usable page (CSS + JS load successfully) when the static-asset middleware has no precompressed siblings to fall back to | Journey 1 |
162+
163+
---
164+
165+
## Non-Functional Requirements
166+
167+
| ID | Category | Requirement | Metric | Condition | Measurement |
168+
|----|----------|-------------|--------|-----------|-------------|
169+
| NFR-001 | Size | System shall emit each `dist*/` artifact below the existing cap | ≤ 3 MB | After `npm run build` for both `stable` and `nightly` images | `du -sh dist dist-nightly` + `scripts/build.mjs` cap assertion |
170+
| NFR-002 | Reversibility | System shall allow restoring precompressed assets by flipping a single boolean | 1 line in `template/svelte.config.js` | Forever | Diff inspection |
171+
| NFR-003 | Compatibility | System shall not regress `npm run smoke` exit code | exit code = 0 | Local + CI matrix (ubuntu/macos/windows) | `npm run smoke` |
172+
173+
---
174+
175+
## Risks & Mitigations
176+
177+
| ID | Risk | Probability | Impact | Mitigation | Owner |
178+
|----|------|-------------|--------|------------|-------|
179+
| R-1 | Without precompressed siblings, sirv serves assets uncompressed → +700 KB first-load wire traffic for users on slow links | Medium | Low (local-first deployments dominate) | Document in README + this PRD; if real complaints surface, add an on-the-fly gzip middleware in `template/src/hooks.server.ts` or revert to `precompress: true` | fedorovvvv |
180+
| R-2 | A future custom hook in `template/` assumes `.br`/`.gz` siblings exist | Low | Medium | Grep template for `\.br` / `\.gz` references in this PR; none found at time of writing | fedorovvvv |
181+
| R-3 | Adapter-node behavior changes in a future major version, making `precompress: false` ineffective | Low | Low | Pinned via `template/package.json#dependencies`; bump-with-test is the existing process | fedorovvvv |
182+
183+
---
184+
185+
## Affected Files
186+
187+
- `template/svelte.config.js` — flip `adapter()``adapter({ precompress: false })`.
188+
- `dist/`, `dist-nightly/` — regenerated by `npm run build` (no source-controlled change beyond the regenerated bundles).
189+
190+
## Related Artifacts
191+
192+
| Artifact | Relation | Status |
193+
|----------|----------|--------|
194+
| PRD-014 | parent (tarball-size cap SC-1) | active |
195+
| PRD-030 | parent (dual-image pipeline `stable` + `nightly`) | active |
196+
| PR #117 | predecessor (server bundle minify, 1.4 MB → 683 KB) | merged |
197+
| Issue #120 | source | open (this PRD closes it) |
198+
199+
---
200+
201+
> **Decision recorded (closes issue #120 acceptance criterion 1)**:
202+
> Option A — disable precompress at the adapter level. Rejected:
203+
> Option B (build.mjs strip — strictly more code for the same runtime
204+
> behaviour) and Option C (keep `.br` only — half the size win for
205+
> ~zero additional safety, since a sirv fallback regression would still
206+
> need investigation).
207+
208+
209+
210+

template/svelte.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
55
const config = {
66
preprocess: vitePreprocess(),
77
kit: {
8-
adapter: adapter(),
8+
adapter: adapter({ precompress: false }),
99
files: {
1010
assets: 'static',
1111
lib: 'src/shared'

0 commit comments

Comments
 (0)