Next-gen Web Apps with Universal JavaScript

Gert Hengeveld
May 1, 2015 · 6 min read

On the road to Isomorphism and Adaptive web applications.

The web application landscape has recently seen a big shift in application architecture. Nowadays we build so-called Single Page Applications. These are web applications which render and run in the browser, powered by JavaScript. They are called “Single Page” because in such an application the browser never actually switches between pages. All interaction takes place within a single HTML document. This is great because users will not see a ”flash of white” whenever they perform an action, so all interaction feels much more fluid and natural. The application seems to respond much quicker which has a positive effect on user experience and conversion of the site. Unfortunately Single Page Applications also have several big drawbacks, mostly concerning the initial loading time and poor rankings in search engines.

To achieve better initial page load times your best option is to render the application on the server. This way you can simply have the server return the fully compiled initial view as an HTML document, so a user doesn’t have to wait for the JavaScript to download and execute before displaying useful content. We can even enhance this further by inlining above-the-fold stylesheets (so called Critical CSS) so we don’t have to wait for CSS files to load. Furthermore, if the server renders the whole page, search engine crawlers won’t have to resort to executing JavaScript but can crawl your pages directly. This should drastically improve your PageRank.


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.

Aside from the absence of a DOM engine, running JavaScript on the server (using Node.js) is very different from running it in the browser. For example, a browser cannot read files from disk but has to resort to XHR (AJAX) to asynchronously fetch data. Another example is URL routing. In Node.js we are handling actual HTTP requests, while in the browser we’re merely watching the current URL.

Enter Isomorphism

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:

  1. Functional isomorphism: A client can seamlessly switch between server-side and client-side rendering.
  2. Technical isomorphism, also known as Universal JavaScript: The same client application logic (code) is used in the browser and on the server.

The first can be achieved using any technology we like, so long as the server can render (more or less) the same HTML as the SPA does. However, this is terribly inefficient since we’d have to implement a lot of logic twice. That’s why we also want the second part: using the same code on both platforms. This is commonly referred to as Universal JavaScript and can be achieved by creating a set of abstractions that decouple our application logic from the underlying implementation. Effectively you can write your application logic in such a way that it won’t matter if it runs in the browser or on the server. We just have to rethink our application stack.

The Universal JavaScript Stack

In order to build a Universal JavaScript application, each part of the stack has to support it. The challenge lies in getting the same code to execute identically in the browser context and in Node.js. While the Node environment is quite flexible and supports most of what you’d expect of a back-end system, the browser is a much more restricted environment. To achieve isomorphism in terms of limiting code duplication, we need wrapper libraries so we can use the same API to talk to both the browser and Node.js. In fact, every layer in our application stack must be universal (support both platforms). Notable subjects are:

  • Handling HTTP requests (AJAX)
  • Routing of URLs
  • View rendering (templating)
  • Module loading (e.g. CommonJS)

A common misconception is that in an isomorphic application the whole back-end system must be implemented using JavaScript. In fact this is not true and even discouraged. We should still implement the core of our application using the technology that suits the job (at Xebia we often use Scala with Akka). Only the client application should be isomorphic. It can communicate with the back-end using a REST API as normal.

HTTP requests

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

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.

View rendering

Whether we choose to directly manipulate the DOM, stick with string-based HTML templating or opt for a UI component library with a DOM abstraction, we need to be able to generate markup in both environments. When using DOM manipulation we can use one of the Node-based DOM emulators such as JSDOM or Cheerio. A string-based approach doesn’t even need an emulator, as long as it’s plain JavaScript it should work isomorphically. Handlebars seems to be the most popular choice, but LinkedIn has done some more research and settled on dust.

Module loaders

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.

    Gert Hengeveld

    Written by

    Enabling Component-Driven Development with @ChromaUI and @Storybookjs.

    Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
    Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
    Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade