 Hello, EmberConf 2021. This is the second virtual EmberConf, hopefully the last one. It really has been 10 years since we shipped AmberJS and then a week later renamed it to EmberJS. And when we started, we used to say Ember was going to be a framework for 10 years. Now we didn't have any idea what 10 years from 2011 was going to look like at the time Vauer was really popular, ES modules hadn't landed yet, transpilers were still a pipe dream. All we knew was that things were going to change a lot over the next 10 years, and we knew that we wanted to build apps that could last for a while with our framework. And we also had lived through a lot of breaking changes in the past with other frameworks, and we also lived through a lot of apps that ended up doing rewrites for not very good reasons. And we really didn't like that, we really hated that. And we figured there must be other people who share our view that shipping apps is the most important thing, we should stop worrying about all this other stuff. And maybe we could build a community out of that. So how do we do, well, my own app Skylight started on Ember before 1.0, and it's on Ember 3.25 today. And it's not just on Ember 3.25, because Ember is compatible, we migrated to native classes in 2019, we migrated to TypeScript in 2020. And you should definitely check out Kristin's talk about how we migrated to Ember and how you how we migrated to TypeScript, and how you can use TypeScript to become a more confident developer. In general, if you look at our code base today, it really looks like an Ember 3.25 app, which looks pretty modern. So here's an example, this is a component that was in the repository in 2014, which is the first commit with a readme. And yeah, that really is what Ember used to look like, underscore super, this.get, define property, all that stuff. And this is the same component today, minus some comments. And it really looks like a component that you could write in today, 2021. And there's not really any good reason why we had to keep updating this component. It's just a feature flag component is quite small. But just because we kept upgrading Ember and just because people occasionally look at this, it got updated. And I think the really cool thing is that, again, even though this is such a simple component, we even migrated to use name blocks when they became useful. So I think this is pretty good evidence that an app that started way back when, was able to use Ember to continuously ship features and eventually end up on something that looks really modern. But it's not just Skylight. It's not just because I work on Skylight that made that true. Heroku, Intercom, Square, and LinkedIn all started their apps on early versions of Ember, and they all kept shipping their features through all the things that happened over the intervening years. Intercom started using Ember in 2013, and Gavin loves showing this graph of what happened to their productivity. And they kept up with Ember over the years. And the fact that they did that really powered their ability to keep that going. I really love that graph. And it's not just high-profile companies. Really small companies also got the benefits of this. This whole spectrum shows that our plan of building a framework that we could use to build apps for 10 years really did work. When people compare frameworks, they almost always evaluate frameworks frozen in time. And they rarely examine a framework's philosophy for adapting to change. And they rarely ask how application should adapt to change. The truth is that a framework can only be understood by evaluating how it adapts to change. By the time we're done today, I hope that I will be able to convince you that Ember's approach to change is the reason that so many apps that started nearly a decade ago with Ember are still going strong today. And if you take a look at those apps, they look like they were started yesterday, they look like they were started in 2021. Our story starts in 2011. At the time, I was a web app developer who had a full-time job working on Rails 3 and I was a core contributor to jQuery. And you know, I learned a lot about open source from John Rezeg. He taught me my pin tweet. John's opinion about breaking changes was that they were bad. Breaking changes disrupt the ecosystem. They kill momentum. If people get left behind, then the benefit of going forward is slower because there's less people. Plus, you end up having to spend a ton of time on supporting people who can't upgrade. And this is all true, but when jQuery started, we did all this work to make mouse enter work, even though it wasn't supported on all browsers. But jQuery today still doesn't come with TAP events. And the plugin model is based on manipulating a global prototype, like what? And you might want to use arrow functions, but oh well, jQuery's API is based on this. And this stuff adds up. Rails on the other hand had a pretty different perspective. We cherish progress over stability. I worked on Rails 3, I had pretty big goals for Rails 3, so I was pretty on board at first. But then I eventually realized that framing software development as a battle between progress and stability forced us to make a bad choice. Of course you would prefer to deal with regular breakage if the alternative is failing, but these aren't the only options. The truth is that progress and stability are two sides of a coin, and people force themselves into zero-sum thinking here. People get really annoyed at cruft in their code bases, and it becomes really easy to come up with a rationalization for making a big break. And you just have to get people whipped up for a little bit of time to make it happen. So you hear things like, we just have to rip the band-aid off. At the moment, these decisions sound reasonable. If you read the Python 3 blog post, it sounds perfectly reasonable. But here's the thing. Their perspective is on the extreme end of the spectrum. It's just that we have better language for talking about things that are on the extreme end of the spectrum than balance. For as long as I can remember, I had been haunted by the unfinished work of Jamie Zewinski, one of the original founders of Mozilla. After a year without shipping, he gave up. For him, shipping was the thing. But on his way out, he said, it wasn't obvious to him that his goals weren't possible. When he quit Mozilla, he left an open question. He left unfinished business. Was it possible to build an open source project owned by a community that prioritized shipping continuously? I was unsatisfied with the answers I had learned from JQuery and Rails. To avoid disruption, JQuery had accepted stagnation as a fact of life. And to avoid stagnation, Rails had accepted disruption as a fact of life. What this meant was that the status quo in open source was that you would get breaking changes every few years and you'd end up rewriting your app every few years. Both disruption and stagnation are enemies of shipping. So if we want to find a solution for shipping continuously, we need to find a way to balance them. After wrapping up Rails, I found myself adrift. DHH hated client-side JavaScript and the JavaScript community hated Rails. I really wanted something better in JavaScript, but basically all I really knew about JavaScript at the time was that JQuery turned into spaghetti once your app got big enough. I really wanted HTML and CSS to be central to the way I did client-side web development going forward. So I wrote Handlebars, a templating engine with an eye towards auto-updating, but I didn't really have any idea how to do auto-updating. Luck was on my side. Charles Jolly had built the mobile MeSuite web applications for Apple and he was leaving Apple to invest in open client-side tooling. Charles had a reactivity system and I had a templating engine. I was in. I joined his new company, Strobe. Charles introduced me to Tom Dale. At the time, Tom still worked on mobile me and we convinced Tom to join our team. This is how I imagined it went down. Charles taught me what he learned at Apple. Unfortunately, Strobe ran out of money before we managed to ship our product in HTML5 deployment platform. When we were thinking about what a framework for 10 years meant in the early days, what we thought was that apps after 10 years should still look idiomatic for 10 years in the future. jQuery didn't pull that off and even though Rails prioritized progress over stability, even Rails didn't pull it off. If we were gonna build a framework for 10 years, we needed to figure out how to roll in innovation on a regular schedule. A framework for 10 years was an aspiration. What that meant is that shipping apps is the North Star. Disruption and stagnation are enemies of shipping so we needed a solution that minimized both of those things. I wanna give you a glimpse into what software looked like back then. We're gonna take a look at the Firefox 3 release schedule but this is pretty representative. It reminds me a lot of other software projects I worked on at the time. What you're looking at is a table that I got from the Firefox 3 planning wiki. Let's check in in February. Whoops. Okay, let's check in again in May. Whoops. These tables are full of references to code freezes. So let's check in again in July. The beta date from the previous schedule. Nope, didn't make it either. What about milestone nine scheduled for October? What are these question marks? Okay, I'll put us out of our misery and jump ahead to March 2008. Well, we got some betas going so that's great and at this point it kind of given up on trying to predict anything and the table is just a list of releases that already happened. They ultimately shipped in June 2008 over a year after the original plan. You could tell the system wasn't really working by how much people had to beg you to follow the rules. We eventually reached a point where we wanted to commit to stability and so we announced a pre 1.0 to start fleshing out what that looked like. In the meantime, Chrome had come out and in their opening blog post, they said something is wrong with the conventional wisdom. They said shipping faster means higher quality. It was counterintuitive at the time, to me at least, but it was intriguing. The thing is it was aspirational. The thing that Chrome announced when they first shipped didn't work in the first year. There's a really good presentation by Anthony LaForge who was Chrome's project manager at the time that said basically that deadlines are bad. You don't want engineers to have to rush to avoid missing the train and when that happens releases get delayed anyway. And so they announced a two part solution. It's not just about changing the timeline although that matters a lot. Just going faster stresses people out so you would need some new rules. So it comes down to two basic rules. Always iterate on the development branch and always make sure the development branch is ready to ship. This all looked great to us. It was finally a way of thinking about development that didn't introduce all these moments that triggered disruptions. We announced that we would follow the Chrome release model with a release every six weeks. A week later, we announced the details of the plan. It looks a lot like the Chrome model. We wanted to make sure it was super easy for our users to follow so we invested in illustrations and diagrams. Our initial plan included all the elements that Chrome and Firefox had identified as essential. We would develop Ember on the master branch. New features landed behind feature flags which avoid disruption. We would use overlapping channels named exactly like Chrome. We would branch every six weeks. And importantly, we formed a release team to handle the automation. To ship a feature, all we had to do was change the feature and features that JSON2 enabled. And then the next time we reached the branch point, the feature would be enabled in beta and eventually release. If I'm being honest, Robert Jackson is really the secret sauce in our automation system. So our aspiration was release every six weeks, no deadlines for features, no code freezes, and if somebody feels pressured to ship a feature by a given date, there's always another train. So we had the first part of our evolution strategy, a strategy to evolve Ember predictably, smoothly, and with minimal disruption over 10 years. How are we gonna add brand new features? Release channels, the strategy was release channels. The next challenge came in our planning for 2014. Ember 1.0 had shipped with globals. Not just Ember as a global, but you stuck all of your application code into an application global, and people really wanted modules. So this raised the two new questions for our evolution strategy. First, how should we roll out transitions from one idiom to another? And second of all, how should we future prove idioms? We could do some migrations from time to time, but we wanted to avoid having it happen all the time. The situation back then was a mess. Forcing applications to pick module systems was a ticking time bomb to costly migration, exactly what we were trying to avoid. Things were so bleak that people seriously considered UMD, this monstrosity. And the built-in ecosystem wasn't in better shape. Bauer was the package manager for the front end at the time. Gulp, grunt, and brunch were popular, but they were primitive. This was a real challenge. We're not gonna ask Ember users to wait it out until things settle down. It's not okay for Ember apps to be stuck on globals for years after everyone realizes it's a dead end. But on the other hand, rolling your own is also not okay. It's not okay for Ember applications to have to manage their own transition off of Bauer, off of grunt, or off of whatever. But why couldn't we just say that this problem was out of scope? That it was something that Ember was just not gonna handle. Migrating off of Bauer was gonna be disruptive to applications whether we caused it or not. We were looking for a solution that would allow Ember applications to use modules in 2014 without just kicking the can down the road to a costly migration in the future. This became something of a mantra to me. Narrowly focusing on Semver is a recipe for bad decisions. Semver is a means to avoid disruption. It is not an end in and of itself. Ultimately, we went with JavaScript modules. This was in 2013, but what we realized is that static analysis is really helpful for making adaptations over time. And static analysis was not easy with CommonJS and was not easy with AMD. But at the time, nothing supported standard modules. They weren't standard yet. If our goal was that everybody in the Ember community would use modules as the way of writing code, we needed a standardized build system. And the build system had to be part of our evolution story. What it means for something to be part of our evolution story is that it rides the train. What that would mean is that upgrading Ember updated your build tools. Evolving away from Bauer or AMD could be managed through tooling updates. And because deprecations are an important migration tool, this allowed us to deprecate tooling changes. It allowed us to deprecate idioms. We had a solution for transitioning idioms over time. Ember CLI. But what about future proofing? We want a solution to our problem that doesn't just recreate the same issue a year later. I don't mean that the solution is gonna work forever, but rather that we've put some thought into the solution and that it has some properties that minimize future disruption. This part of the Ember 2014 blog post is a really good summary of why this worked out. The highlighted sentence, the lingua franca module format in Ember as ES6 modules is still true today. But the second one is barely recognizable. It's a fossil from a bygone era. We ended up with a principle. When we're faced with a choice between today's popular solution and an equally expressive declarative solution, we pick the declarative solution. It's really hard to understand this principle from a single moment in time. At every moment in time, people say, why not just go with the popular thing? But when you look at this from the perspective of multiple points in time, over multiple years, the popular thing changes over time. That doesn't mean you should over abstract just to future proof. The declarative solution has to basically be as good as the popular solution, if not better. And there has to be a meaningful explanation for why we expect change to happen in the medium term. Modules were a pretty good fit for this philosophy. I'm not gonna mark this as checked off because I think that there are other aspects of future proofing other than declarative. But this is a pretty good lead and it actually served us pretty well over the years. But what about removing features? When we first started, we carbocalled to JQuery's defer readiness concept in a bunch of our APIs. But then promises happened. And so we needed a way to remove those APIs entirely, something we hadn't done before. And so we designed our first deprecation system. Of course, you can only remove something in major versions. That's what Sembr is all about. But you first have to deprecate features. This is something that Sembr doesn't actually say anything about. Additionally, we said that features can only be deprecated in Ember if a replacement is already available and enabled. That would mean that by the time something's deprecated, it's always possible for people to move on to the next thing. Only the first of these bullets has anything to do with Sembr at all. But the point of Sembr is to minimize disruption. And so our policy for removing features was focused on minimizing disruption, not just Sembr. We lived through our first feature removal and added a new tactic to our evolution strategy, deprecation and Sembr. Ember's first templating engine was a variation of Handlebars' normal string-based story. But that had some serious limitations. These solutions were reasonable compromises when we got started, but it was really time to do something about them. Here's the good news. Our evolution strategy that we'd been building so far was a really good fit for what we were trying to accomplish here. First, the new HTMLBars compiler takes advantage of the declarative Handlebars syntax to swap out the implementation entirely. Second of all, we had already created an Ember CLI HTMLBars add-on to pre-compile templates. Because we did that, the Ember CLI HTMLBars add-on can be updated as part of an Ember upgrade. Third of all, the work is definitely gonna take more than six weeks, and we can stage it using feature flags, and we can roll it out to release channels when it's ready. And actually it's a good thing we did that because we found the problem in 1.9 and we had to back it out. All we had to do was flip a flag in features.json and everything else happened automatically. I think it's worth taking a step back here and think about what we accomplished. At first glance, it might look like all we did was work on process for a year. But all the process that we spent time on allowed us to do something pretty epic. We were able to take a string-based templating engine which was aging out and replace it with a much better strategy without breaking changes. We were able to evolve Ember without disrupting our users. This is what it means to focus on stability without stagnation. Stability is the means by which we deliver new features and adapt to changes without disrupting users. In a short amount of time, we were able to fully replace a string-based templating engine with a DOM-based templating engine and solve a whole bunch of real problems for our users without disruption. And that's a big deal. We got a lot done in the 1.9 era. It was all compatible so it's hard to remember if you were around back then, but some of these things were really important. I got a standing ovation for killing metamorph tags and binadder twice. And if you look at this screen, there's just a whole bunch of stuff here that really matters. We made pretty massive changes. We added Ember CLI. We added HTML bars. We landed modules. And nevertheless, people were sticking close to the latest release. We were successfully cranking out new features. We were successfully bringing in the best ideas from the ecosystem to Ember. We were successfully avoiding disruption. EmberConf 2014 was great. At the time, Ryan Florence was in the Ember community. And I gotta say, on top of it just always being a moral boost for me to go to EmberConf and see all the awesome things everyone's doing, I always love our commitment to good illustrations and art. It always makes me feel good to go back to 2014 and see that our art was awesome even back then. We covered how to add features to Ember, how to migrate idioms, how to remove features, but what about designing Ember? If members of the core team have special status, that means that you can get disrupted out of the blue with no chance to weigh in. Also, members of the core team don't hold a special monopoly on understanding. Active contributors can often be just as productive as core team members in discussions. Community members have special expertise that can come up when you least expect it. And honestly, sometimes people just wanna weigh in on stuff. Letting people weigh in on stuff without having to be an expert in the code lets more people participate. We added an RFC process to Ember before the stages process. And here are the rules. To add a new API that would require a feature flag, first you need an RFC. Removing an API that already landed on stable needs an RFC. And RFCs have to be merged before its feature flag can be enabled. And those rules apply to the core team, to contributors, and anyone who wants to participate in Ember's design. The solution for evolving the design of Ember's APIs, and also a lot of other things, including actual design like the website, is gonna be RFCs. But things don't stand still. In 2011, nobody was really doing client-side web frameworks other than things like Dojo, Sproutcore, and Cappuccino. But by 2014, other frameworks had become popular. Angular was hugely popular. Directives were like jQuery plugins you could use as attributes. And React, React wasn't yet the 800 pound gorilla we know and love. It had shaken up front end for good reason. Basically everyone had cargo-cultured two-way bindings from desktop frameworks. React showed that it's better to use functions to calculate values than just have data flying around. React also showed that diffing output is better than diffing data. And coarse-grained updates can perform better than ultra-fine-grained updates when considering initial render. And you really should consider initial render. All this made sense to us, but it wasn't gonna be the last time there was a revelation in the front end. And we didn't think that people should just change frameworks every time someone comes up with a good idea. We got an additional sense of urgency when Ryan Florence, who had since moved on to the React community, showed Ember being embarrassingly slow in his talk at ReactConf. He said Ember made him say no when he wanted to say yes to things. He couldn't ship, so he switched to React. And that makes sense. Our commitment to continuous shipping doesn't matter if you can't ship what you want. But here's the good news, we thought. We've been building a system for doing evolution, and this is what we've been building up to. So Tom and I set to work on a rethink of our rendering engine. We got one-way data flow, we got coarse-grained updating, and the results were competitive. But there was a problem. We hacked and slashed to make it compatible with 112. And we couldn't wait to get rid of the code. And we thought, that's what breaking changes are for, right? Time to rip the band-aid off. We didn't actually say that, I'm just foreshadowing. This is like the Titanic. If you're from the Ember community, you already know the end of the story. And what's really strange is that we focused a lot on stability without stagnation during this period. We had done a ton of work to define a process for evolving Ember smoothly. And things were working really, really well. Angular had recently said that because the web was changing a lot, we needed a full break. And we didn't think that was true. When we got the results of the 2015 Ember survey, it was confidence inspiring. The plan was definitely working. We thought we had our bases covered. So we talked about it. We can do pretty phenomenal things like introduce a brand new rendering engine that brings your apps neck and neck. And in some informal benchmarks, actually faster than React. I think that's a really powerful thing. And I think we need more of that in the JavaScript community. This is more foreshadowing. We thought, we're gonna make really good use of the strategies we defined. We have for least channels. We have RFCs. We made a road to Ember RFC. We have deprecation. We're definitely gonna make sure we deprecate everything we're gonna remove. We felt really good about all of this. But then it all went to hell. You see, we had made a critical mistake. We thought deprecations and removals are now part of our process. So we can do a lot of them. After we landed 2.0, it started out really rocky. When we took a step back, we realized that our technical strategy worked. It let us stage features without disruption in 1x. And this is important. By following our deprecation process, we were able to land new features in 113 that were needed for the transition. So in 113, you were able to do all the migrations that you needed to do to get to 2.0. 2.0 just removed the features that were no longer being used. So that all worked. That all made sense. But we just did too many breaking changes. It felt like a failure. The thing is, evolution is not just about Semver. Semver helps us achieve the goal of shipping. If you use Semver to violate this goal, it's not worth much. There was a bright spot. A lot of apps did migrate. As best as we can tell, the apps that were stuck on 113 were just a minority of Ember apps. And our careful strategy worked for a lot of people. What we did was a meaningfully better path than just breaking everything. And the add-on ecosystem was making the jump. And as always, our community was amazing. Every time I go to EmberConf makes me feel better. So I wasn't ready to just pack it in. But honestly, the miscalibration really hurt. We figured out a lot in the 1x era, but our first shot at breaking changes got a lot of people stuck. If our evolution strategy was gonna mean anything, we needed to rescue as many of the 113 apps as possible. The first order of business was making 2.x a nice landing place. When we took a look at the situation, we realized we only had one kind of deprecation. So the first thing we did was change the policy on deprecations to require that deprecations have a structured ID and a structured point in which they will be removed. And we also added an API so people could write tools for dealing with them one at a time. On top of that, the six-week release cycle wasn't working for everybody. Some people could migrate to a new version of Ember every six weeks, but some people just couldn't ship continuously if they had to upgrade every six weeks for various reasons. And if people couldn't upgrade, then the entire strategy fell apart. Let's take a look at the 1.0 series. It goes from Canary to beta, and then finally to stable. And the same thing happens with 1.1, the same thing happens with 1.2, et cetera. We added a new channel called LTS. Let's take a look at the highlighted version again. We would add a new LTS version every four versions. All it really does is extend the cycle. The nice thing about this is that all of our automation works the same, all of the mental model works the same. It's just another channel. The RSC was approved in October, two months after Ember 2.0. So what did it do to our process? First of all, it changed the release channel rules to add one more branch point. And we also made a small tweak to Intimate APIs. Despite 1.13, it felt like we were making progress. Our system was firming up. And then the 2016 survey came in. We already knew about the problems, but we didn't know exactly how bad it was. It was pretty bad. 36% of our users were stuck on 1.13. On the bright side, people who made it to 2x looked a lot like the 1x survey from 2015. But the thing is we were supposed to find balance between stability and stagnation. And to be honest, 1.13 just really hurt. So it was time to restore balance. We thought, let's try to do this stability without stagnation thing again. The process worked on 1x, so let's do it. Emberanos, assemble. So for the first half of the 2x series, we basically repeated the process, plus improvements to the release process. We added subteams based on the Rust Teams experience. We added a new mascot in Zoey. We landed LTS releases. We got some features done. And we just did just a whole bunch of quality of life improvements. And the thing is it worked. The community was able to build on a stable foundation. Things were trending in the right direction. But we had one more problem. We landed Glimmer in 1.13 to address a performance problem. But we created a new one. Component creation performance had gotten really slow. And this not only affected 1.13, but it also affected all of 2x. On top of all of that, Glimmer was better than the old rendering engine, but it was still not competitive with React, especially for first render, especially with a lot of components. I had a lot of ideas for how to make things better, but we knew we couldn't break everything again. We thought back to the 1x series and we realized that we had done HTML bars compatibly. The mistake that we made was just breaking too many things all at once. At this point, Tom had moved on to LinkedIn, so I did the work with Godfrey. We're gonna do a drop-in replacement by leveraging our declarative syntax. We have release channels. Release channels are gonna let us stage the work. It's definitely gonna take more than six weeks again. We have Ember CLI to distribute the compiler change, and it's a drop-in replacement, so we don't really need an RFC, and we're gonna try really hard not to deprecate things for no reason. We shop for the moon. We made a new benchmark that's stress-tested component creation. Every one of those boxes on that benchmark is a separate component. And it worked. Glimmer 1 was much slower than React, but Glimmer 2 was twice as fast as React, so we talked about it at EmberConf. By 2.4, we had the new code base integrated behind the flag, but unlike Glimmer, we wanted to commit to a drop-in replacement. Before we announced anything at EmberConf, we got skylight working, at least as a proof of concept, but we had a problem. If we wanted to commit to compatibility, Ember's tests were gonna get in the way. They were pretty coupled to the old implementation, and that was gonna be a ton of work. We had hundreds of tests that were coupled to old implementation details. We needed all of our tests to be abstract enough to test both Glimmer 1 and Glimmer 2 at the same time, and that's easier said than done. And it wasn't a very mechanical problem. This required real engineering, but what we realized is that people always underestimate non-contributors in their communities. A lot of people would love to help, but they just don't know how. So Godfrey took a day and wrote a long and detailed description of the plan for migrating our tests. 45 different areas of the tests we needed to be updated. It was time to call the Ember community to action. Let's do this. And people did it. A community formed around it. The people who did the first implementations helped the people after that. When all was said and done, this became a key way that we think about solving problems going forward, quest issues. We've used it ever since. It works. But here's the thing. The more you commit to writing instructions, the better it works. You have to take a leap of faith. You have to spend time writing instructions on the belief that people will show up. And in my experience, they always do. After reporting, we had a new problem. Porting the tests made them run on Glimmer 1 and Glimmer 2, but sometimes the ported tests failed on Glimmer 2. Obviously, that was the point. So we had to make them pass. And what we did was we repeated the process. We did another quest issue. We made a big list of failing tests and encouraged people to help us fix them. Again, quest issues worked. Eventually, we got all the tests to pass. And it was time to get the community to test out Glimmer 2. At every opportunity, we made it clear that it was a drop in replacement. If an existing app didn't work, we wanted to know. We wanted the release to be a drop in replacement, no excuses. And things had really firmed up at this point. We had gotten back to the momentum that we were used to in the 1x series. And we didn't want to risk that. So after squashing all the bugs we could find in the alpha, we enabled the feature flag in the Ember 2.9 beta. At this point, the system had gotten really wrote. Of course we're gonna use release channels. Of course that's gonna be how we roll things out. The beta started really well. People reported great success. But then a slow trickle of issues started coming in. They were mostly edge cases like block parameters named component, obscure issues with positional parameters, using string.length in the template, even though it's not data bindable. We could have easily justified moving forward on grounds that these were edge cases. And honestly, if we hadn't had the 113 experience, I think that's probably what we would have done. We would have said these issues are all about behavior that is not in the public API. It's beyond the scope of Semver. But as always, it's not just Semver. We needed to prove that we could do this as a drop in replacement. On the bright side, our channels made it really easy to do this mechanically. Instead of leaving the feature on features.json when we branched to stable, we turned it off. Automation, handle the rest. And there were pretty high stakes. If we could get this done as a drop in replacement, it would mean that we could make other major changes in the future inside of a major version. But if we couldn't, it wasn't clear that we would be able to adapt to future changes in the ecosystem fast enough. Major versions just don't come around quickly enough. And what we learned from one axis is that we can't pile in a bunch of breaking changes. So the stakes were really high. Deferring Glimmer 2 for an extra release was disappointing, but it was the right decision. We could always make the next train. We needed to find out if we could do this. Once we made the decision, the implementation was straightforward. Crank those gears. After 2.9, we burned down the rest of the issues and we released Glimmer 2 with Ember 2.10. Glimmer 2 was in a stable version of Ember. Again, once we made the decision to land the feature in the stable version of Ember, we cranked the gears. And I think it's really important to take a step back and think about this. Because we landed Glimmer 2 a complete rewrite of our entire rendering engine behind a feature flag, we were able to make fine-grained decisions in real time about whether or not we should include it in any particular release. This allowed us to keep moving. It allowed us to work on other features. It allowed us to reduce disruption. It really worked. Once it landed in 2.10, people were psyched. They saw massive reductions in bundle sizes. And these tweets actually understate how important it was. The old templates were compiled JavaScript. The new templates were JSON. Not only were app bundles half the size, but all of the JavaScript parsing time for all of the old templates is gone. It's a huge win. And I knew that something like this would happen intellectually. But we couldn't actually tell without trying it in real apps. And the fact that it was drop-in meant that tons and tons of real apps could use it. And when they used it, they saw huge wins. When you let yourself get seduced by the dark side and given to temptation to rip the band-aid off, sure, you might be able to land something like Glimmer 2, but nobody will be able to use it. By following the process that we established by cranking the gears, we were able to land Glimmer 2 in a way that everyone could use. We started out by saying that our goal was to let people ship consistently over time without disruption, without rewrites. And we just faced our most serious challenge. After failing with Glimmer 1, our process had allowed us to swap the rendering engine in place, compatibly, without disruption. And all Ember users were able to use it. Glimmer has landed. We had learned how to make changes as radical as Glimmer 2 without disruption. What that meant is that we were gonna be able to do it again. We had a process for making huge, huge changes to the framework without breaking changes while bringing people along. And then we got to 2017 survey results and there was good news. By staying the course and giving people a good landing place in 2.x, people finally did migrate off of 1.13. People who were stuck on 1.12 for performance reasons also got off of 1.13. And here's the best part. We don't see a repetition of the 1.13 problem in 2.9. Another huge win? The LTS process was working. People were clustered around LTSs. Our improvements were really making a difference. At that year's EmberConf, we reflected. I just want to say bluntly, from now on, the process that we use for engines, for fastboot, for Glimmer, is how we're doing it from now on. We're not gonna lean on breaking changes. We're not gonna lean on big upfront design. We're gonna instead lean on small primitives and compatibility as our main mantra for how we do new feature design. Eventually it was becoming clear that we needed to do another major version. Honestly, the biggest impetus is that IE9 didn't have weak maps and we really wanted weak maps. Reflecting on the 1.13 release, we realized that it affected the master branch a lot. You're not supposed to care about releases in the channel model, so something went wrong. We couldn't lean on experience from browsers and Rust here because browsers and Rust don't have breaking changes. So there was nothing to copy directly. But the release channel system is not that complicated. It's based on a small number of axioms. And so once you're bought in and you really believe in it, you can work out what you gotta do. Just like feature flags let us take a piece of work that takes longer than six weeks and stage it and then present it to the user all at once. Deprecations let us take a removal, stage it and present it to the user all at once. So deprecations are kind of like reverse feature flags. For 3.0, we decided that we didn't wanna introduce any deprecations at the last minute. So we said, there's gonna be a deadline for deprecations. No new deprecations after the deadline. But wait, did I just say deadline? Did I just say code freeze? Actually no. We can add new deprecations whenever we want after the quote unquote deadline, but we just can't target 3.0. So if you wanna add a deprecation, you add it, it's totally fine. You're not stopped from adding a deprecation just because the deadline passed. But the number you put next to it is 4.0, not 3.0. It felt pretty different this time. When we were doing 2.0, we were kind of making it up as we went along. It's not really that surprising that things didn't work as well as we hoped. But when we were working on 3.0, all of our process gave us direction. Almost every problem that we had had a pretty clear answer in our process. So we were pretty confident that it would work. And this adds another change to our process. I wanna call something out here. This sentence only really makes sense on top of everything else that we did so far. We started out by saying that what we wanted to do was prioritize continuous shipping and minimizing disruption. And the thing is, this is what it looks like. It looks like focusing on this stuff. And it's worth pointing out that all of these changes were in service of shipping things. They were in service of landing things. They didn't slow us down. They didn't make it hard for us to land new stuff. They helped us land stuff faster. They gave us a rule book to follow whenever we wanted to land something. And most of the time, almost all the time, we can find a way of doing it faster than a major release faster than a breaking change. Now that we've fleshed out so much of the system, we had a realization. The key tactic of release channels is staging. And by staging, I mean taking work that's longer than six weeks and still letting it happen on master. That's a pretty big deal. It lets all dev work happen in one place, but still reliably ship every six weeks. So what's a staging problem? A staging problem is a part of the process that forces work that could be done incrementally to become atomic. In this case, the mistake was thinking that feature should only become stable when they are also ready to be recommended. But fundamentally, a feature is ready to become stable when the team is ready to make semverg commitments. By saying that feature shouldn't land on stable until we're ready to recommend them, we're blocking features from landing on stable that are ready for semverg commitments. One major consequence of this is that add-on authors who could take advantage of the feature becoming stable to start doing migration work have to wait until we're ready to recommend a feature to the whole community before they can start. If you think about it, that's kind of a deadlock. Another major consequence is that it prevents people from using an in-progress but stable feature even if they're willing to accept the confusion and churn. So we normally solve this sort of problem with release channels. A release channel clearly communicates what level of risk the user is taking. We have a similar problem here. Most people don't want to have to deal with features that aren't ready yet. They might want docs. They might want migration tools. They might care about other features that are part of the same feature suite. But that's not the same thing as Canary. So what's the solution to the problem? Basically a new channel. This lets us incrementally stabilize features and have a separate mechanism for letting the ecosystem know when it's ready to use. Now, if a way to stage stable work sounds like a contradiction in terms, I think that's because we're assuming that stability is definitionally the end of a feature's development process. But as we've been saying all along, stability and semver are not the whole story. Thinking of stability as the end of a feature's development process short changes all the other things that go into making a feature ready to use. So we have a solution to the coherence problem, additions. Additions solve the coherence problem by uncoupling stability in the semver sense from recommended. We finally have a way of describing everything that goes into our evolution strategy that goes beyond semver. Now that Ember and Russ did our first addition, we know that they're a big deal. But when we first announced them and looking at this list, it's hard to put your finger on it. And again, I think the deal is people are so used to thinking about everything in terms of stability and semver that it's really hard to think about the fact that things like a coordinated change to idioms or a product wide refresh or a marketing event should matter. So the dominant reaction that we got and the dominant reaction that Russ got was what's the point of this? For me, the key takeaway is that there's so much about what it takes to build an ecosystem that can support continuous shipping that can migrate, that can evolve, that has nothing to do with semver at all. But those things matter. They're the difference between applications that can start on Ember 1.0 and land on Ember 3.25 using TypeScript and applications that have to rewrite themselves multiple times over the years to accommodate ecosystem change. It's exactly the set of things that aren't captured by semver that make all the difference. And in my mind, that's really the lesson of all of this. By the way, we ultimately realized that additions are unnecessarily coarse-grained. The problem we're trying to solve is that we needed a way to take a feature and stabilize pieces of it incrementally. We want add-ons to be able to start taking advantage of stability and we want users who really, really need the feature to be able to use it as soon as we can commit to stability. But we also wanted to find an endpoint for a feature that's more polished than what we normally require for landing it in the first place. The thing that's great about the recommended stage is that it gives us a clear definition of readiness that can be determined by the framework team and therefore can be understood by users. It lets us say features land on stable when we're ready to commit to semver and then we have quality control requirements that are necessary before a feature goes into the recommended stage. So let's make one final tweak to our evolution strategy. Instead of additions, the solution to the coherence problem is the recommended stage. So what's the whole picture now? First of all, we have stability stages. A feature starts out as an idea and becomes an RFC. When it's accepted by the team, it can be implemented behind a feature flag. When the team decides that they're ready to make a semver commitment to it, they flip the flag and it makes it into the beta channel. After a six week shakedown cruise, it makes it into the release channel and then we call it stable. Stable means the feature has semver guarantees. Then we have a readiness stage. This has nothing to do with semantic versioning. This has to do with readiness. A feature makes it into the recommended stage when the team believes that it meets the quality control requirements. And from this perspective, in addition is just a collection of features that have made it into the recommended stage. LTS is not really a stage at all. LTS is there to help apps and the ecosystem synchronize cadence. Deprecated features have to be replaced with a documented migration path to a recommended feature. When we first added this policy, we wanted to make sure that deprecated features could be replaced with something that was ready to use. But we had no way of describing that with the recommended stage. It's really easy to explain. A deprecated feature can only be replaced with a documented migration path to a recommended feature. And finally, we prefer a primitive first approach to new feature development wherever possible. As a bonus, now that we have the recommended stage, we can crisply define the pit of incoherence. The pit of incoherence is that point in time last year that motivated additions. From this perspective, the pit of incoherence is just the time between stable and recommended. The long and short of it is that staging is now the core way that we understand feature evolution. And there are two new RFCs, RFC 617 for adding features and 649 for deprecating features that describe the staging process in detail. And our toolkit is now filled out. After years of using it, I think we can safely say that it works. Now, I'm pretty sure we're not done here, but the system works. It works really well. I'm glad we stuck it out long enough to figure it out.