The problem with many SPA frameworks (notably AngularJS) is their reliance on the browser DOM engine. They render web pages by directly manipulating the DOM rather than generating an HTML string the way a server-side app would. Consequently, such apps must always run in the context of a browser and therefore do not support isomorphism. Some attempts have been made to circumvent this limitation, including running a headless browser on the server to have it render the HTML. This approach has been streamlined and commercialized by the folks at Prerender.io and works well for search engine optimization, but does not address the end-user experience.
Several big names in the industry have acknowledged the shortcomings of Single Page Applications and have tackled it by introducing the concept of isomorphism. The goal is to serve fully rendered HTML from the server for performance and SEO while maintaining the perceived speed and flexibility of a client-side application. We also want to avoid writing the same client application logic twice. In short, being fully isomorphic means two things:
- Functional isomorphism: A client can seamlessly switch between server-side and client-side rendering.
- Handling HTTP requests (AJAX)
- Routing of URLs
- View rendering (templating)
- Module loading (e.g. CommonJS)
An obvious example is handling HTTP requests. In Node this is simple because we can use its “http” module. In the browser we have to rely on XHR (AJAX) to do HTTP requests. These two have different characteristics and a different API, so we can’t directly use these in an isomorphic application. Currently there are two major HTTP libraries with support for isomorphism: SuperAgent and Axios.
Routing in an isomorphic application means interpreting a URL and rendering different content based on it. There are several router libraries which support both Node.js and a browser environment. Some of these support hashbang routing for legacy browsers, but part of the idea with isomorphism is to have legacy browsers fall back to server-side rendering instead. Since routing is usually coupled with the view layer, many view-rendering libraries also provide routing (e.g. react-router). There’s a few which work well with any stack, including Director and Monorouter.
In Node.js we are accustomed to loading external modules with “require”. This allows us to easily compose our application. Browsers unfortunately don’t have a similar way to load dependencies (yet), instead all scripts must be loaded up front. With ECMAScript 6 this will change, until then we can use Browserify or Webpack to bundle up our application.
Adaptive web applications
An isomorphic app renders the initial request on the server and render subsequent page transitions in the browser. This is an impressive feat, but with isomorphic web applications we’re just getting started. Once we have the capability to choose whether to render our application on the server or in the browser, we can start to do so intelligently.
With upcoming WebAPIs such as the Battery Status API, an adaptive web application can constantly monitor how it behaves and adapt itself to ever-changing conditions. When the page loaded, the device might have had a full battery, but as it runs low the CPU might be throttled down and it may be better for the app to switch back to server-side rendering.
I think we’re simply working towards the ultimate form of progressive enhancement, and that’s a good thing.