Skip to content

Investigate: Internally-managed hot reload with single-port dev mode #6221

@masenf

Description

@masenf

Summary

Investigate whether it's feasible and desirable for Reflex to manage hot reload internally rather than relying on vite's dev server. In this mode, Reflex would invoke bun/vite for a dev build, serve the files statically, watch for Python source changes, perform minimal recompiles, trigger incremental bun/vite rebuilds, and notify the browser to reload.

This is an investigation issue — the goal is to answer the feasibility questions and produce a recommendation, not to implement the full feature.

Background & Motivation

Today, the dev workflow involves two servers: the Reflex backend (Python/FastAPI) and the vite dev server (Node). The vite dev server handles JS bundling, HMR (hot module replacement), and serving the frontend. This works but has drawbacks:

  • Two ports: The frontend runs on a different port than the backend, requiring proxy configuration and complicating deployment/debugging
  • Vite controls the reload cycle: Even when Reflex finishes recompiling quickly, the user still waits for vite's HMR pipeline
  • Limited control over what gets rebuilt: Reflex writes to .web/ and vite picks up all changes via filesystem watching — there's no fine-grained signaling from Reflex to vite about what actually changed
  • Overhead of vite dev server: The vite dev server has its own memory footprint and startup time

What the new compiler work enables

With the single-pass compiler (ENG-9142-ENG-9149), caching (ENG-9148), and per-module JS output for memo components, Reflex will have:

  1. Knowledge of exactly which JS files changed after a Python edit
  2. Ability to detect changes that don't impact UI code (e.g., backend-only state logic changes that don't affect component rendering)
  3. Granular JS output — more small files that change independently, rather than a few large files that change frequently

This makes internally-managed hot reload potentially viable.

Proposed Architecture (to investigate)

┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│  Python FS  │────▶│  Reflex      │────▶│  bun/vite   │
│  Watcher    │     │  Compiler    │     │  (build)    │
└─────────────┘     │  (minimal    │     └──────┬──────┘
                    │   recompile) │            │
                    └──────────────┘            │
                           │                   │
                    ┌──────▼──────┐     ┌──────▼──────┐
                    │  Detect:    │     │  Static     │
                    │  UI change? │     │  file       │
                    │  or backend │     │  server     │
                    │  only?      │     │  (Reflex)   │
                    └──────┬──────┘     └──────┬──────┘
                           │                   │
                    ┌──────▼──────────────────▼──────┐
                    │  Browser: reload JS / refresh  │
                    └────────────────────────────────┘
  1. Python file watcher — Reflex watches tracked Python modules for changes
  2. Minimal recompile — Only recompile affected pages/modules (using caching from ENG-9148)
  3. Change classification — Determine if the change affects UI code (component tree) or is backend-only (state logic, event handlers)
  4. Incremental build — Invoke bun/vite for a production-style build, but only on changed files
  5. Static serving — Reflex serves the built files directly (single port)
  6. Reload notification — Push a reload signal to the browser via the existing websocket connection

Key questions to answer

  1. Is vite's incremental build fast enough? Vite's vite build is slower than its dev mode HMR. With granular JS files (from memo provenance tracking), is the incremental build time acceptable? What's the typical delta?
  2. Is this actually faster than letting vite do HMR? If the new compiler outputs many small files that rarely change, vite's HMR might already be fast enough that managing it ourselves adds complexity without benefit.
  3. Can we do module-level HMR without vite? Instead of a full page reload, can Reflex inject only the changed JS module? This would require implementing a basic HMR runtime — is it worth it?
  4. What about CSS/asset changes? Vite handles CSS HMR, PostCSS processing, asset optimization. If we bypass vite's dev server, we need to handle these ourselves or accept page reloads for CSS changes.
  5. Backend-only change detection: For changes that only affect state logic (event handler implementations), no browser reload is needed at all — the websocket reconnects to the updated backend. How reliably can we detect this?
  6. Single-port serving: What changes are needed to serve the frontend statically from the Reflex backend? FastAPI's StaticFiles mount should work, but are there path/routing considerations?
  7. Do we even need this? If the compiler outputs more granular files and vite's HMR becomes fast enough as a result, the complexity of managing our own hot reload may not be justified. The investigation should honestly assess whether the current vite dev server approach, combined with better compiler output, is already good enough.

Investigation Deliverables

  • Benchmark: vite build vs vite HMR — Measure incremental build time (vite build with changed files) vs HMR time for the same changes on a representative app
  • Prototype: single-port static serving — Simple proof of concept: Reflex serves vite's build output and triggers browser reload via websocket after recompile + rebuild
  • Benchmark: end-to-end hot reload latency — Compare Python edit → browser update latency for: (a) current vite dev server, (b) prototype single-port mode, (c) current mode but with granular compiler output from other issues
  • Analysis: backend-only change detection accuracy — How reliably can we determine that a Python change doesn't affect UI? What's the false-positive rate (unnecessary reloads)?
  • Write-up: recommendation — Based on findings, recommend one of: (a) implement single-port mode, (b) defer in favor of improving vite integration, (c) hybrid approach

Related Issues

  • ENG-9142-ENG-9149 — Single-pass compiler (enables caching and minimal recompile)
  • ENG-9148 — Selective page recompilation and caching
  • Memo provenance tracking (enables per-module JS output)

Notes

  • This investigation should be done after the core compiler improvements land, since its viability depends on those capabilities (especially caching and granular output).
  • esbuild (used by vite internally) supports an incremental build API that could be used directly, bypassing vite's higher-level abstractions. This might be worth exploring.
  • The React Router framework mode that Reflex uses may have opinions about how HMR works — investigate compatibility.
  • Even if full single-port mode isn't pursued, the "detect backend-only changes and skip browser reload" optimization is independently valuable and lower risk.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementAnything you want improved

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions