Adopting Embroider in a mature Ember app

Embroider is Ember's modern build system: instead of the classic ember-cli broccoli pipeline, it compiles your app and its addons into a static package graph that a mainstream bundler can process. The payoff for a mature app is real — route-based code splitting, actual tree shaking, faster rebuilds, and a build your newer hires recognize. But mature apps also carry exactly the patterns Embroider's static analysis dislikes, so adoption is a ratchet, not a switch. Here's the sequence that works.

Understand the compatibility ladder

Embroider runs in modes. With all static flags off, it wraps even badly behaved addons in a compatibility layer and mostly just works. Each flag you enable — staticHelpers, staticModifiers, staticComponents, and eventually splitAtRoutes — trades runtime flexibility for static analyzability. The strategy is to get green at the permissive end, then earn the flags one at a time.

Stage 1: Build at all, flags off

Add the Embroider packages and get a production build and a passing test suite with maximum compatibility settings. Most failures at this stage come from addons, not your code. Triage each failing addon three ways: upgrade it (many popular addons shipped Embroider-safe or v2-format releases years ago), replace it (the ember-observer graveyard is full of addons whose maintained successor is easy to find), or vendor the small ones and fix them yourself. This stage is also simply a high-quality audit — the list of addons that fail is very close to the list of dependencies that were already risks.

Stage 2: Static helpers and modifiers

Turn on staticHelpers and staticModifiers. These are usually cheap: helpers and modifiers are almost always invoked by literal name in templates, so the resolver's dynamic fallback rarely mattered. When something breaks here it's typically a helper invoked through a variable, which is worth rewriting anyway for readability.

Stage 3: Static components — the real work

staticComponents requires every component invocation to be statically resolvable, and this is where mature codebases meet their {{component}} habit. Fifteen-year-old pattern: a form builder or dashboard that does {{component this.rowType}} with a string assembled at runtime. Embroider can't know what that resolves to, so it can't tree-shake anything — and with the flag on, it refuses.

The fix is to make the choices explicit. Replace string-assembled lookups with an explicit map — import the candidate components, choose among them in JavaScript, and invoke the chosen class. The ensure-safe-component helper from @embroider/util covers the transitional cases where a component name still arrives as a string from outside. Expect this stage to be a series of small, honest refactors; each one also removes a place where a typo'd string failed silently at runtime.

Stage 4: Route splitting

With the static flags green, splitAtRoutes starts paying the dividends: users on the login screen stop downloading the admin console. Mark the route boundaries you want split and check the lazy edges — code that assumed a component class was loadable synchronously from anywhere (modals and route-crossing shared state are the usual suspects) needs the import graph cleaned up. Measure before and after with a bundle analyzer; on the apps we work with, first-load JavaScript typically drops dramatically once the biggest authenticated sections are split out.

Practical advice that saves weeks

  • Ratchet in CI immediately. The moment a stage is green, make it the CI build. A compatibility level that isn't enforced regresses within a sprint.
  • Keep a scorecard, not a branch. Addon fixes, {{component}} refactors, and flag flips all land on main independently. The long-lived "embroider branch" fails for the same reason every long-lived branch fails.
  • Do it after Octane, not before. Octane conversion reduces exactly the dynamic patterns Embroider objects to. Sequencing Embroider first means paying for the same refactors twice.
  • Budget by grep. {{component occurrences, addon count, and ember-observer scores give you a defensible effort estimate before you commit to anything.

Why bother, honestly

Because the build is where a legacy app's age shows most. An Ember app with a static, split, tree-shaken build behaves — and hires — like a current one, and Embroider is also the bridge to the ecosystem's ongoing tooling work rather than a dead end. For an app you intend to keep, it's among the highest-value modernization moves available. For an app you intend to leave, stop at stage 1 — a working Embroider build with flags off is still a better launchpad for a strangler migration than the classic pipeline.