NK

Search

Search pages, projects, posts, components, and icons

All articles
Mobile12 min read

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.

flutterdarttoolingfundamentals

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.

Glossary

Everything this post uses, defined before it is used. Skip it if the terms are already familiar, or come back when one of them trips you up.

The one sentence the rest of this post expands: hot reload swaps your code inside the running app and rebuilds the screen, and it never re-runs code that only runs once.

Terms

TermMeaning
Hot reloadPushing changed code into the app that is already running, keeping its state.
Hot restartKilling the running program and starting it again from main(), losing state.
Dart VMThe program that runs your Dart code during development. Think of it as the engine your app sits inside.
IsolateDart's unit of execution, with its own memory. Your app runs in one by default.
Kernel fileThe compiled form Dart code is turned into before the VM runs it.
Incremental compilerA compiler that stays running and recompiles only the files you touched.
Debug buildThe build you develop against. Slower, but patchable at runtime.
Release buildThe build users install. Compiled ahead of time, so nothing can be patched.
WidgetAn immutable description of a piece of screen, thrown away and recreated constantly.
ElementThe long-lived object behind a widget. It survives rebuilds and holds State.
StateThe object holding mutable data for a stateful widget, attached to an element.
RebuildRunning build() again to get a fresh description.
RemountCreating the element from scratch, which does run initState again.
initStateThe method that runs once, when the element is first created.
Field initialiserA value assigned where a field is declared, evaluated once.
SingletonOne shared instance created at startup and reused everywhere.
reassembleA debug-only hook the framework calls after every hot reload.

Abbreviations

ShortFull formIn plain words
VMVirtual MachineThe program that runs your Dart code, rather than the chip running it directly
JITJust In TimeCompiled while the app runs, which is what makes patching possible
AOTAhead Of TimeCompiled before shipping, into fixed native machine code
UIUser InterfaceEverything the user sees and touches
URLUniform Resource LocatorA web address
FAQFrequently Asked QuestionsThe question section near the end

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

Before the detail, the plain version. During development your app runs inside a program that can be told "these functions have new bodies now", and it accepts the swap without stopping. Nothing is thrown away and rebuilt from scratch.

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

Compiling a whole app from cold takes seconds. Compiling one file you just edited takes almost none, as long as something remembers the rest.

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.

A mental model that stays true at every level: widgets are the menu, elements are the table with your coat still on the chair. Hot reload reprints the menu. It does not clear the table.

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 StatelessWidget to StatefulWidget or 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.

Going deeper: 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);
  }
}

Going deeper: 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 State survive. Widgets do not. That is why state is preserved and why edits to build apply.
  • 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.
  • reassemble is 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.

References

Official documentation for the topics covered here.

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.