UPDATE: With January 1st, 2017 we rebranded our hosted CI Platform for Docker from “Jet” to what is now known as “Codeship Pro”. Please be aware that the name “Jet” is only being used four our local development CLI tool. The Jet CLI is used to locally debug and test builds for Codeship Pro, as well as to assist with several important tasks like encrypting secure credentials.
Whether or not you've ever heard of VueJS before, never fear. I'm here to share some insights on how and why we at Codeship used this "new" library for our Docker-builds UI,.
What Is VueJS?
To get a rough idea of what Vue (pronounced "view") does, think about it as the 'V' in an MV* pattern. If you'd like to compare it to something else, it's probably closest to React but with a far lower barrier of entry. No need to learn something like JSX or pre-compile files just to give it a try. Drop it into your HTMLs header like any JS library, and you're all set. Templating will probably look familiar to anyone who has worked with Angular or Mustache in the past.
So let's talk about what made VueJS so attractive for us at Codeship.
Data-driven versus DOM-driven UIs
Separation of concerns is a common term in software engineering. Yet when it comes to UIs, we see code like this all too often:
function textReturningFunction () { return 'Hello World!' } // Set some text in jQuery $('.someElement').text(textReturningFunction())
I wrote JS like this for a long time early in my career. But if you think about it, this code is pretty far removed from the idea of separation of concerns.
The JS has to know a lot about your markup and the general structure of the DOM. At any given time when the markup changes -- .someElement
gets renamed to .some_element
, for example -- your JS will break. The function that's actually returning the data we want to show did not break though. It's still returning Hello World!
when called. In this instance, the data is the same, but the binding is broken. This happened because the JS is DOM driven.
Let's look at a simple VueJS instance that should print out the same text as above.
The Markup:
<div class="app"> <span class="someElement"> {{ msg }} </span> </div>
The JS:
new Vue({ el: '.app', data: { msg: 'Hello World!' } })
The Vue instance is a simple object. Running this code will do the trick, and you'll find <span class="someElement">Hello World!</span>
on the page. If the class on the span
would change now, it will not break the JS because the class is not referenced.
To stay true from the start, we still referenced an element in our Vue instance. The .app
element is the entry point from which Vue will handle things. If that one gets changed, the reference needs to be updated as well.
Reactivity
All variables in Vue are reactive. This means they are observable and can have watchers attached. Variables that change will automatically inform their peers of the change. On the VueJS side, there's actually a very detailed description of this system that I highly recommend you read.
For quick understanding, compare Vue's behavior to dirty checking in Angular. If something changes in Angular, it will start to search for that change. It'll compare new values with old until no more changes are found, and it will update what's necessary. This takes time and eats up resources while correlating with the complexity of the application.
A reactive data system like Vue uses may need more initiation time upfront, but that's a good price to pay.
Components
Vue's flexible but simple system is built around the concept of components. Components are small, reusable parts of the UI. Actually, this should be nothing new -- the majority of JS frameworks these days are built around that concept. Web Components are a known solution for quite some time now. The problem is always the browser-wide implementation.
Vue takes care of this by providing a browser-independent component system. This system is straightforward to use and falls in line with the rest of Vue's structure.
Why We Chose VueJS
Inside the Codeship application, we have one high-load and interaction-heavy page: It's the build detail page of our Docker-based build infrastructure (Jet), letting the user read terminal output. The challenges of this page after the initial release included:
Fix the performance problems we were facing (frozen browser)
Allow efficient rendering of objects (10K+)
Make the code behind the system more accessible
Keep the overhead of introducing a new technology small
For the first implementation, we used Angular. The terminal output consisted of a large string coming from the server. On the client, that string got transformed and rendered onto the page in one process. When we tried to use objects for rendering, Angular wasn't performing well enough. At a certain number of lines, those logs where so big that they could crash the browser. After some investigation, we were able to trace this back to the scope used by Angular. The sheer number of lines was not the issue, but rather that Angular tried to keep track of the whole DOM and all its changes.
We needed a new tool that didn't care for the DOM in a way that allowed it to crash like that. Also, we couldn't afford to greatly change how we handled assets, by introducing new build tools or attaching new infrastructure. Vue seemed like it would fulfill those needs.
A rough prototype with large HTML files (more than 1MB) dumped into the browser was promising. No impact on the performance of the page was noticeable. In the next iteration, we tried to render HTML based on plain JS objects. Ten thousand lines were no problem at all. After some further testing and a pros/cons list, it made sense to move forward with Vue. Let me show you what we did.
Starting Simply with VueJS
Our first iteration of rebuilding focused on preparing the base structure of the build detail page of Jet; our first goal was to render the sidebar of the page. This sidebar contains steps and services that are clickable elements following a given structure, in this case, an HTML structure.
<!-- Application Example --> <div id="app"> <aside> <ul class="services"> <li v-for="service in services"> {{ service.name }} </li> </ul> </aside> </div>
Notice how easy this small snippet of HTML is to read. Even if you're not familiar with the exact syntax of Vue, the code makes sense. For every service
object out of the array services
, we plan to render a li
element with the name of a service.
Let's see what the Vue code would look like:
new Vue({ el: '#app', data: { services: [ {name: 'first service'}, {name: 'second service'} ] } })
When firing up the browser, the result is as you would expect:
<!-- Rendered HTML --> <div id="app"> <aside> <ul class="services"> <li>first service</li> <li>second service</li> </ul> </aside> </div>
The next step is leveraging components:
<!-- Application Example --> <div id="app"> <aside> <ul class="services"> <!-- Placeholder for the component --> <jet-service v-for="service in services" v-bind:service="service"></jet-service> </ul> </aside> </div>
The components in Vue fall into place quite easily:
var JetServiceComponent = Vue.extend({ name: 'jet-service', props: ['service'], // made available by v-bind:service="service" template: ` <li> <strong>{{ service.name}}</strong> </li> ` }) // Register that component in our Vue Object new Vue({ el: '#app', data: { services: [ {name: 'first service'}, {name: 'second service'} ] }, components: { 'jet-service': JetServiceComponent } })
Running our updated code in the browser will eventually produce the following output:
<!-- Rendered HTML with components--> <div id="app"> <aside> <ul class="services"> <li><strong>first service</strong></li> <li><strong>second service</strong></li> </ul> </aside> </div>
What should be obvious so far is that using VueJS is very straight-forward. There's no need for a lot of boilerplate code to get started. The only thing you need is a good understanding of JS objects and functions.
Adding Complexity with VueJS
The jet-steps
component was a little more complex. Steps can be grouped and nested, and the structure is probably different for every project. Let's proceed by aiming for the following structure:
-- some step -- step group -- grouped step alpha -- grouped step beta
or as JSON
[ { name: 'some step', type: 'step'}, { name: 'step group', type: 'group_step', steps: [ { name: 'grouped step alpha', type: 'step'}, { name: 'grouped step beta', type: 'step'}, ]} ]
The component should be able to self-invoke based on the structure, making it a little more complex. The Vue object uses specific key values for structuring. During the setup process, getters
are made available that can then be used in a template. The computed
key allows us to store functions on the Vue object that return computed values. Let's use a function for checking whether a step is a group step:
var JetStepsComponent = Vue.extend({ name: 'jet-step', props: ['step'], computed: { isGroupStep: function () { return this.step.type === 'group_step' } }, template: ` <li> <span>{{ step.name }}</span> <ul v-if="isGroupStep"> <jet-step v-for="step in step.steps" v-bind:step="step"></jet-step> </ul> </li> ` })
You may have already guessed how the Vue object would look. Vue is not getting more complex to allow nested components. There's a line written by Evan You, the creator of Vue, that I want to quote here:
Thoughts on simple versus easy: Why not make it simple AND easy?
This is exactly what Vue tends to do, and it does it really well.
VueJS Performance
At this point, let's step away from looking at the syntax of Vue and see where it outshines our previous implementation. Performance was the biggest issue as I mentioned earlier. The UI should show a large amount of terminal output to the user.
I ran some benchmarks on the performance at certain steps of the development process. The test generates 5,000 lines of a Base64 encoded random log line. A log object would look something like this:
{ timestamp: 'some UTC timestamp', service: 'app', payload: 'A base64 decoded string' }
The first intention is probably to go the route we did with Vue by rendering every object line in a loop. That's pretty much how it should have worked, but Angular was unperformant doing this.
The problem
The implemented way was to prerender the HTML as a string on the server and pass it down to the client. The client would then use plain JS to attach that string to the DOM. That approach brought the fastest results.
But as soon we got close to around 8,000 lines, Angular regularly froze the browser tab, especially on slower clients. After investigating, it turned out that Angular's need for keeping track of inner scope was killing it. Eight thousand lines of log output generated 32,000 DOM nodes. Angular could not process all of those as easily as we had hoped.
The first solution
The way we made it work to give Angular the necessary "breathing room" was a custom worker. That worker transformed the string into DOM nodes straight away but only injected 200 lines every 40 ms into the view. This worked fine, and Angular was then able to process up to 15,000 lines of log. However, at this level, the performance of the browser went down. Scrolling became slow.
Obviously, this still wasn't good enough -- we had clients that had even larger logs. Also we moved a lot of work to the server. Base64 decoding and pre-rendering was all done outside of the client side code.
To get a rough understanding of efficency in rendering, I tested the 5,000 log lines in the old UI and the first Vue implementation. The only difference here was that Vue missed the worker and the log was directly dumped into the View.
Tech
Processes
Rendering
Total
Angular
270
6.000
6.270
Vue
120
2.000
2.120
Times in ms
Processes are tasks done by the code outside of the log rendering
Just by switching the tool, we already cut the time more than in half.
The final solution
In the next iteration, I wanted to fully leverage client-side rendering of the log lines. The plain objects should get passed down, with the payload still Base64 encoded. The loop for rendering the log lines was now as simple as one might guess.
html <div class="logLine" v-for="line in log"> <span> {{ line.timestamp }} </span> <span> {{ line.service }} </span> <span> {{ line.payload }} </span> </div>
Before printing out the log, I had to decode it. For this, I created a simple JS class with functions. Vue did not force me to do things in any certain or complicated way. The code looks something like this minus the Ajax parts:
// The class is written in ES6 class LogHelper { getLog () { // ... function that gets the log from the server // eventually we have the raw log available for further use let rawLog = [...] return _prepareLog(rawLog) } _prepareLog (arr) { let decodedLog = arr.map( (line) => { line.payload = this._decode(line.payload) return Object.freeze(line) }) return decodedLog } _decode (str) { // This correctly preserves UTF8 characters return decodeURIComponent(Array.prototype.map.call(atob(str), function(c) { return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2) }).join('')) } }
In the end, we now have a reference to the log in our Vue instance, so the UI has access to it. I ended up using Vuex, a state management inspired by Flux or Redux, but that's another story. As soon as the log
was available for the template, Vue took care of rendering it. See the new benchmark added to the table:
Tech
Processes
Rendering
Total
Angular
270
6.000
6.270
Vue
120
2.000
2.120
Vue (Objects)
1.900
1.700
3.600
Those numbers were pretty interesting to me for various reasons. Let's break it down real quick:
Angular versus Vue (Objects)
The significant detail here is that the total process time is still almost 50 percent faster. But besides cutting the time in half, we also freed up process time on the server. We now Base64 decode on the client and don't need to pre-render an HTML string. This also makes the payload we need to initially load from the server smaller. Clean win!
Vue versus Vue (Objects)
One of the biggest questions is probably why the general process' time is so much bigger. The reason for this is that Vue pre-renders all the objects in the Virtual DOM of the browser. In correlation, the actual rendering time is even 300ms faster than in the previous Vue implementation.
The Virtual DOM starts shining when it comes to the point of filtering log lines. Every object-connected DOM element is already cached and can be re-rendered instantly. That's a cost I was willing to take.
Why Vue Could Work for You
There is no such thing as the perfect tool for every job, but there is a right tool for a particular job. Vue is a tool I found to be very helpful and efficient. It only tries to be very good at one thing and nails it: bringing data into the view of your web application is a breeze as we've seen in this article. What's even more appealing about Vue is the fact that it can grow into something more when needed.
These days, Vue comes with a great ecosystem already.
Routing? Vue-Router is available.
Ajax Resources? Vue-resource takes care of this. Edit: As of now, axios is the recommended choice
Application State Management? Vuex is ready.
Webpack or Browserify? Vue has the tools already prepared.
Vue has a thriving community, a lot of stars on GitHub (Vue & Angular at 55,000+ (a.o. 6th. June 2017)) and a great maintainer I fully trust.
Please let me know what you think about Vue in the comments below, and I hope this article at least sparked your interest in considering Vue for your next project.