feat(export): self-extracting gzip standalone HTML via compress=True

FA
4d ago
+370-21

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 via textContent (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_bundle expects unminified markers in the vite-minified bundle — unrelated).
  • Hooks: pre-commit run --all-files, ruff check ., ruff format --check . all green.