Having built large apps with both approaches, I think SPA is the right way to develop any non-trivial web apps. One huge advantage of SPA approach is that you have clear separation between the client and the server. The client manages all the UI state, and the server provides a query API for the client.
Managing the state client-side makes a lot of sense in practice. It’s easier to reason about, you end up sending less data across, and you generally have a smoother user experience because you have less refreshes happening.
This approach also allows for building alternate UIs later on, such as native apps. Since you end up with a service API from the start, it’s much easier to write alternative clients against it in the future.
There are also benefits on the server-side as well. Since the state is managed on the client, the server can be stateless making caching, horizontal scaling, and load balancing much simpler. The server code becomes much simpler as well as it turns into a set of independent operations handling different types of queries by the client.
I think a lot of the reservations around SPAs come from poor tooling and frameworks available in JavaScript. However, alternative languages provide a much better experience nowadays. This article has a nice comparison across some Js frameworks and compile-to-Js languages like Elm and ClojureScript. My team works with ClojureScript, and we use re-frame for all our front-end development.
ClojureScript addresses many of the pain points present in Js ecosystem. The language itself is much cleaner and simpler than Js. The tooling is much better as well. Leiningen manages dependencies, runs tests, hot loads code during development, and packages the release artifacts. This is typically accomplished by a bundle of tools in Js ecosystem with varying degrees of success. The compiler prunes unused code, does code splitting, and minifies release artifacts out of the box. A lot of these things are either difficult or outright impossible to do well with plain Js. Meanwhile, interop with Js is pretty seamless, and it’s possible to leverage Js libraries out of the box.
For example, I’m using React Semanitc UI in a project currently. Here are the steps I needed to leverage it:
add the dependency to the project [cljsjs/semantic-ui-react "0.81.1-0"]
require the library in the namespace (:require [cljsjs.semantic-ui-react :as ui])
use its components: [:> ui/Menu [:> ui/MenuItem {:name “Home” :onClick #(js/alert “Hello”)}]]
If Js is what’s stopping you from moving your UI to the front-end I highly recommend taking a look at ClojureScript. This workshop is a good walkthrough a small project from start to finish. I recommend spending a few minutes working through it to get a feel for the workflow.
Interesting. A couple more thoughts on why developers feel compelled to go the SPA route:
Developers, or their employers, sometimes assume that they have to develop a no-compromise native mobile app. That’s like a SPA, so one might as well be consistent and use the same back-end for both.
We live in a disconnected & battery powered world, but our technology and best practices are a leftover from the always connected & steadily powered past.
What’s ironic about that is that many SPAs are themselves power-hungry, having been developed under the assumptions of the desktop-centric past.
For those that choose to go the SPA route, I think Sapper is an interesting option. It’s isomorphic, has server-side rendering, and focuses on small JavaScript bundles (with code splitting). My one reservation about it is that it doesn’t seem to support TypeScript.
But I agree with the author that for a great many applications, an SPA isn’t justified.
Well, me too, kinda – Redux feels a bit over-engineered for simple tasks. That’s why I use the simpler way to get unidirectional data flow: immer. (Used to use freezer before that.) With these small libraries, you just modify the objects as if they were mutable, but everything is updated from the root, because you’re actually modifying proxies.
For your SPA logic, you will want a rich model of objects that represent your domain and its rules
Not necessarily?? Nothing prevents you from having an SPA that e.g. doesn’t even validate forms and just relies on the server to check all the data-related things.
Anyway, there are different kinds of applications, and different approaches are appropriate for each kind. Somehow I ended up working on three apps where SPA was the right way:
an internal accounting app – I picked PostgREST for the backend. There was no need to do any server side rendering, since there’s no public website. So an SPA that talks pretty much directly to the database was great. I just wrote SQL and client side JS, that’s it.
an internal search app – ElasticSearch is already an HTTP+JSON API, so the “backend” I wrote was a proxy that added authentication and authorization to ES requests. The SPA makes ElasticSearch queries, the proxy doesn’t know much about them except for adding constraints to only let users search what they’re authorized to search.
micro-panel – maybe not strictly SPA, but it’s an embeddable component thing that pops up an entry editor on top of a personal website/blog and uses the Micropub protocol to talk to the server.
For more traditional apps where non-JS operation is important, content is displayed publicly for search engines and complex server logic is required, server side rendering might indeed be more appropriate. I guess my point is, not every app is like that.
As someone that is currently migrating/migrated mvc/applications to an Spa/rest platform, once a competitor in the same field does, in order to offer the same front end features/“experience” , e.g heavy JS front ends with lots of different libraries, an Spa is just easier than maintaining multiple JS scripts via templates. It also means you can have a solid rest backend and change the fronted as required.
I personally use angular for front end code and there are some great tutorials and articles that help/guide users towards server side rendering on larger applications (works well at enterprise level) . Currently it is just easier to use/create an Spa that not (when using lots of JS libraries), it’s not the most enjoyable of experiences, but… Users want features and others want a clean/maintainable/lts/supported code base.
The client needs to convert the JSON into HTML before rendering anything. Depending on the device and amount of JSON to transform, this can introduce a noticeable delay.
Yes, as a user this is really annoying.
If you build a data analysis tool with an interactive chart, realtime data feed and so on, I can’t blame you for handling most of the stuff with js. I’ve done the same thing, although a little bit different. Website is given by the server, actual data is retrieved by json, so if the query takes some time you’re having a clue what is happening.
But if you’re going to give me some article with text and let’s say a picture, like Lobster, DON’T let me load a website made out of JS. I will not enable JS on my mobile phone, just to load the website and wait 2 seconds until your JS retrieved everything (assuming my connection is stable) and rendered it. Just to display some text.
The main thing overlooked when comparing SPA and turbolinks is the perceived performance advantage of knowing which parts of the page are going to change.
On a turbolinks project the default loading indicator is a bar across the top of the page, vs an SPA where you can blank out the part of the page which will change. The latter looks way faster.
IMO this is unfortunate because turbolinks is much easier to get right. I’m interested in trying to improve the situation but it seems difficult without adding markup at template entry points and being able to statically determine what templates a route will hit.
Having built large apps with both approaches, I think SPA is the right way to develop any non-trivial web apps. One huge advantage of SPA approach is that you have clear separation between the client and the server. The client manages all the UI state, and the server provides a query API for the client.
Managing the state client-side makes a lot of sense in practice. It’s easier to reason about, you end up sending less data across, and you generally have a smoother user experience because you have less refreshes happening.
This approach also allows for building alternate UIs later on, such as native apps. Since you end up with a service API from the start, it’s much easier to write alternative clients against it in the future.
There are also benefits on the server-side as well. Since the state is managed on the client, the server can be stateless making caching, horizontal scaling, and load balancing much simpler. The server code becomes much simpler as well as it turns into a set of independent operations handling different types of queries by the client.
I think a lot of the reservations around SPAs come from poor tooling and frameworks available in JavaScript. However, alternative languages provide a much better experience nowadays. This article has a nice comparison across some Js frameworks and compile-to-Js languages like Elm and ClojureScript. My team works with ClojureScript, and we use re-frame for all our front-end development.
ClojureScript addresses many of the pain points present in Js ecosystem. The language itself is much cleaner and simpler than Js. The tooling is much better as well. Leiningen manages dependencies, runs tests, hot loads code during development, and packages the release artifacts. This is typically accomplished by a bundle of tools in Js ecosystem with varying degrees of success. The compiler prunes unused code, does code splitting, and minifies release artifacts out of the box. A lot of these things are either difficult or outright impossible to do well with plain Js. Meanwhile, interop with Js is pretty seamless, and it’s possible to leverage Js libraries out of the box.
For example, I’m using React Semanitc UI in a project currently. Here are the steps I needed to leverage it:
[cljsjs/semantic-ui-react "0.81.1-0"](:require [cljsjs.semantic-ui-react :as ui])If Js is what’s stopping you from moving your UI to the front-end I highly recommend taking a look at ClojureScript. This workshop is a good walkthrough a small project from start to finish. I recommend spending a few minutes working through it to get a feel for the workflow.
Interesting. A couple more thoughts on why developers feel compelled to go the SPA route:
Developers, or their employers, sometimes assume that they have to develop a no-compromise native mobile app. That’s like a SPA, so one might as well be consistent and use the same back-end for both.
There’s also the offline first camp:
What’s ironic about that is that many SPAs are themselves power-hungry, having been developed under the assumptions of the desktop-centric past.
For those that choose to go the SPA route, I think Sapper is an interesting option. It’s isomorphic, has server-side rendering, and focuses on small JavaScript bundles (with code splitting). My one reservation about it is that it doesn’t seem to support TypeScript.
But I agree with the author that for a great many applications, an SPA isn’t justified.
Well, me too, kinda – Redux feels a bit over-engineered for simple tasks. That’s why I use the simpler way to get unidirectional data flow: immer. (Used to use freezer before that.) With these small libraries, you just modify the objects as if they were mutable, but everything is updated from the root, because you’re actually modifying proxies.
Not necessarily?? Nothing prevents you from having an SPA that e.g. doesn’t even validate forms and just relies on the server to check all the data-related things.
Anyway, there are different kinds of applications, and different approaches are appropriate for each kind. Somehow I ended up working on three apps where SPA was the right way:
For more traditional apps where non-JS operation is important, content is displayed publicly for search engines and complex server logic is required, server side rendering might indeed be more appropriate. I guess my point is, not every app is like that.
As someone that is currently migrating/migrated mvc/applications to an Spa/rest platform, once a competitor in the same field does, in order to offer the same front end features/“experience” , e.g heavy JS front ends with lots of different libraries, an Spa is just easier than maintaining multiple JS scripts via templates. It also means you can have a solid rest backend and change the fronted as required.
I personally use angular for front end code and there are some great tutorials and articles that help/guide users towards server side rendering on larger applications (works well at enterprise level) . Currently it is just easier to use/create an Spa that not (when using lots of JS libraries), it’s not the most enjoyable of experiences, but… Users want features and others want a clean/maintainable/lts/supported code base.
Yes, as a user this is really annoying. If you build a data analysis tool with an interactive chart, realtime data feed and so on, I can’t blame you for handling most of the stuff with js. I’ve done the same thing, although a little bit different. Website is given by the server, actual data is retrieved by json, so if the query takes some time you’re having a clue what is happening.
But if you’re going to give me some article with text and let’s say a picture, like Lobster, DON’T let me load a website made out of JS. I will not enable JS on my mobile phone, just to load the website and wait 2 seconds until your JS retrieved everything (assuming my connection is stable) and rendered it. Just to display some text.
The main thing overlooked when comparing SPA and turbolinks is the perceived performance advantage of knowing which parts of the page are going to change.
On a turbolinks project the default loading indicator is a bar across the top of the page, vs an SPA where you can blank out the part of the page which will change. The latter looks way faster.
IMO this is unfortunate because turbolinks is much easier to get right. I’m interested in trying to improve the situation but it seems difficult without adding markup at template entry points and being able to statically determine what templates a route will hit.