I am currently working on a legacy Ember.js application (Ember version 3.4.8). We are trying to improve the build and load time of our Ember application. I came across both Embroider and Ember Engines, and by the initial thought both seem to work for our case. Can both Embroider and Ember Engines be used in conjunction with each other, is it a best practice? TIA!
Great question! I have plenty of advice to give here because It’s something my team will be evaluating soon (engines + embroider). We already use 6 or 7 lazy-loaded routable engines. We are just upgrading from 3.28 to 4.0 and hopefully soon after that we’ll move through 4.x and 5.x versions quickly.
In the past we used engines for the lazy-loading benefits and also for the isolation. As our team has scaled they have been helpful for organization and defining ownership boundaries. Splitting engines out makes it easier to have isolated test suites which can help with CI/CD parallelization (although that’s optional). That said there are some downsides to engines as well. They’re not necessarily first class citizens in the Ember ecosystem so there are some rough edges. Initial setup is non-trivial.
Now that we’re working on Embroider adoption we’re going to have to weigh the pros and cons and decide if we want to keep engines or migrate them to a different package/routing strategy. Relevant factors include:
Under embroider the lazy-loading benefits are no longer a factor because (eventually) you get code splitting and tree shaking
It’s unclear if engines will even work with embroider and other features of Ember in the long-term because they need some investment. They aren’t yet v2-addon compatible IIRC, and some things like strict mode template tags in tests just don’t work (gts/gjs components work fine though).
There are still organizational benefits to isolating a subgraph of the router in an engine. AFAIK there’s currently no way to do the equivalent with routes in a v2 addon. Isolating routes is nice so your main application router doesn’t end up HUGE. Engines can also be mounted more than once or in multiple applications if you have that kind of use case. That said there are downsides if you frequently link between engines (external routes are more boilerplate). It’s possible the planned router overhaul will make this sort of thing easier without engines but I have no idea.
There was some discussion of trying to get engines to work with Module Federation (meaning engines could be independently deployable). This would be a very compelling reason to keep using them. That said it doesn’t seem like this effort has gone anywhere and module federation in general seems like a messy wild-west thing in the broader ecosystem.
So… there’s a lot to consider but here’s my general advice to you:
Invest whatever resources you possibly can into upgrading Ember. I’d upgrade at most a couple minor versions at a time (at least until you get out of the 3.x series) and make sure you clear any deprecation warnings as they come up instead of saving them all for the 4.0 upgrade.
Once you get up to 3.28 you can upgrade your addons to newer versions that are v2 addons. This will help a little bit with bundle size and build times because they’ll use ember-auto-import.
If the only reason you were exploring engines was lazy loading or separate bundle sizes I’d strongly recommend avoiding. Even if you think they’d be beneficial for other reasons I’d focus on upgrading Ember and adopting Embroider first. This will be by far the biggest bang for your buck in terms of bundle size, build times, and general developer experience. If you decide engines are something you want to look into later you can always explore that down the road.
Let me know if you want to dig in on anything more, and good luck!
Thanks for the detailed explanation and advice, it did clear up a lot of my doubts. Much appreciated!
My query stemmed from my understanding of Ember Engines and Embroider - I thought both of them to serve very different purposes. Anyway, for the initial phase we are just trying to speed up the build and load time, and looks like investing time in Embroider might be beneficial rather than trying to use Ember Engines. Also, regarding the Ember version upgrade, unfortunately that is currently not in the pipeline of my org for the foreseeable future (mainly due to the effort that it will take).
Also thought I should clarify a few more doubts in this thread,
We use ember-cli version 2.16.2 and ember-source version 3.4.8 (not sure which is actually called the ember version). We don’t use any fancy Glimmer components, just pure Ember.js classic components. Anyway, is Embroider compatible with these versions?
We use dynamic components {{component componentName}} in a lot of places - which can’t be mapped to a ComponentMap due to the truly dynamic nature of it. How do we handle that when using Embroider?
Also, regarding the Ember version upgrade, unfortunately that is currently not in the pipeline of my org for the foreseeable future (mainly due to the effort that it will take).
I know this happens in many/most companies but you may be out of luck with Embroider unless you can also upgrade. I don’t think you could adopt Embroider given your current versions and even if you could get it working it would be in full compatibility mode which means far fewer of the benefits (and no Vite!).
Partly why I suggested upgrading in increments is that you can pace out the upgrades in small chunks (it’s also easier to actally do the upgrades). This can let you upgrade incrementally (e.g. if using scrum like one ticket per sprint) rather than a big expensive hard-to-justify investment.
I’ve also found success framing it in terms of cost of NOT upgradting. For example Ember 3.x is way outside security patch range, and as you are experiencing has some big performance and feature downsides to more modern versions. This extends not just to the app/framework itself but also to the underlying technology e.g. Node (running older versions of node could become increasingly risky and more difficult).
Ember 3.4 is now ~6.5 years old which is a lifetime in javascript years. Not saying that to denigrate you or your app or company in any way (believe me, I’ve been there) but it’s a problem that only compounds if you don’t tackle it. So I guess you have to sit down and ask what the long term strategy is here. My concern with going down the engines route is that you could end up piling more tech debt on yourself when it’s not a real solution to the problems you have.
We use ember-cli version 2.16.2 and ember-source version 3.4.8 (not sure which is actually called the ember version). We don’t use any fancy Glimmer components, just pure Ember.js classic components. Anyway, is Embroider compatible with these versions?
ember-source is mainly what we mean by “ember version” although the CLI version is relevant too. I would not expect Embroider to be able to work until the upper 3.x versions, probably ~3.28. Even if you can get it to work in that version you’d have to run it with a lot of the compatibility features turned on which cut into the upsides.
We use dynamic components {{component componentName}} in a lot of places - which can’t be mapped to a ComponentMap due to the truly dynamic nature of it. How do we handle that when using Embroider?
The way this looks in “the new world” is that instead of rendering a component by name you’d actually import the class:
import SomeComponent from 'my-app/components/some-component';
...
{{component SomeComponent someArg="foo" ...}}
If your application is so dynamic that you can’t even create a component map that could be problematic for Embroider. But I’d need to know more about your use case.
Thanks again for the prompt reply! Our usecase for dynamic components is that we have a component that has a default behaviour. If the developer wishes to have some custom implementation other than that of the default behaviour, we provide the flexibility to use customComponents (but the params passed to both components will still be the same).
For example, consider our parent component has a default dropdown component in our application, the properties of which will be selectedValue and list of options and that the parentComponent has few actions depending on the dropDown.
If multiple users of this component, want different styles to be applied to the listOptions, or the dropdown style needs to be entirely changed wherein they want to support a nested dropdown, then they can have their own custom component which can be called by the parent component, but the params to the custom component will still be the same.
PS: Not sure, if this a best practice, and if there exists a better way/design pattern to solve this, but this is how it exists in our current application
Yeah that doesn’t seem too crazy. We do this sort of thing for sure (e.g. for custom table cells). The way this would look in “the new world” would be:
We too have a similar component for custom table cells in our application
In our org, the structure is as follows:
Parent codebase - Includes generic components like the table component but has no knowledge of which components might be passed to CustomCellComponent
Each child codebase - Has its own Ember app, uses the table component, and may know which components can be passed as CustomCellComponent, though this logic currently exists on the server side.
If I generate a componentMap for all possible CustomCellComponents, it will keep growing, increasing route chunk size and load time. Wouldn’t this go against Embroider’s design principles? Is there a better alternative?
I think this is probably the key part. In order to be able to build with tree-shaking you need to be able to tell which components are used at build time. If that logic exists on the server I’m not sure there’s a good way to know that.
Yes I think that’s more or less correct. I’d still expect the bundle sizes and load times to be better under “full” Embroider becuase anything else in your app could benefit form tree shaking etc but unless you can know what will be used at build time you have to throw in everything.
I’m guessing you could gradually shift to different patterns without losing the fundamental ideas though. I’m speculating wildly here but if you could have smaller and more specific “component maps” that isn’t really fixing the pattern but at least it’s a step in the right direction. For example if a child codebase knows that it only needs a subset of the components it could define it’s own “component map”.
What we do for custom cell components is specify our cell components in the consuming template, something like:
which of course will soon change to more like this:
import SomeCustomCell from '.../custom-cells/some-custom-cell';
...
col1=(hash ... component=(component SomeCustomCell))
Of course this is easier to map to tree-shaking and static analysis because you know exactly when the component will be used.
Another thing that may help is shifting more towards contextual component patterns. For example the select component example you provided earlier could potentially use contextual components to reduce the number of custom components you need because it provides more flexibility. Again though that’s just speculative based on what you’ve said so far.