We Rewrote our React App in Svelte in Three Weeks
My wife and I have been working on Fablehenge—a writing app for novelists—for several years. It had been a free time project where we’d push hard over a few weekends, then let it sit idle for a couple months perhaps tinkering in the evenings when we had a bit of time.
At the beginning of this year, I decided to take a sabbatical, which was supposed to involve cutting new hiking trails and woodworking. While it has done that, I found I’ve also been working on Fablehenge (more or less) full time.
Fablehenge was written in React. Jen and I are both extremely comfortable with React; I’ve been using it off and on since before it was open sourced and she has been using it exclusively for many years.
I spent a good portion of January struggling to get a somewhat (but not terribly) complicated drag and drop system working. Jen spent another three weeks in February tweaking and improving the usability of my implementation.
Our code was built on dndkit, which, like every react drag and drop library I could find (I spent hours on the search), hasn’t seen a lot of recent maintenance activity. I liked the library, but after the six or so weeks we had collectively spent tweaking it, we discovered that our app had performance issues that we just couldn’t solve.
At some point during my search for a solution, I stumbled upon the svelte-dnd-action library. At the time, it had new commits as recently as 18 hours ago! Oh my god, the jealousy I felt!
I’d heard a lot about the love for Svelte, but I assumed it was more hype than substance; after all, the Javascript community is well known for its “ooooh, shiny” tendencies.
But I needed a break from my React frustrations, so I spent a day studying the
Svelte tutorial and setting up some toy apps. I tested svelte-dnd-action
,
and I was thoroughly sold on it. I sent Jen a link to the tutorial and a few hours
later, she said, “I don’t think I can go back to React after this.”
So “just for fun,” I started porting several years worth of work to Svelte. It was almost too easy. I kept expecting it to just suddenly blow up in my face. Throughout my career, I have encountered many-a-library that seemed wonderful on the surface, but ended up causing more pain than it solved in the long run. Abstractions always leak, eventually. But as I became more confident in my understanding of Svelte, I realized that I was using something very special.
I can’t remember when or where I heard that a new technology has to be 10x better than the incumbent to overcome the lethargy required for a user community to transition. When React came out, it was obviously more than a 10x better developer experience than the primary competitors (jQuery and Angular, which was not the same as Angular today) at the time. The whole community got on board seemingly overnight.
I believed the hype about Svelte a little bit, but I assumed that it wouldn’t be the 10x improvement that could shift the community. Now I think differently.
Faster developer velocity
I’d estimate we had about a year’s worth of accumulated person hours invested in the React app. Our codebase was well-written and maintained, and it’s not so big that there were any unknowns we were worried about. So rewriting it from scratch was always going to take less time than the initial effort. We knew where the complexity was, and we focused on solving that first. We had also structured the app such that we could copy-paste about twenty percent of the code (related to querying and mutating data) with only minor modifications.
It took three weeks to rewrite it. I don’t mean “largely rewrite it”. I mean “completely rewrite it”. The initial commit was made on February 26th. As of March 15, the Svelte app was deployed and in production. We’re already working on new features, and they’re going blazingly fast.
Remember, three weeks is the amount of time that Jen and I each spent
struggling with the drag and drop system in React. Because this was our primary
problem at the time, it was the first thing we tackled in Svelte. I had it
working in its entirety in less than one day. I did find one bug/missing
feature in svelte-dnd-action
that the maintainer was able to fix within two
days of me posting a reliable repro.
If every six weeks of React development could map to one day of Svelte development, we’ll earn back the time it took us to rewrite the app in half a day. Admittedly, that’s probably a little (itsy-bitsy) too much to hope for! But our Svelte development is reliably and consistently more than three times faster than React development ever was.
Less code
Though it is better than many predecessors, React requires quite a lot of boilerplate (even if you don’t use Redux, which we knew to avoid). We’ve been using React for long enough that we were kind of blind to the boilerplate; you just write React code and your eyes gloss over the bits that are the same for every component.
Our Svelte app is implemented with 60% of the code that our React app required. I want to reiterate what I said above: Our React app was well-written and didn’t have any redundant code or unused features. We ported everything. We did redesign a couple of features on the fly, but only because it was easy. If anything, I suspect that redesign resulted in more svelte code rather than less.
I should acknowledge that part of the reason there is so much less code is that we are now using tailwind CSS where we were previously using Chakra, and one thing tailwind is good at is reducing SLOC counts. More on tailwind in a bit (spoiler alert: we don’t love it).
More enjoyable development
Fablehenge is a hobby project. Our top priority is that we love working on it (number two is that authors love using it). We thought we enjoyed React, but the simple truth is that Jen and I have spent many a lunch hour casually griping about various aspects of the React ecosystem. It’s not that React is bad; it’s wonderful! It’s just not as much fun to work with as it could be.
Svelte is way more fun to code than React, though I think it’s going to become even more fun when Svelte 5 is released (real soon now™). Enjoying coding is extremely subjective, of course, but Svelte matches whatever definitions we have for it.
We are a two person team with a project we can fit in our heads. So I can’t fully speak to whether Svelte will scale to a company with thousands of developers, and millions of lines of code the way React was designed to do. My instinct is that it will. Svelte maintains the same component model and compartmentalized styles that React encourages. However in the interest of efficiency, it gives the developer access to powerful features that could become hard to maintain if abused.
Most notably, Svelte emphasizes one way binding, but allows two-way binding when it makes sense (usually with form elements). Unfortunately, there’s no hard rules defining “makes sense”, and I can certainly imagine interns at a large corporation thinking two way binding is simpler when it’s actually going to cause a maintenance nightmare down the road. But any senior developer will tell you that you need to spend more time reviewing an intern’s code than you would have spent writing the code yourself, so I wouldn’t expect these sorts of bad habits to actually merge, right? 😉
In general, though, the less code a project has, the easier it is to maintain. This is why higher level languages and frameworks exist. So if Svelte is reliably 30-40% less code to maintain, I expect it to scale well.
Fast to learn
Even though it is practically a new programming language, Svelte leans so heavily on Javascript, HTML, and CSS, that if you know web development, you practically know Svelte already. There are some oddities that you have to learn, especially around reactivity, but we were amazed at how quickly we were productive in Svelte after spending a few hours reading the tutorial.
It just makes sense, immediately. Svelte really feels like it leverages standard technologies, rather than reimplementing them poorly like so many frameworks do.
In fact, it was so easy to learn, that I often felt like I was remembering how to do Svelte, rather than learning it afresh. “Oh! This is what web development is supposed to be like. I forgot!”
Test suite
Another reason this rewrite was so fast and easy for us is that we have a very comprehensive test suite as well as a spreadsheet of every single feature in the app and how to manually test it. I cannot overstate the importance of these artifacts, and I have never worked for a “real” company that had them.
Our test suite is written entirely in Cypress, mostly as e2e tests. We have a few unit-style tests, but even those are implemented in the Cypress runner (mostly because I didn’t feel like setting up extra CI for testing-library).
Aside: We aren’t in love with Cypress, but at the time we chose it (a couple
years ago), it was the only one that worked reliably with contenteditable
elements. As a novel-writing platform, we have a lot of contenteditable
s!
It’s worked well enough that we haven’t recently experimented with any
alternatives.
The Cypress tests were not a drop-in replacement in our Svelte app, mostly
because we didn’t always port the data-cy
attributes over, but also because
some selectors that made sense in the React app were not directly applicable in
the Svelte app. But only light editing was required, and we have plenty of
unfortunate experience debugging Cypress flake, so it didn’t take us long to
migrate them.
I should note one thing that we don’t like here: on the Svelte app, we had to
litter a lot more cy.wait
calls through our tests. As far as I can tell,
Svelte’s reactivity model is such that just because one element on a page has
updated content does not mean that all other elements that rely on that updated
content has received the new values yet.
So for example in our outline tests, adding a new chapter or scene might show
up in the outline itself, but the toolbar button that manipulates the selected
scene doesn’t know that this item is selected yet. So we throw a cy.wait(50)
in there and grind our teeth. I am holding out hope that svelte 5 will address
some of this, or we might have to add some extra data-
attributes to convey
whatever data is commonly reacted to in clickable elements.
It could definitely be better, but the important takeaway is something that I really hope you already know: If you are porting or rewriting a project, you must have a super comprehensive test suite on the original project that is easy to port to the new version. That’s the only way to know when you’re finished.
Single Page App
Our React app was a single page app. Sveltekit does an incredible job of supporting server-side rendering, prerendering (static sites), and client side rendering. It can even support mixing these paradigms for different parts of the site.
So we built our Svelte app using server-side rendering to begin with, because I assumed that was the most supported/recommended method for Sveltekit. However, because of the way our app is structured we weren’t seeing much benefit from SSR. We don’t make any direct network requests and store everything in IndexedDB, leaving all the heavy lifting to the excellent Dexie Cloud service.
So I felt that the overhead of maintaining and scaling a separate production node service to run Sveltekit’s SSR features was not helpful compared to the simplicity of deploying a static site at the edge. We didn’t notice a performance difference between SSR and the SPA while using Svelte, though both were far far far more performant than the React app.
For transparency, though, I never have been very impressed with the idea of server-side rendering, so it didn’t take much to push me to this decision. Moving compute from the client to the server doesn’t seem like it’s going to gain much in most cases, especially since browsers are extremely well optimized for rendering HTML and running Javascript.
Digression: I recently watched a talk where the developer thought he had a
“brilliant” idea of rendering stuff in an onMount
handler to speed up server
side rendering by pushing compute to the client and I literally face-palmed.
Server side rendering does have its uses, most notably when you have highly dynamic or user-generated data that you also want to be SEO optimized (e.g. an E-commerce site). However, for login-protected data that a search engine isn’t going to see, I don’t think there is a lot of benefit to SSR. It’s optimizing the wrong thing.
Since Svelte seems to be lightning fast regardless of what rendering mode you use, I’m not going to fuss about it too much. Especially considering that Svelte 5 promises to bring us even more performance; I’m guessing that Svelte 5 rendered client side will be fast enough for most situations, and I really do like the minimal devops overhead of serving static HTML and Javascript files.
A note on Svelte 5
Reactivity in Svelte takes a bit of getting used to, mostly because it is hard to turn off. I think I’d been working with it for a week before I felt comfortable that stuff wasn’t going to update when I didn’t expect it to. This is honestly a better problem than having stuff not update when you think it should, but in Svelte 4 the workarounds to turn off reactivity are a bit unintuitive.
Svelte 5 fixes all of this, or so I’m told. The runes and untrack
function
will make it easy to ensure reactivity happens exactly when and where you want
and not anywhere else. In addition, Svelte 5 will be a lot easier to learn as
it has fewer concepts and custom syntax than Svelte 4, but those concepts are
reused more effectively. Also, some of the new Svelte 5 syntax is more
reminiscent of React, so if you are migrating from React, Svelte 5 will be more
comfortable out of the box than Svelte 4.
If anyone is considering a React-Svelte rewrite, I might suggest waiting until Svelte 5 is widely available. It is available in pre-release right now, but in our case, we knew we wanted to ship a production app ASAP (we had no idea that ASAP would be “only three weeks”, though). We didn’t want to rely on alpha software. Especially since we were new to Svelte; it’s hard to learn a framework when the errors you encounter might equally be because you misunderstood the framework or because the framework is broken.
The other reason we didn’t want to start with Svelte 5 was that the library ecosystem hasn’t been migrated yet. Svelte 5 is supposed to be fully compatible with Svelte 4, but with an expectation that you’ll be migrating to new paradigms over time. Even if the Svelte 4 libraries that currently exist worked perfectly well with Svelte 5, I am more interested in the libraries that haven’t been invented yet that will be designed from the ground up (or redesigned from their current incarnation) to leverage Svelte 5’s goodness.
That thing we don’t like about Svelte
It is a hard truth that every library choice involves trade-offs. Svelte is amazing at almost everything. But one area that has been dissatisfying is styling child components.
Svelte has some great features for working with styles on a single component in an isolated way. However, if you want to add styles to a third-party component (e.g. from a component library), you have to resort to a hack of some sort, either escaping to global styles or relying on the library to accept class or prop strings of some sort.
As a result, every component library we’ve found for Svelte is a pain in the ass to style (a possible exception is melt-ui). Most of them rely on tailwind-css and passing classes down to the component, but you have to know what classes the component uses to understand what you are modifying. Worse, if the component is complex, you have to pass different class names depending on which child component is being styled (depending on the framework), so you have to really understand library internals to style it.
We started with flowbite-svelte. It’s an amazing library, and we were able to get from zero to eighty percent in a huge hurry. But the last 20% (in this case, styling) has been… it’s not painful by any means, but it’s not pleasant either. Maybe we don’t hate it, but we don’t love it, and we want to love our app.
Tailwind is sometimes referred to as “polarizing”. We didn’t mind it at first, but have grown to dislike it, so I guess we’re on the negative pole. That’s fine, but after much research, we decided that the only reasonable way to style child components in Svelte is to use Tailwind, or something similar.
The best alternative is to not use a component library, which basically means copy-pasting components into your design system and then styling them yourself. Even if we do that, though (probably using melt-ui), we would have to be thoughtful about which props or classes we pass into our components to ensure they fulfill design requirements.
Another alternative is to use global styles more liberally. That would probably work fine for our small app, but we all know it wouldn’t scale in the long run.
I think that Svelte 5’s Snippets feature will ameliorate this problem, but will not solve it completely. There are numerous issues in the Svelte issue tracker discussing this problem and potential solutions, so I’m hoping that once the Svelte team finishes shipping the massive Svelte 5 rewrite, they will have some time to focus on solving styling with the same elegance and simplicity that Svelte brings to everything else.
Should you do it?
Should you migrate from React to Svelte? I can’t answer that. I do think that Svelte will be a competitive advantage for us, as we can develop more quickly with fewer people. However, if your app is like most React apps I’ve worked on, you probably don’t have adequate tests, and there are dead or outdated code paths that nobody really understands. Porting that kind of stuff is hard. It’s good for the long term health of the project because the dead code paths get pruned and the misunderstood code paths become understood. But long-term health has numerous competing priorities such as new features, user demands, and general ops load. There’s no sense in thinking about maintenance three years from now if you only have 18 months of runway.
I doubt most production projects would be able to migrate as quickly as we did. Abandoning false modesty, we know we are extremely strong developers and as a result, the React app was in a state that we were very confident we could port.
That said, if your app is not in a state that it can be easily ported to something else at a moment’s notice (i.e. it doesn’t have sufficient tests, those tests aren’t e2e, APIs are ad hoc, it’s poorly documented or unmaintainable, it uses deprecated libraries or paradigms that your team never bothered to finish migrating, etc), then it is also not in a state where you can confidently add new features to it, no matter what you tell yourself. A rewrite in this situation will be painful, but it is also an opportunity to tidy your house.
Over my career, I’ve seen many rewrites that have failed and many others that have succeeded. Frontend rewrites tend to go better than backend rewrites because data migration is much simpler or not a problem at all. The successful ones come from a position where someone says, “We have simplified this app as much as possible, now we can rewrite it.” The failures tend to more along the lines of “This app is too complex, let’s rewrite it.”
I do think that Svelte’s advantages in performance, dev velocity, and dev satisfaction make it 10x better than React, and that it therefore has the potential to overtake the incumbent as the primary choice for new projects. I’m not saying React is going away (I still see job postings for jQuery, for god’s sake, even though HTML standards have effectively replaced it), but that Svelte could become more popular than React for new projects.
And yes, if you are starting a brand new project from scratch, I think you’ll be much happier if you use Svelte.
I’ve seen all those headlines about Svelte being the “most loved” framework, and… well I admit, I just ignored it as noise. But the next time that survey comes around, I’ll be right up there with them, waving from the Svelte bandwagon.