feat(export): self-extracting gzip standalone HTML via compress=True
FA
4d ago+370+370+012345678901234567890123456789-21-21-01234567890123456789
What
Opt-in compress=True on the standalone HTML export surfaces (export.to_html, Figure.to_html/html, Chart.to_html/html, FacetGrid.to_html, FacetChart.to_html): the embedded render client and data blob ride as base64-of-gzip chunks and are inflated in-browser via native DecompressionStream("gzip") before first render.
Why
At small N the inlined client dominates the file — measured 73.6% of a 10k-point export (304 KB client / 413 KB file). Entry-trimmed bundle experiments showed client surgery can't fix this (severable widget-comm code is ~16 KB; the core renderer every path needs is ~187 KB), so the lever is compression at the container level.
Measured
- 10k-point scatter export: 413 KB → 215 KB (~49% smaller)
- Headless-Chromium screenshot of the compressed export is byte-identical to the plain one (single figure and facet grid)
compress=False(default) output is byte-for-byte the pre-existing format- gzip pins
mtime=0, so identical exports stay byte-identical (determinism contract, spec §4)
Notes
- Browsers without
DecompressionStream(pre-mid-2023) get a plain-text notice viatextContent(never markup) instead of a chart. - CSP is unchanged: the inflated client is injected as an inline script element under the existing
_STANDALONE_CSP. - A shared-external-client mode (plotly's
include_plotlyjs='directory'shape) was evaluated, validated, and deliberately dropped — the reflex adapter already provides cached-shared-client economics, and it would widen the CSP and add sibling-file lifecycle. - Spec updated:
spec/api/export.md§9 (new) and the §29 static-export transport row.
Tests
tests/test_html_export_modes.py: loader/feature-check pinning, exact-bytes Python-side gzip roundtrip of client + data chunks, determinism, default-output invariance, facet parity.- Full local suite: 616 passed; 1 pre-existing failure on clean main (
tests/reflex_adapter/test_assets.py::test_client_source_is_the_installed_bundleexpects unminified markers in the vite-minified bundle — unrelated). - Hooks:
pre-commit run --all-files,ruff check .,ruff format --check .all green.