In January 2019 I sat in a planning meeting where a staff engineer proposed rewriting our front end. Again. The pitch was thirty slides about rendering performance and developer velocity, and it was a good pitch, because he had written the previous one in 2017 and the one before that in 2015. That is when it hit me that our team had a rewrite cadence, and the cadence was almost exactly two years.
So I went back through our own git history and priced it out. The jQuery to Backbone move in 2012 cost four months and lost a settings page that quietly disappeared and was not noticed for a year. The Backbone to Angular move in 2014 cost seven months, shipped zero features, and ended with two senior engineers leaving. The Angular to React move in 2016 cost nine months and produced an application our users described as “the same, but slower.” The pattern was consistent: the framework was always the most exciting thing we shipped that year, and it was the only thing we shipped that year.
What the rewrites actually cost

The visible cost is calendar time. The invisible costs are the ones that showed up in my accounting.
Every rewrite carries a translation tax. Features get ported at whatever fidelity the deadline allows, and the long tail of half-ported behavior becomes the bug backlog for the next two years. Support tickets from 2013 would surface in 2018 as regressions that nobody could explain because the code that used to handle them no longer existed.
The second cost is attention. A rewrite is a project that looks like engineering and feels like progress, so it absorbs the people who would otherwise be fixing the product. Meanwhile the roadmap holds still. The competition does not.
The third cost is trust. Teams that rewrite on a timer learn that anything they build might be deleted on schedule, so they stop building deep. Why would anyone invest in a careful internal design when the ground moves every two years?
What we do instead

We stopped rewriting and started migrating. The distinction matters operationally. A rewrite replaces the whole surface at once. A migration replaces one slice at a time, behind the same URL, with the old code still standing next to the new code until the slice proves itself.
In practice that means three habits. First, adapters at every boundary: whatever the system under us does, our code talks to it through one module we own, so swapping a piece does not require a campaign. Second, codemods over seasons of manual typing, so the boring 80% of a migration is a script and a review. Third, written exit criteria for libraries: when a dependency loses its maintainers or starts breaking minors, we replace that module. The application stays.
Tests are the thing that makes any of this possible. Code you can trust is code you can move. Code you cannot test is code you have to rewrite, which is exactly how the two-year cycle fed itself.
When a rewrite is justified

It happens. The honest triggers are platform-level: when the runtime you depend on is actually going away, like the Flash and Flex applications that had to move in the early 2010s, or when the browsers you must support have changed under you. A rewrite is also fair when the team can no longer hire anyone who wants to work in the stack, or when shipping a single change takes longer than the feature is worth. Those are structural facts, and structural facts beat preferences.
Style preferences are not structural facts. Neither is a conference talk, however good.
The quiet payoff
We are four years into the new policy. The front end contains code written in four different eras, and it ships weekly. The slices we migrated in 2021 are still fine. The slices we left alone are still fine. The engineers who arrived in 2022 were productive in their first month, because the codebase taught them itself instead of asking them to wait for a big bang.
Nobody has ever written a conference talk about the front end we did not rewrite. There is no screenshot of the nine months we got back, or the features that survived because nobody had to translate them. That is fine. The users could not see our framework before, and they cannot see its absence now. All they know is that the product kept getting better every month, which was the point of the whole business to begin with.