Skip to content

Commit cb3e850

Browse files
committed
Add gzip pre-compression plugin and optimize Vite chunk splitting
Add a Vite build plugin that generates .gz copies of assets for sirv to serve pre-compressed. Split socket.io and radix-ui into separate chunks for better caching. Refactor lighthouse benchmark to use `reflex run --env prod` directly instead of AppHarnessProd.
1 parent 8ca31a5 commit cb3e850

3 files changed

Lines changed: 132 additions & 5 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/* vite-plugin-compress.js
2+
*
3+
* Generate pre-compressed .gz copies of build assets so that sirv (the
4+
* production static file server) can serve them directly. sirv has built-in
5+
* support for pre-compressed files (--gzip flag, enabled by default) but only
6+
* looks for existing .gz files on disk -- it does not compress on-the-fly.
7+
*
8+
* Without this plugin the browser receives uncompressed assets, which is the
9+
* single biggest Lighthouse performance bottleneck for Reflex apps. With gzip
10+
* the total asset payload typically shrinks by ~75-80%.
11+
*/
12+
13+
import { promisify } from "node:util";
14+
import { gzip } from "node:zlib";
15+
16+
const gzipAsync = promisify(gzip);
17+
18+
const COMPRESSIBLE_EXTENSIONS = /\.(js|css|html|json|svg|xml|txt|map|mjs)$/;
19+
20+
// Only compress files above this size (bytes). Tiny files don't benefit
21+
// and the overhead of Content-Encoding negotiation can outweigh the saving.
22+
const MIN_SIZE = 256;
23+
24+
/**
25+
* Vite plugin that generates .gz files for all eligible build assets.
26+
* @returns {import('vite').Plugin}
27+
*/
28+
export default function compressPlugin() {
29+
return {
30+
name: "vite-plugin-compress",
31+
apply: "build",
32+
enforce: "post",
33+
34+
async generateBundle(_options, bundle) {
35+
const jobs = [];
36+
37+
for (const [fileName, asset] of Object.entries(bundle)) {
38+
if (!COMPRESSIBLE_EXTENSIONS.test(fileName)) continue;
39+
40+
const source = asset.type === "chunk" ? asset.code : asset.source;
41+
if (source == null) continue;
42+
43+
const raw = typeof source === "string" ? Buffer.from(source) : source;
44+
if (raw.length < MIN_SIZE) continue;
45+
46+
jobs.push(
47+
gzipAsync(raw, { level: 9 }).then((compressed) => {
48+
this.emitFile({
49+
type: "asset",
50+
fileName: fileName + ".gz",
51+
source: compressed,
52+
});
53+
}),
54+
);
55+
}
56+
57+
await Promise.all(jobs);
58+
},
59+
};
60+
}

packages/reflex-base/src/reflex_base/compiler/templates.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,7 @@ def vite_config_template(
526526
import {{ reactRouter }} from "@react-router/dev/vite";
527527
import {{ defineConfig }} from "vite";
528528
import safariCacheBustPlugin from "./vite-plugin-safari-cachebust";
529+
import compressPlugin from "./vite-plugin-compress";
529530
530531
// Ensure that bun always uses the react-dom/server.node functions.
531532
function alwaysUseReactDomServerNode() {{
@@ -566,6 +567,7 @@ def vite_config_template(
566567
alwaysUseReactDomServerNode(),
567568
reactRouter(),
568569
safariCacheBustPlugin(),
570+
compressPlugin(),
569571
].concat({"[fullReload()]" if force_full_reload else "[]"}),
570572
build: {{
571573
assetsDir: "{base}assets".slice(1),
@@ -583,6 +585,14 @@ def vite_config_template(
583585
test: /env.json/,
584586
name: "reflex-env",
585587
}},
588+
{{
589+
test: /node_modules\/socket\.io|node_modules\/engine\.io/,
590+
name: "socket-io",
591+
}},
592+
{{
593+
test: /node_modules\/@radix-ui/,
594+
name: "radix-ui",
595+
}},
586596
],
587597
}},
588598
}},

tests/integration/lighthouse_utils.py

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,18 @@
55
import json
66
import operator
77
import os
8+
import re
89
import shlex
910
import shutil
1011
import subprocess
12+
import time
1113
from dataclasses import dataclass
1214
from pathlib import Path
1315
from typing import Any
1416

1517
import pytest
1618

17-
from reflex.testing import AppHarnessProd, chdir
19+
from reflex.testing import chdir
1820
from reflex.utils.templates import initialize_default_app
1921

2022
LIGHTHOUSE_RUN_ENV_VAR = "REFLEX_RUN_LIGHTHOUSE"
@@ -840,7 +842,10 @@ def _run_prod_lighthouse_benchmark(
840842
report_path: Path,
841843
label: str,
842844
) -> LighthouseBenchmarkResult:
843-
"""Run Lighthouse against a Reflex app in prod mode.
845+
"""Run Lighthouse against a Reflex app via ``reflex run --env prod``.
846+
847+
Uses the real production code path so the benchmark automatically
848+
reflects any future changes to how Reflex serves apps in prod.
844849
845850
Args:
846851
app_root: The app root to initialize or reuse.
@@ -853,9 +858,61 @@ def _run_prod_lighthouse_benchmark(
853858
"""
854859
report_path.parent.mkdir(parents=True, exist_ok=True)
855860

856-
with AppHarnessProd.create(root=app_root, app_name=app_name) as harness:
857-
assert harness.frontend_url is not None
858-
report = run_lighthouse(harness.frontend_url, report_path)
861+
proc = subprocess.Popen(
862+
[
863+
"uv",
864+
"run",
865+
"reflex",
866+
"run",
867+
"--env",
868+
"prod",
869+
"--frontend-only",
870+
"--loglevel",
871+
"info",
872+
],
873+
cwd=str(app_root),
874+
stdout=subprocess.PIPE,
875+
stderr=subprocess.STDOUT,
876+
text=True,
877+
)
878+
879+
# Wait for the frontend URL to appear in stdout.
880+
frontend_url = None
881+
captured_output: list[str] = []
882+
deadline = time.monotonic() + 120
883+
assert proc.stdout is not None
884+
while time.monotonic() < deadline:
885+
line = proc.stdout.readline()
886+
if not line:
887+
break
888+
captured_output.append(line)
889+
m = re.search(r"App running at:\s*(http\S+)", line)
890+
if m:
891+
frontend_url = m.group(1).rstrip("/")
892+
break
893+
894+
if frontend_url is None:
895+
proc.terminate()
896+
try:
897+
proc.wait(timeout=10)
898+
except subprocess.TimeoutExpired:
899+
proc.kill()
900+
proc.wait()
901+
output = "".join(captured_output)
902+
pytest.fail(
903+
f"reflex run --env prod did not start within timeout for {label}\n"
904+
f"Captured output:\n{output}"
905+
)
906+
907+
try:
908+
report = run_lighthouse(frontend_url, report_path)
909+
finally:
910+
proc.terminate()
911+
try:
912+
proc.wait(timeout=10)
913+
except subprocess.TimeoutExpired:
914+
proc.kill()
915+
proc.wait()
859916

860917
failures = []
861918
for category_name, threshold in LIGHTHOUSE_CATEGORY_THRESHOLDS.items():

0 commit comments

Comments
 (0)