Every frontend rewrite I've seen starts the same way: "the app is slow and hard to change, let's move to X." A year later the app is slow and hard to change in X.
The framework was never the problem. The same few habits show up in every codebase I join, whatever it's written in, and they move house with you.
Rewriting instead of measuring
There's always a new framework. Faster benchmarks, smaller bundle, better DX, a very confident launch post. And some of them are genuinely good! But a rewrite doesn't fix a team that doesn't understand why the old app was slow. It just resets the clock on the same mistakes, and costs you a year of features while it does.
So before switching tools, find out what's actually slow. Open the Performance panel with the CPU throttled to a mid-range phone and record a page load. Every time I've done this, the culprit wasn't the rendering library. It was four analytics scripts, an unoptimised hero image, and a waterfall of API calls nobody ever drew on a diagram. All of those follow you to the new framework.
Syncing state instead of deriving it
Here's a search box I've met in every #react codebase I've worked on, in some form. Each effect sets state that triggers the next:

Type a letter: React renders with the new query, runs the effects, renders again with the new list and the old count, runs the next effect, renders a third time. Nobody can say which state is the real one, because they're all copies.
Two of those values are computed (count is just filtered.length),
and the third is a reaction to the user typing, so it belongs in the
handler that handles the typing:

An effect is for keeping React in step with something outside it: a
subscription, a socket, a third-party widget. If both ends of the sync
are inside your component, it isn't an effect, it's a calculation or
an event. The React team wrote a whole page about this,
You Might Not Need an Effect,
and most codebases stopped reading at "might." Turn on the React hooks
set-state-in-effect
lint and let it find them for you.
Modelling states as a pile of booleans
Every fetch starts with one flag, and then it grows:

The bug report is always about one of the other four: a spinner on top
of an error message, because a catch forgot setIsLoading(false).
Name the states that actually exist, and the impossible ones can't be
written down:
user now only exists when there is one, and a switch over status
can be checked for exhaustiveness.
Keeping server data in a client store
The data came from the API, so someone copies it into the global store. Then come the loading flags (see above), a refetch when the tab regains focus, and an invalidation scheme that's mostly right. That's a cache, written by hand, and it's the worst-tested code in the app.
Server data has an owner, and it isn't the browser. Hand it to a server-state cache like #tanstack Query, or your framework's data loaders:
Keep the global store for state the client genuinely owns, like which panel is open. In most apps that turns out to be very little.
Abstracting on the second copy
Two screens have a similar card, so it becomes <Card>. The third
screen is slightly different, so it gets a prop. A year later:
Every caller now pays for every other caller's edge case, and nobody dares touch it. Sandi Metz put it best: "duplication is far cheaper than the wrong abstraction." Wait for the third copy, and when you do extract, give callers small pieces to compose instead of one component with a flag for every difference.
Building buttons out of divs
It works with a mouse, so it ships. It can't be reached with Tab,
doesn't fire on Enter or Space, and a screen reader announces it as
plain text. Patching the div means a role, a tabIndex and a key
handler, and you're still missing disabled and form submission.
<button> has had all of that since before any of us started. If it
does something, it's a button; if it goes somewhere, it's a link.
Shipping an app to show a document
A page with a heading, three paragraphs and a signup form. It ships half a megabyte of JavaScript (about the median for a mobile page) so it can hydrate a heading, three paragraphs and a signup form. On a mid-range phone on a train, that's seconds of a page that looks ready but ignores taps, and INP is the metric that catches it.
"But we server-render" doesn't save you. A server-rendered page still downloads and hydrates every component on it; the HTML just shows up sooner. What removes the JavaScript is not sending code for the parts that never move. React calls that Server Components, other stacks call it islands, and a plain HTML page with one script tag has done it since the nineties. Every version fails the same way: someone marks the whole page interactive because the form needed a submit handler, and you're back to shipping the paragraphs.
Most of the web is documents. Ship HTML, and make every bit of client JavaScript justify itself. (On #nextjs? I wrote a cheat sheet for picking a strategy per route.)
Read the list again and it's one habit in different clothes: adding something (a framework, an effect, a flag, a store, a prop, a click handler, a bundle) instead of understanding what's already there. The fix is usually a deletion, and it works in whatever you're running.