Rewriting older React-SPA to fast SSR: Comparing SvelteKit and NextJS
Table of contents
- Arguments for using NextJS
- Arguments for using SvelteKit
- Import headers and cookies to read the current request
This is part two of my blog posts on rewriting an older React-app into an SSR framework to make it feel a lot faster for the user, while also increasing readability and DevEX. For details about the existing application, and the need for a rewrite, see [Part one](https://blog.runar.dev/upcoming-comparing-sveltekit-vs-nextjs-by-rewring-lagacy-app)
TLDR; SvelteKit is very nice to work with, and wins in just about every category I could think of.
Most importantly, the code created is smaller, more intuitive, and easier to read and reason about.
I've now created a PoC for the application, one with NextJS, and one with SvelteKit, and will compare my experience with both of them.
When I initially started planning this, I was under the belief that NextJS would in many fields be better than SvelteKit, but that SvelteKit would in the end come out ahead, since it is a bit nicer to work with among other this. However, the gap between them is a lot bigger than I had anticipated, in SvelteKit's favor.
SvelteKit has done some amazing work here in creating an intuitive framework, and also with Svelte itself.
Working with NextJS after having worked with SvelteKit seems frustrating. It's like going back in time, to a time when React's hooks are everywhere.
At this time, I simply cannot recommend using NextJS, unless you are highly dependent on using React.
Both SvelteKit and NextJS support SSR and SSG with hydration on the client.
Routing, by file tree
They have a very similar project structure where folders and conventionally named files create the routes available automatically, and each route is automatically code split. SvelteKit has drawn a lot of inspiration from NextJS/Remix here, so they are very similar.
Arguments for using NextJS
By using NextJS, you can continue to use your existing React components and therefore do not need to rewrite them.
I'm not sold on this one. If you have a legacy web application, you will still need to rewrite a whole lot of it just to adhere to the strict nature of how NextJS forces a greater separation of Server-components and Client-components, each with [their own set of rules](https://nextjs.org/docs/getting-started/react-essentials#when-to-use-server-and-client-components), of which you cannot easily combine (you can use server-components inside client-components, but you need to send them in as props).
I think on should consider doing a forced rewrite, to ensure one does not only copy existing code and thereby continueing to use legacy-solutions that may or may not fit into the new application.
No need to learn a new framework, (if you know React)
Learning Svelte is for the same reason, easier to learn than React.
Suspense. It makes it a lot easier to control nested streaming components and loading-states. It works quite well and unlocks a few new features that Svelte does not support at this time. Svelte feels also a bit weird when it automatically awaits promises returned in load-functions at the first level, but at nested levels, it treats them as promises (this is by design, but I find it weird).
NextJS has a very nice built-in component for rendering and generating images for various sizes and formats, which picks the best one for each client. Although available for SvelteKit too, it requires a dependency.
I am sure there are more reasons to use NextJS, I am just having trouble finding them. I must admit, I do not have much experience with NextJS (but I have a solid experience with React), so I am sure there are some niceties about NextJS too.
Arguments for using SvelteKit
HTML, JS and web standards
Svelte and SvelteKit are easier to learn since you interact a lot less with svelte-specific code, but mostly write standard HTML and JS.
Have you ever tried pasting HTML into your TSX file, only to have the compiler scream at you? Svete did not rename the html-props like
tabindex or made
style into an object, so almost all HTML that you intend to paste in a svelte-file will work with no changes. Admittedly, they did rename event-handlers like
on:click, but typically most HTML that you want to paste in does not have many event-handlers, they usually are just some standard html-tags, with static properties like
Automatic reactivity of variables
Using simple assignments (e.g.
count = count++ to a variable ensures that every reference to that variable is updated on the page, and the whole component is not rerendered, only that variable. Not only is this better for performance, but more importantly, it is intuitive to use. No need to use a React-hook like
useState(which cannot be used in server-components), just simple, intuitive JS.
Since Svelte is compiled, there is no big "svelte-framework" included in the final bundle, only the exact parts of Svelte that you are indeed using on that exact page.
I am amazed by SvelteKit's autotyping. It works so well. No need to add typing-information everywhere, or map types between server-load-functions and render-components. SvelteKit automatically does this in the backround, so that any object that you return from your load-function, you get that type driectly onto your
data in +page.svelte-files. This is a huge timesaver for the developer.
No unused CSS
Styling uses regular CSS by default, or SCSS/LESS by simply setting the lang-property of the style-tag. This is very intuitive. What I like here is that Svelte will detect and complain if I have an unused CSS-class or selector. This is great when one is changing code, and one can be confident that the accompanying CSS can be removed without negative effects.
Keeping the style within the same file is a blessing in this way, as it keeps the file structure clean, removes the need for an import, and again, ensures that there is no unused CSS.
Things I found annoying when using NextJS
Some of these are just nitpicking, and I do not mean to bash NextJS at all. I am sure I would have liked NextJS a lot more if it hadn't been for Svelte, which in so many places just shows how much easier and intuitive things can be.
So even though these are nitpicks, when seen together, frustration starts to build.
Documentation, and app-routers
As of August 2023, NextJS is currently transitioning from an older Paging-router into an App-Router, as they call them. They are trying to clean up the documentation by having a toggle at the top of the page to see only the relevant documentation, the documentation is not complete yet, and there are several inconsistencies. This makes it hard to pick up the framework at this time unless I go for the older Paging-router. I am sure this will improve rapidly.
The documentation is a lot bigger than SvelteKits, yet I find SvelteKits documentation to include almost every feature of NextJS, and SvelteKits documentation is clearer.
Simply just finding which type I should use for a Page-component was difficult to figure out.
How do I get the request-url?
With the new AppRouter, it looks to me like it is not really possible to get the request-url in a server-component. If googling, you are constantly met with solutions for the older page-router, but they don't work, and the newer API that you are supposed to migrate to, is not equal.
I eventually moved the logic into one Middleware and one action. With SvelteKit, I could just do this in a top-level layout-file (or use an express-like-hook).
Import headers and cookies to read the current request
This just looks weird. To access the request-headers or cookies, you import the module and it is just magically linked to the current request. Usage after that is great, but I found SvelteKits use of simply just having it available as the first argument to your load-functions very intuitive and mimics a bit how it is like in any server-handler-func, like in Go, Express, Python, C# or similar.
Internal stack traces
This may just be a fluke, but on at least two occasions I get an error during development. These were just typos on my behalf, but the errors produces were just cryptic and the stack traces only referred to internal next-js-files, and no references to any of my own files. Although it was easy to fix at these times, since I knew where the error was without the help of stack traces, it made me a bit nervous about the thought of debugging something like this in a larger codebase.
I needed a way to have a global cache, which I implemented simply as a basic
Map, with an expiry date. There is [an open issue here](https://github.com/vercel/next.js/discussions/15054) regarding it.
This ate a good hour of my time and a whole lot more patience.
I managed to solve it partially by using the same method as described [by Prisma](https://www.prisma.io/docs/guides/other/troubleshooting-orm/help-articles/nextjs-prisma-client-dev-practices).
Link components for glorified
I guess this comes from legacy-code, and react-router, where each internal link needs to use a
Link-component instead of
a. Normal in React, but when seeing Svelte just automatically working with
a out of the box, it seems that NextJS could have used the opportunity while creating a new Router to fix this annoyance.
Forced splitting of components
As mentioned a bit earlier, there are a lot more stricter rules for what kind of code can be performed in a server-component vs a client-component. server-components cannot have hooks (even simple ones, like getting the current URL, or initializing a useHook-veriable). If you want to use a server-component within a client-component, you need to pass it as a prop. So you split your code into subscomponents a whole lot more, even compared to normal React.
The code in React is a bit larger than compared to Svelte, so it feels like there is a need to split components into subcomponents a lot sooner. I find the readability to be worse with React since there are a lot more prop-drilling and more issuing of hooks and especially