This article is an update to a previous article why you want to build a SPA which I think was laking some nuance and precision. Reading this article is not required to read this one. I'm writing this update because SPAs are very popular and I recently though more about them and realized my previous article was missing some important points. If you see some points that are still missing after reading this article or just want to share your opinion, don't hesitate to react in the comments!
Some note on the context: I am currently working professionally on a website that is not a SPA but I built big SPAs as side projects. I'd like to rely on my experience to explain the pain points I encounter with this project in the hope to convince you to consider building a SPA from scratch on you next project. I hope that with this article, whether you finally build a SPA or not will be based on relevant arguments for your team and project.
Since I work on a "hybrid" project with pages being generated server side and part of the page managed by jQuery or React, I experience many pain points due to this architecture. I guess it was the main motivation why I wrote the previous article: these points wouldn't exist if we had built a SPA. I'd also like to note that most of these decisions were made before I arrived in the company. Let's review these pain points:
- Interactions between jQuery or vanilla JS and React (I'll talk mostly of React here because that's what I use at work, but my examples should apply to any other "framework" ). That is a very big pain point. Some actions are triggered by jQuery by interactions made on the page. The event handlers are attached by jQuery to the DOM. This is mainly done to hide and show elements on click. The problem is: React can destroy any part of the DOM at any time when re-rendering a component, thus destroying the elements you attached an event handler on, therefore breaking interactivity. Some strategies were built to prevent this: mainly to hook on component lifecycle to readd the event handlers once React added the new elements in the DOM. It is impractical and a big source of bugs. Since React can handle events for us and can show/hide elements, there is no point to use jQuery for this. Just go with strait React, the result will be more simple and you will avoid lots of bugs that are hard to find and fix. Luckily, this practice died in our project, but I still insist on it because it was such a pain and I guess we aren't (weren't?) the only ones doing this.
- Big DOM manipulations in plain JS or jQuery. I don't really know why it's this way since we have React for some pages but sometimes we just create lots of HTML elements with jQuery to avoid a full reload of the page or to hide and display form elements based on very complex business rules. These pages don't even load React, all is done with jQuery. The code is bad and hiding/displaying elements based on events is a source of bugs. I guess it was made this way because it was perceive as easier and quicker. What's sad is that from what I understand the price for React was already paid when these pages were built:
- We already had pages (part of pages to be exact) in React so it's there and available (and a better fit for these problems).
- Team members had to learn React at this point and from my perspective it makes sense to pay the investment where/when it makes sense. In the long run, it will allow us to go faster with less bugs.
- The code is hard to understand and buggy.
- On some pages we do the initial render with Django and then React kicks in to re-render the page and add interaction. This feels like duplicated work that can be optimized.
- Interactions between different part of the page can be hard: we can have several small React apps on a page and sometime they must communicate or share common data (for instance: whether the user is logged in or not). Since they are independent and not part of a bigger app, communications is harder than it could be.
The question now is: is this enough to build a SPA? The ecosystem is now mature enough so we can have a way to create and reuse components, very good support for templating, updating components if some data changes and routing with any framework. By routing I mean that the URL in the URL will be updated as if it were rendered on the server (and not just a hash change) with history and back/forward buttons working as expected. Now we can also have Server Side Rendering (or SSR for short) to pre-render our app on the server and thus speed up initial page load as well as making our app easy to index by crawler. This will avoid us any SEO impact. So, it looks like everything would be better in the wonderful world of SPAs, but is it?
Why wouldn't we build a SPA?
Maybe instead of thinking at why we want to make a SPA, we can start by looking at why we wouldn't want to build one (the points below are not really in importance order):
- Habit or never built a SPA before. That's the weakest argument since SPA can definitely be a good thing. My advise would be: learn about SPAs and their advantages and downsides, learn how to build one and then try to build one in a test project to see how it goes. You will then have better insights on whether a SPA would be a good fit for your project.
- Lack of skills: a much better argument, in my opinion. Building a SPA requires much more JS and frontend skills than building what I'll call a "standard" or "normal" site. But if you avoid building a mess with jQuery, it will be easier for you to hire developers with the right skill set (I don't think they'll join you to maintain the jQuery mess) and to maintain and add feature to it in the long term. And these skills can be learned: from my experience, modern JS and its frameworks can be learned (more or less rapidly depending on which framework you choose and your experience with JS and they'll of course come with their own set of quirks). But it's definitely doable. If you team is lacking UX or design skill, try to rely a contractor to help you, since (from my perspective) those skills will be harder to learn for developers.
- SPA are complex to build and also require a mindset changes from how we do things in the backend. That's true, and if you are not used to building them, depending on the application your are building it may not be worth the trouble. So keep reading to learn more!
- SPAs hurt SEO: since the server will respond with a HTML file without content, crawlers can't parse anything interesting from the page. It's a valid concern that can be mitigated with Server Side Rendering (SSR) or pre-rendering. Read the sections about this below if you want to know more.
- Slow initial page load: same as above, the browser needs to load and parse JS before displaying anything. It may even require some extra HTTP requests to get required data before displaying anything. This can also be mitigated by SSR or pre-rendering, see below.
- Extra work: you must re-implement things that comes for free in the browser like a loading icon or error handling. Frameworks or libraries can help, but it sure is extra work.
- Bundle size: the browser will need to load more JS which can be an issue on low end devices (like some phones) or on poor connections (mostly on phones too). Depending on who you are targeting this may be a big issue. This can be mitigated by the fact that you can load only part of the JS on each page. Don't forget that marketing typically also includes lots of JS because of various trackers and advertisers. So "your" JS may not be that big (or big enough to be an actual issue) compared to all the other things you have to load (but it is more important for the page to render).
- Memory leaks: since your user will stay a very long time in you app, if you don't pay attention, some objects may never be garbage collected resulting in poor performance and high memory usage. But normally, if you follow the best practices of JS and your framework this shouldn't be an issues (like clearing timeouts and interval, unsubscribing to store events…).
- Security: how do you authenticate? How do you protect against XSS and CSRF? Those are hard questions and I think we find many misleading information on the internet. I'm no expert and encourage you to do your own research on this (it's also way outside the scope of this article). I'll just write some notes based on mine:
- Enable CSP (Content Security Policy). This will allow you to choose what your browser can do. For instance, it can be used to disallow execution of inline JS to prevent XSS attacks.
- If you use tokens (with JWT for instance), beware of how and where you store them. See this article for more details on this.
- User experience: it can be harder to achieve a good UX because we need to correctly re-implement things that our browser handles natively. And we can do it poorly. I'm thinking of GitHub here: once you loaded a page, you will navigate without a full page reload. Sometimes, it's slow and I seem stuck on a page and just by pressing F5, I can reach the page I'm trying to access immediately (you probably experienced this too). So I guess even for very good tech teams this can be hard.
- The technology is still evolving fast, how can we know it will stick around? A fair and complex question. If the list big names seems to have stabilized, the frameworks themselves are still evolving. I think the "big ones" (React, Vue.js, Angular…) will stick for a while because of the sheer number of projects using them. Let's also be honest: what says your backend tech will stay around or won't add breaking changes? It's used widely now but it can be replaced by something else (by a new shiny thing on Deno?).
- Overload the backend with too many requests: I guess it can happen if your app need to do a bazillion requests on each page. But I think it's more a design issue than a problem with SPA: why is it doing so many requests? Can we group some? Can tools like GraphQL help?
- Reinventing the wheel: For example you will need to find a way to restore the scroll position by any page change, add extra loaders to show that the contents are not yet ready, keep the page <title> in sync with your content and much more… It's free in the browser and it can take time to do all of these little features correctly.
- Improper status code: I don't mean error handling of an AJAX requests that failed, I mean that your server must always answer something and, a priori, cannot know whether a route exist. So it will always respond with 200 and the index file. This can be mitigated with SSR, see below.
- Accessibility: you can have issues but it's definitely getting better and your classic sites may lack important accessibility features too, let's be honest here. I think it's more whether you will take the time to add these features to your site than a problem with the tools themselves. See this article for more.
- Your application requirements don't need it. That's a traitorous argument I think because the requirements can evolve and you may end up needing much more interactivity than you initially though. In the end, it will highly depend on the project you're building and your trust in the requirements to continue not to need a lot of JS. Here are some:
- Your app is read only.
- Your are just dealing with basic forms, where HTML5 validation (or just a bit of JS validation) will be enough.
- Your app must work without JS.
Since SSR is trendy and can help mitigate some of the most blocking issues with SPA (namely initial load and SEO), here are a few notes SSR.
What is it and how does it work? As I said in the introduction, the point of SSR is to pre-render your app on the server.
If you enable SSR, your app will first run on the server thanks to NodeJS. Since the server ran the app, it can supply the browser with a complete HTML file that is ready for display. This means the page will display faster in the browser of the user, it will work correctly with search engines and environments that don't support JS. You should also be able to make the server correctly respond 404 for pages that don't exist (which is not easy to do normally since the app written in JS knows whether the route exists or not, the server that serves the base HTML file doesn't).
It comes at a cost though:
- Your app must be able to run in NodeJS. Depending on your host provider, architecture…, it may not be possible. One thing is sure, your architecture will be more complex if you go this way.
- Your application must be designed to run in the browser and in NodeJS (code that can do that is said to be isomorphic). But you cannot code as if you were only supporting the browser. For instance, you cannot access the DOM directly since it doesn't exist on Node, you must go through an abstraction which should be provided by your framework. I think making your app run correctly in the two environments will be one of the most challenging part when doing SSR.
- If your app takes time to start after the HTML is sent to the user, you will have to capture events (like button clicks) to replay them once your app has started. And if it takes really too long, I think you defeated the point of SSR and displaying fast an usable app to your users is worse than starting with a spinner that at least indicates the app is loading.
- Since the code will be run many times on the server, you need to pay attention to:
- Not use setTimeout since it can block rendering until the timeout has expired or use setInterval because intervals may pile on causing memory and performance issues (this can be mitigated if you don't forget to clean them correctly).
- Don't update prototype of global objects.
- Call required cleanup functions (like unbind, detach, unsubscribe in Aurelia) to clean the memory of the process and make sure page renders don't leak memory.
- The response time can increase if the server is busy and it will be harder to support high loads: without SSR, most of the work is done on the client side, the server that serves the app only has to supply static files, so the server will handle high loads better. If you have many or big API calls to populate the content of the page, it may be a problem in both cases though. But keep in mind it's another potential bottle neck.
So depending on what you do and what your stack is, it may not be possible, very hard or not worth the cost. But if you can do it, it can be a huge plus to avoid major pain points. Small disclaimer to conclude with this section: although SSR looks mature for all the major frameworks I have never used it in production yet, so I have no idea of what other issues you can encounter if you go down this path (I did test it though).
One alternative to SSR, is pre-rendering. It's almost the same except instead of rendering the page when we ask it, we pre-renders all the public pages of the website, save the result and serve the result directly when requested. So it should be faster and reduce server load since we mostly server static files on the first request.
I haven't tested this, but I can guess that if it is done properly, when you app boots, you can override part of it (let's say to display a username) just like you would have with a normal site. It may flicker a bit while you update the pre-rendered version though (but again, just like on a normal site). I also guess you can make sure only anonymous users get the pre-rendered pages to avoid this altogether.
I'll also note that this technique can work with SSR: you rely on SSR to pre-render your website and you then send this pages to your users.
So when would we?
Now that we've discussed in detail when not to build a SPA while giving nuance to each counter arguments to correctly understand whether or why it is valid, let's review some arguments in favor of building one (yes that's way shorter, but it doesn't mean the advantages aren't worth it):
- Your app needs a complex and interactive user interface or state that must be persisted across many pages. The result will be way more simple in the end if you build a SPA than some kind of hybrid stuff with a static page that will need to interact with JS. That's the main reason why you would build a SPA and still accept the drawbacks of this method.
- Your team is already so familiar with JS and SPA (and eventually SSR), that you know in the end it will just be quicker this way.
- You must expose a public API. You can then reuse it directly in your SPA avoiding duplicating some work to get the data on the site.
I'd also like to point out:
- Your life will be easier if you can update framework and build tools frequently and if you use supported tooling (like create-react-app). They evolve fast and bring enhancement frequently.
- Building a SPA is not required for a PWA. See for instance my article about PWA and Django where I transform a normal Django site into a PWA.
- You should uniformize conventions. This can be a function that convert snake case into camel case (and reciprocally) so you don't have a mixed conventions in your frontend (or in both your frontend and your backend).
- You can still handle style with CSS (or SCSS) files, you don't have to go down the CSS in JS road.
- SPA can handle translations and internationalization. The framework you choose should have libraries to help you do that.
How about hybrid?
You may already have a website and need to add interactivity to it. Should you rewrite everything? Probably not, it would take a lot of time and your site already works. If your pages are private and don't require SEO, maybe you can convert some pages to "SPA" (you won't get an app, just a page but it will be close of a SPA for the principles used).
If not, you can include interactivity to part of a page to build what I'll call an hybrid app. This is typically done with React or Vue.js because they have a much smaller footprint than the other frameworks. This is because they do less things and are only concerned with rendering. It also means you may need more extra libraries to do what you want than with frameworks like Angular or Aurelia.
Will the result be a monstrosity like most hybrids in fiction? Not necessarily. Here are some tips that can help you:
- Use CSS and vanilla JS if you can since this will be less complex. Just keep in mind this will only work for the most basic cases.
- For more advanced cases, you will need a framework like React or Vue.js. One important thing to remember, is to always use the framework for rendering and handling events: it can react to all events you'll need (click, form submit, hover…) and conditional hide/display something. Don't interfere with it, if you do, you will run into trouble and hard to correct bugs (as I pointed out earlier). Once you are in the framework, you use the framework.
- You can pre-render the content of the app in your backend with your backend templating language if you don't wish to display a spinner or if you wish to display content faster or to support SEO. Just make sure the app loads fast so everything works when your users try to interact with the page. Also note you will have to duplicate the display and probably part of the logic in the backend templates. Make sure it's worth it.
- Use JSON for communication with your backend if you can. It will be easier to read and debug when a problem arises.
- Display a loader and handle errors (eg display a message to the user) when things go wrong as you would in a SPA.
- Rely on your backend for security: don't try to use tokens, just go with cookies and session authentication just as you would if your were building a "classic" site. Don't disable CSRF protection!
- If many small independent JS applications need to communicate on the same page, you can create a global store to allow them to share the same source of data and interact with each other.
- If you are in this situation, your team is probably mostly backend or full stack developers. Don't hesitate to train them so they are efficient with the frontend tech you choose and to rely on a designer for design and UX.
- By all means, avoid jQuery in the JS apps. As I said earlier this can lead to issues when interacting with the framework.
- Please don't build a hybrid with jQuery, it's awful to work with and thus can hurt hiring and developers retention. jQuery can still be used if your needs for interactivity are low, in my opinion, but be ready to move away from it if your needs grow bigger and to rewrite part of your app.
If you are starting a new project and intend to build a hybrid instead of a SPA (or a classical site), please:
- Think again, you may need more interactivity than you initially though and a SPA may make things easier and more simple in the long run despite a slower start.
- Pay attention to the skills in your team and train/prototype if needed.
- Don't underestimate the work of maintaining two templates if you need SEO (and don't go for CSS in JS since you will need to duplicate the style in CSS for your main templates to render correctly). This of course only apply if you don't use pre-rendering.
- Whether to build a SPA highly depends on your team and requirements. There is no general answer.
- Please don't make a SPA only because it's popular or that's what trendy startup or big companies like Facebook or Google do. This can turn out to be a disaster for you. Make sure to understand the tradeoffs involved.
- On many occasions, a static website (ie a site only made of static pages that can be served directly by a web server) like this blog can be the best option. There are many generators out there to help you build one with plain HTML file or markup format like markdown. Jekyll, Pelican and Hugo are popular solutions for this.
- Don't use jQuery for complex use cases, we have better solutions now (mostly if you want to do big DOM manipulations).
- Don't try to make jQuery (or vanilla JS) interact with the framework. Rely on the framework all the way!
- Keep in mind that simple is different than easy: something hard now can turn out to be simple in the long term. So learning how to do SPAs and building one, although harder at first may be the right option in the long term.
- Whatever you do now, learn frontend dev (or backend dev if your are a frontend person), you will need these skills to at least help make the best decisions (like choosing when to do a SPA) and to interact with the other side of the spectrum.
- You probably will need more JS and interactions than you initially think.
To give a small conclusion about the project I work on: all in all, the fact that it's not a SPA is not a big deal. I have issues with some pages true, and I think we should use React more in some cases and rewrite some pages in React. But I still value Django and its ecosystem for the type of site I'm currently building. I think it gave us ease and speed to develop the site. So I think for us, a hybrid is a good choice: it gives us the possibility to add JS interactions where we need while preserving SEO. I just wished we went for React on some pages that requires lots of interactivity instead of jQuery. Since we don't need much interaction on most pages, all in all this works well. And we do have a SPA for some back office stuff where it makes sense.
If you have a remark, a question or just disagree, please leave a comment below.
Small frameworks list
Here's a small list of frameworks (at time of writing in June 2020) that you may want to look into to build a SPA:
- ReactJS: Most likely the most popular choice right now. While it was initially design to be included in a "static" page to add some dynamism to it, you can build full SPAs with it and even mobile apps. React itself is quite small so you will probably need to add some other libraries to extend its capabilities a bit. React can be added incrementally to you site to add dynamic parts step by step.
- Vue.js: Another very popular option. Very similar to React in principles (small, incremental) with a different approach that you may prefer coming from a "traditional web development" background.
- Angular: While the first two are relatively small and can be added incrementally, Angular is a big framework designed to build full SPAs. I don't think you will be able to easily enhance an existing static website with it. While its size may be daunting, it is complete and you probably won't need much additional libraries with it. Furthermore, its tooling is mature and able to reduce the size of your code to only include the code you actually use to keep the JS you send small. Given its approach and use of TypeScript, it might be the right choice for you if you come from the Java or .NET world.
Some interesting resources I read to help me improve my opinion on SPAs:
|||You may say React and Vue are more libraries than frameworks. I agree, but I'll keep calling them frameworks to ease writing since I also want to include SPA frameworks like Angular or Aurelia.|