How OWL Changed Odoo Frontend Development
1. Introduction
Why OWL Exists and Why It Keeps Changing
If you've built Odoo modules for a while, you've watched the frontend quietly transform.
The old jQuery-heavy, widget-wiring days are mostly behind us. What replaced them is
OWL
Odoo Web Library a reactive component framework that genuinely competes with React and Vue.
OWL landed with Odoo 14 in 2020, and the shift was significant. Instead of manually wrestling with the DOM and chasing legacy widget events, you could write clean, self-contained components and let the framework handle the heavy lifting.
But it didn't stop there. OWL 2 (Odoo 16/17) introduced hooks and delivered serious performance improvements. OWL 3 is still taking shape, but it's already promising signals-based reactivity and a plugin architecture that could change how we build Odoo UIs entirely.
This guide walks through all three versions what changed, why, and what it actually means for your day-to-day work. Think of it less as documentation and more as a developer-to-developer conversation about a framework that's still finding its ceiling.
Quick Version Summary
OWL 1 → Component classes with lifecycle methods (Odoo 14–15) | OWL 2 → Hooks-based API, 20× faster rendering (Odoo 16–17) | OWL 3 → Signals, plugins, fine-grained reactivity (Planned, 2026+)
2. Internal Workflow
What Happens When OWL Renders
Before diving into version differences, it's worth understanding what OWL is actually doing. At its core, it handles three things: compiling templates into DOM, managing reactive state, and updating the UI efficiently when data changes. Each version tackles this a bit differently.
2.1 OWL 1 Parse, Compile, Render
When your component loads, OWL's QWeb engine compiles your XML templates into a virtual DOM. That gets mounted into the browser, and the UI appears. When something changes a button click, a form submission state updates, OWL queues a re-render, diffs the result against the current DOM, and applies only what's necessary. Familiar concept if you've used React; just living entirely inside the Odoo ecosystem.
2.2 OWL 2 Hooks Take the Wheel
The rendering pipeline stays largely the same, but how components are
initialized changes completely. Out go lifecycle overrides like
mounted() and willStart(). In comes a single
setup() function where you register everything as hooks.
The framework owns the lifecycle; your code just plugs in where it needs to.
Key Concept
This isn't just cleaner it makes components genuinely easier to test and compose. It also lets OWL batch and defer re-renders more precisely, which explains the noticeable performance jump between versions.
2.3 OWL 3 Signals Change the Game (Proposed)
OWL 3 wants to ditch the "state changed → re-render component" model entirely. Instead, signals lightweight reactive primitives track exactly which parts of the UI depend on them. Change a signal, and only the specific nodes reading it update. Nothing else touches.
Clarification
It's the same approach Vue 3 and Svelte have proven out, and for complex, data-heavy UIs, the performance difference can be substantial. Less guesswork, more precision.
3. Architecture
How Components Are Structured Across Versions
The clearest way to see OWL's evolution is through the component lifecycle: how it starts, fetches data, updates, and cleans up.
3.1 OWL 1 Class Methods for Everything
Components extended owl.Component and used override methods for
each lifecycle stage. willStart() for async data,
mounted() after DOM insertion, willUnmount() for cleanup.
Reactive state came from useState() mutate the returned proxy,
and OWL schedules a re-render automatically.
It worked, but logic scattered across multiple methods made components harder to follow as they grew.
3.2 OWL 2 One Function to Rule Them All
OWL 2 pulls everything into setup(). Instead of overriding methods,
you register hooks onWillStart(), onMounted(),
onWillUnmount() as plain function calls in one place.
Read a component top to bottom, understand it completely. No jumping around.
Two other quality-of-life upgrades came along for the ride. Props moved from
a loose string array to a typed schema, making components self-documenting and
catching bad prop usage early. Services like RPC and notifications became
accessible through useService(), replacing global imports with
explicit, testable dependencies. It's a smaller surface area with significantly
fewer surprises.
4. Real Examples
Seeing the Difference in Action
Abstract comparisons only go so far. The clearest way to understand the shift from OWL 1 to OWL 2 (and to OWL 3) is to look at the same component written across versions.
4.1 Example 1: A Simple Counter
Let's start with the most basic example a button that increments a counter.
class Counter extends owl.Component {
setup() {
this.state = useState({ count: 0 });
}
increment() {
this.state.count++;
}
}
Counter.template = xml`
<button t-on-click="increment">
Count: <t t-esc="state.count"/>
</button>`;
class Counter2 extends Component {
static props = {
start: { type: Number }
};
setup() {
this.state = useState({
count: this.props.start || 0
});
onMounted(() => console.log('Counter ready'));
}
increment() {
this.state.count++;
}
}
import { signal, onMounted } from "@odoo/owl";
class Counter3 extends Component {
setup() {
const count = signal(0);
onMounted(() => console.log('Ready'));
return { count };
}
increment() {
this.count.set(this.count() + 1);
}
}
Notice the progression: OWL 1 uses simple property assignment inside
setup(). OWL 2 adds proper props validation and lifecycle hooks.
OWL 3 replaces the proxy-based useState() with a signal
a leaner, more targeted reactive primitive.
4.2 Example 2: Fetching Data from the Odoo Backend
A more realistic scenario: a component that loads records from Odoo via RPC and renders them in a list.
class NameList extends Component {
setup() {
this.state = {
names: [],
loading: true
};
this.rpc = owl.services.rpc;
}
async willStart() {
const res = await this.rpc(
"/web/dataset/call_kw",
{
model: "res.partner",
method: "search_read",
args: [[], ['name'], 5],
kwargs: {},
}
);
this.state.names = res.map(r => r.name);
this.state.loading = false;
}
}
import { useService } from "@web/core/utils/hooks";
class NameList2 extends Component {
setup() {
this.state = {
names: [],
loading: true
};
const rpc = useService("rpc");
onWillStart(async () => {
const res = await rpc(
"/web/dataset/call_kw",
{ ... }
);
this.state.names = res.map(r => r.name);
this.state.loading = false;
});
}
}
The logic is identical only the structure changes. The OWL 2 version is
arguably easier to read because everything related to the component's behaviour
lives inside setup(), rather than being split across multiple methods.
For large components, this difference really adds up.
5. Performance
Where OWL 2 Made the Biggest Leap
This is where the upgrade gets hard to ignore. Odoo's own team reported backend views loading up to 20× faster after moving to OWL 2. One view that used to take ~800ms was rendering in ~40ms. That's not a benchmark number that's a difference you actually feel while using the app.
The gains came from smarter batching. Instead of re-rendering on every individual state mutation, OWL 2 groups changes and applies one pass. Fetch data, update five state properties, get one re-render. OWL 1 would've fired five. That adds up fast on complex pages.
Fewer redundant RPC calls and cleaner lifecycle management helped too pages loaded with dozens of charts and dynamic widgets started feeling genuinely snappy.
OWL 3 wants to go further. Signals mean only the exact DOM nodes tied to a changed value get updated nothing else even looks at it. For real-time dashboards or collaborative views with constant data churn, that surgical precision could be another meaningful leap. That said, for most Odoo projects, OWL 2 is already plenty fast. Unless you're managing thousands of simultaneous reactive bindings, you're unlikely to hit a wall.
OWL 2 in Numbers
20× faster page loads in backend views | ~800ms → ~40ms for large view rendering | Fewer unnecessary RPC calls | Smooth rendering even with 1,000+ list items
6. Writing OWL Components That Age Well
Guidelines to keep OWL components predictable, testable, and ready for future upgrades.
7. Common Mistakes
What Trips Developers Up
Migrating Lifecycle Methods but Missing One
The most frequent OWL 1→2 migration bug is converting
mounted() to onMounted() but forgetting
patched() or willUnmount(). The old methods
still exist silently they just don't fire. Cleanup or chart-update
logic simply stops working, and it's not always obvious why.
Calling Hooks Conditionally
OWL 2 hooks follow the same rules as React hooks: they must be called in the same order on every render. Hooks must always be called unconditionally from the top level of setup().
Directly Mutating Objects Inside useState
Writing this.state.items.push(newItem) looks like it
should work, but deeply nested mutations can slip past OWL's reactivity
proxy without triggering a re-render. Either reassign the array or use
OWL's reactive utilities.
Confusing @odoo/owl Imports with Global owl
OWL 2 uses ES module imports:
import { Component, useState } from '@odoo/owl'.
Using the legacy global owl object in a mixed codebase
can cause version conflicts that are surprisingly hard to debug.
Migrating to OWL 3 Too Early
OWL 3 is still experimental. Using speculative OWL 3 patterns in production OWL 2 code introduces instability and makes your codebase diverge from what Odoo officially supports. Prepare by writing clean, future-compatible OWL 2 code, and migrate to OWL 3 once it ships with a stable release.
8. Migration
Moving Between OWL Versions
8.1 OWL 1 → OWL 2: Manageable with a Checklist
The OWL 1 to OWL 2 migration is largely mechanical. The component structure stays the same you're mostly renaming things and reorganizing where code lives.
After converting lifecycle methods to hooks and updating the props schema, test each component individually before deploying. The Odoo 17/18 environment is your best validator.
8.2 OWL 2 → OWL 3: A More Significant Rewrite
The OWL 3 migration will be more involved because the reactivity model changes
at a fundamental level. useState() is deprecated in favour of
signal(). The env object goes away, replaced by a plugin system.
Some template directives are renamed or removed entirely.
The key migration steps, based on current OWL 3 draft documentation:
-
Replace
useState({ ... })with individualsignal()calls for each reactive value -
Use
computed()for values derived from other signals, rather than JavaScript getters -
Migrate from
useService('rpc')to the plugin API equivalent (usePlugin('rpc')) -
Update templates:
t-slotbecomest-call-slot, andt-portalis removed -
Fix
t-callusage:t-setvariables inside t-call blocks must be passed as attributes - Remove reliance on the global env object in all components
OWL 3 Migration Advice
Don't rush the OWL 3 migration. Wait for the stable release and official migration tooling. In the meantime, writing clean OWL 2 code with flat state and minimal env usage will make the eventual upgrade much less painful.
9. Side-by-Side Comparison
10. Conclusion
OWL's Journey and What It Means for You
OWL's evolution tells the story of Odoo's frontend growing up. Version 1 was a genuine improvement over the old widget system declarative, reactive, and component-based. But it was still learning. Version 2 took all that potential and delivered it at scale: hooks made code more composable, performance improved dramatically, and the development experience began to feel genuinely modern.
Version 3, once it arrives, will push the boundary further. Signal-based reactivity is a more precise and efficient model than proxy-based state, and a plugin architecture will make it far easier to build modular, testable Odoo front-ends. But it's still on the horizon treat it as something to prepare for, not something to adopt today.
For most Odoo developers right now, the practical takeaway is this: if you're still on OWL 1, the migration to OWL 2 is straightforward and well worth doing. The lifecycle hook rename is mostly mechanical, and the performance and maintainability gains are real. If you're already on OWL 2, you're in a good place write clean, hook-based components, keep props validated, and let the framework do what it does well. The Odoo frontend has never been more capable. Understanding how it evolved helps you write code that takes full advantage of where it is today and stays ready for where it's heading.
Quick Reference
Everything you should remember about OWL's evolution at a glance.
setup()
Register state, services, and lifecycle hooks all in one place.
Performance
OWL 2 batches state changes into a single re-render pass.
Services
Access RPC and notifications explicitly via useService().
Signals
OWL 3's fine-grained reactivity, replacing useState() proxies.
Props
Use a typed object schema instead of a string array.
Cleanup
Tear down listeners and subscriptions in onWillUnmount().
Frequently Asked Questions
Common questions developers ask about OWL in Odoo.
What is OWL in Odoo?
OWL Odoo Web Library is Odoo's reactive component framework, comparable to React or Vue. It handles template compilation, reactive state, and efficient DOM updates, and has powered Odoo's frontend since version 14.
What's the main difference between OWL 1 and OWL 2?
OWL 1 used class method overrides like willStart() and mounted(). OWL 2 replaces these with hooks called inside a single setup() function, and adds typed props and useService() for cleaner, more testable components.
How much faster is OWL 2 than OWL 1?
Odoo reported backend views loading up to 20× faster after the OWL 2 migration, with some views dropping from ~800ms to ~40ms, mainly thanks to smarter batching of state updates.
Should I start using OWL 3 now?
No. OWL 3 is still experimental and not officially supported. Write clean OWL 2 code flat state, minimal env usage, computed getters so the eventual migration to signals is straightforward once OWL 3 ships stable.