How Hot Reload Actually Works
Sub-second reloads are a consequence of Dart's JIT, an incremental compiler, and the fact that widgets are disposable - plus the rules that decide when it fails.
Hot reload is the feature that sells Flutter in the first ten minutes. Change a colour, save, and the running app updates in under a second with your navigation stack, scroll position, and form input intact.
It looks like magic, and it is worth understanding for a practical reason: hot reload has rules. Knowing them turns "why didn't that apply?" from a mystery into a diagnosis, and stops the reflex of restarting the app every time something looks stale - which throws away the state you were trying to keep.
Three pieces make it possible
Hot reload is not one clever trick. It is three ordinary capabilities that combine well.
The Dart VM can swap code at runtime
In debug builds, Dart runs JIT-compiled inside the VM, and the VM supports loading new code into a live isolate. When you save, the tool sends the VM a kernel file containing the changed libraries, and the VM patches its class and function tables in place.
Existing objects keep their identity. A State instance that existed before the
reload is the same instance afterwards - its class's methods were replaced
underneath it.
This is also why hot reload is a debug-only feature. A release build is AOT compiled to native machine code with no VM to patch, which is the deliberate trade: development flexibility, release performance.
The compiler only rebuilds what changed
Flutter runs an incremental frontend compiler that keeps the whole program's state in memory. On save, it recompiles only the changed libraries and their dependents, producing a small delta rather than a fresh build.
That delta is typically a few kilobytes, hence the wire time is negligible. Most of the sub-second budget goes to compilation, and it stays small because the compiler is warm and already knows the rest of your program.
Widgets are disposable by design
After the code is swapped, the framework marks the entire widget tree dirty and rebuilds it from the root.
This is where Flutter's architecture pays off. Rebuilding everything is cheap
because widgets are immutable descriptions, and it is safe because the element
tree and its State objects persist across the rebuild - the same reconciliation
that makes setState cheap makes hot reload possible.
A framework where UI objects are long-lived and mutable could not do this. It would have to reconcile new class definitions against existing instances field by field. Flutter sidesteps that by throwing the descriptions away and keeping only the instances.
Why it sometimes doesn't work
Every hot reload limitation follows from one of those three pieces.
initState and initialisers do not re-run
State.initState ran when the element was created, and the element survives the
reload. Change a value you set there and nothing happens, because that code is
not executed again.
The same applies to field initialisers on a State, values computed once in a
constructor, and anything cached in a singleton at startup. If your change lives
in code that runs once, the reload swapped the code and never called it.
class _CounterState extends State<Counter> {
// Editing this and hot reloading does nothing - initState already ran.
int _count = 10;
@override
Widget build(BuildContext context) => Text('$_count'); // edits here apply
}The workaround is a hot restart (R rather than r), which tears down the
isolate and starts fresh, losing all state.
Structural changes force a restart
Some edits change things the VM cannot patch safely:
- Changing a class hierarchy - adding or removing a superclass or mixin.
- Changing a
StatelessWidgettoStatefulWidgetor vice versa. The element type must change, so the old element cannot be reused. - Enum-to-class conversions, and some generic signature changes.
main()edits, which already ran.- Global and static field initialisers, which are evaluated lazily but only once.
Flutter tells you when it needs a restart rather than failing silently, which is the important part - you get a message, not stale behaviour.
Compile errors leave the old code running
If the changed file does not compile, the tool reports the error and the app keeps running the previous version. This is a feature, but it produces a recognisable confusion: you edit, save, see no change, and assume hot reload is broken, when the terminal is telling you there is a syntax error.
Checking the console before restarting saves a surprising amount of time.
State outside the widget tree survives differently
Hot reload preserves State objects because the element tree survives. But most
real apps keep state somewhere else too, and those follow different rules.
A bloc or notifier held by a provider survives, because the provider element survives - so an edit to its handler logic applies to future events while the current state stays put. That is usually what you want, and occasionally baffling: you fix a reducer, and the value on screen is still the one the old reducer produced.
A singleton registered in get_it at startup survives too, but its
constructor does not re-run. Change what it computes on construction and nothing
happens until a restart.
A module-level final is initialised lazily, once. After that it is frozen
for the life of the isolate.
The pattern is consistent: hot reload swaps code, never re-runs initialisation. Everything confusing about it follows from that one sentence.
// Edit the URL here and hot reload: nothing changes.
// It was read once, when the singleton was constructed.
final apiClient = ApiClient(baseUrl: "https://api.example.com");
class HomeState extends State<Home> {
// Edit this and hot reload: nothing changes. initState already ran.
late final _controller = TabController(length: 3, vsync: this);
@override
Widget build(BuildContext context) {
// Edit anything here and hot reload: applies immediately.
return TabBarView(controller: _controller, children: _pages);
}
}Why the whole tree rebuilds, not just the changed file
Flutter does not try to work out which widgets your edit affected. After the code swap it marks the root dirty and rebuilds everything.
That sounds wasteful and is the pragmatic choice. Tracking which classes changed and which elements instantiate them would mean maintaining a dependency graph between source files and live objects - fragile, and wrong the moment a changed function is called indirectly. Rebuilding everything is O(tree), and a Flutter tree is cheap to rebuild by design.
It also has a practical consequence worth knowing: a rebuild is not a
remount. Your build methods re-run, but initState, didChangeDependencies,
and dispose do not fire. If a hot reload leaves the app in a strange state, it
is almost always because you edited code in that second category.
Getting more out of it
Hot reload is fast enough that its real limit is usually your app's structure, not the tool.
Keep state out of initState where you can. State that lives in a bloc, a
notifier, or a repository is not tied to element creation, so more of your edits
apply cleanly.
Build a screen you can reach quickly. Hot reload preserves navigation, so a deep screen behind five taps stays reachable - until something forces a restart, and then you are tapping again. A debug-only route that jumps straight to the screen under construction pays for itself.
Use reassemble for expensive caches. It is called after every hot reload,
which makes it the right place to invalidate a cache that a code change
invalidates:
@override
void reassemble() {
super.reassemble();
_layoutCache.clear(); // debug-only recompute after each reload
}Remember stateful hot reload is state you might not want. A bug that only appears after several reloads is often stale state, not a real defect. Confirm with a restart before investigating.
Key takeaways
- Three pieces, not magic: a VM that patches code in a live isolate, an incremental compiler that sends a small delta, and a widget tree that is cheap to throw away.
- Elements and
Statesurvive; widgets do not. That is why state is preserved and why edits tobuildapply. - Code that runs once does not re-run.
initState, field initialisers,main, and static initialisers need a hot restart. - Structural edits force a restart - changing a class hierarchy or switching stateless to stateful.
- A compile error keeps the old code running. Read the console before assuming the tool failed.
reassembleis the hook for invalidating caches on every reload.
FAQ
What is the exact difference between hot reload and hot restart?
Hot reload injects new code and rebuilds the widget tree, keeping app state. Hot
restart destroys the isolate and starts main() again, keeping only the compiler
warm. Reload is sub-second; restart is a few seconds.
Does hot reload re-run my build method for every widget?
Yes - the framework marks the root dirty and rebuilds the whole tree. That is cheap because widgets are immutable descriptions, and it avoids maintaining a fragile map from changed source files to live elements.
Why doesn't hot reload work in release builds?
There is no JIT and no VM to patch - release is AOT-compiled native code. This is the same trade that makes release builds fast and bridge-free.
Does hot reload work on web and desktop?
Yes on desktop, with the same rules. Flutter web supports hot restart reliably; hot reload support has improved but is less complete than on native targets.
Why did my app end up in a weird state after reloading?
Because state was preserved across a change your code assumed would run at startup. It is usually a reload artefact rather than a bug - verify with a restart.
Does hot reload affect performance profiling?
Yes, and significantly. Debug builds are JIT-compiled and unoptimised. Never draw performance conclusions from a debug build; profile in profile mode.
Conclusion
Hot reload is not a bolted-on developer convenience. It falls out of decisions Flutter made for other reasons - a VM-hosted development mode, an incremental compiler, and immutable widgets over persistent elements - and it is a good example of an architecture paying off somewhere it was not aimed.
Knowing which of those three pieces a given limitation comes from turns the failures into a short, predictable list rather than a source of superstition.
Read more
The reconciliation that makes reload safe is the same one that makes rebuilds cheap - see How Flutter Rendering Actually Works. For the compilation model behind it, see Flutter Is Not Just a UI Framework.