Converting a classic Ember app to Octane, in working order

Octane has been Ember's programming model since 3.15, and every codebase we assess sits somewhere on the spectrum between "fully classic" and "fully converted." The apps that get stuck are almost never stuck on the mechanics of any one conversion — they're stuck because someone tried the conversions in the wrong order, hit a wall of interlocking failures, and reverted the branch. Order is the whole game. Here's the sequence we use, and why each step comes where it does.

Step 0: Get to a version where Octane idioms work

Everything below assumes you're on at least 3.28 LTS with the Octane edition enabled. If you're earlier, do that upgrade first as pure version work — don't mix idiom conversion into a version bump. A branch that changes the framework version and the component model has no useful bisect points when something breaks.

Step 1: Angle-bracket invocation

Convert {{my-component foo=bar}} to <MyComponent @foo={{bar}} /> across the entire template tree using ember-angle-brackets-codemod. This goes first because it's the safest transform (purely syntactic, no runtime behavior change), it makes the argument/attribute distinction visible, and every later step reads better with explicit @-prefixed arguments. Run it wholesale, review the diff for the handful of ambiguous cases (component-vs-helper name collisions), and land it in one PR per app section if the diff is unmanageably large.

Step 2: Explicit this in templates

Run ember-no-implicit-this-codemod so every property reference in templates is {{this.foo}} or an @argument. Like step 1, it's syntactic and safe — and it's a prerequisite for reasoning about what a template actually depends on, which you'll need constantly in step 4.

Step 3: Native classes — but deal with mixins first

ember-native-class-codemod converts EmberObject.extend({...}) classes to native class syntax with decorators. It works well, with one hard blocker: mixins. A class using SomeMixin can technically remain class Foo extends Component.extend(SomeMixin), but that hybrid is where conversions go to die — you get native-class ergonomics with classic-object semantics underneath.

Before running the codemod, inventory your mixins and sort them into three buckets: behavior that becomes a utility function or service (most of them), behavior that becomes a base class (rare, but legitimate), and behavior that becomes a decorator or class field. Retire the easy ones first. The codebase doesn't need to be mixin-free to proceed, but every mixin you keep is a cost you'll pay again in step 5.

Step 4: Tracked properties

This is the step that changes runtime behavior, so it gets the most care. The move is from computed properties with declared dependent keys to @tracked fields plus plain getters. Two patterns need real thought rather than mechanical conversion:

Observers. There is no Octane equivalent of an observer, on purpose. Each one has to be redesigned — usually into a getter the template pulls from, occasionally into an explicit call at the mutation site. Budget actual engineering time per observer; this is where "the codemod will handle it" optimism dies.

Computed properties with side effects or caching assumptions. A classic computed property caches until a dependent key invalidates it; a native getter runs every time it's consumed, tracking makes re-renders correct but not free. The overwhelming majority of computed properties convert cleanly to getters. The ones doing expensive work need @cached (from ember-cached-decorator-polyfill on older versions) or a redesign.

Step 5: Glimmer components, leaf-first

Classic @ember/component and Glimmer components coexist fine, so convert incrementally, leaf components first — they have fewer arguments flowing through them and no yielded blocks to reason about. The recurring work items are predictable: lifecycle hooks (didReceiveAttrs, didInsertElement) become getters, modifiers like {{did-insert}}, or custom modifiers; implicit attribute bindings become explicit ...attributes; anything reading this.element needs a modifier. Components that are mostly template convert in minutes. Components that are mostly lifecycle hooks are redesigns — schedule them as such.

What to skip

Don't convert dead components (delete them), don't convert components slated for replacement in the next quarter's product work, and don't hold the whole effort hostage to the worst five components in the app. A codebase that is 90% Octane with five well-understood classic stragglers is a healthy codebase.

The meta-rule

Each step above lands independently, keeps the app shippable, and makes the next step easier. If you find yourself with a long-lived octane-conversion branch, stop — that's a rewrite wearing a codemod's clothes, and it will lose to the roadmap every time.