At eBay we are using Marko to render over a billion requests every day and this has required us to finely tune Marko, our open source UI library. We have heavily optimized Marko for fast rendering, advanced performance techniques and to achieve a minimal page weight (~10kb gzipped). Performance is only one concern because we have also had to scale Marko to support development across hundreds of teams in a way that allows developers to efficiently create maintainable and robust web apps.
We have created our own benchmarks and we have added Marko to other benchmarks, but benchmarks cannot always be trusted. While we make every effort to be fair with our benchmarks, what matters most is performance in real world applications as opposed to focusing on micro benchmarks. This is one reason that the V8 team has switched to a new methodology to measure and understand real-world JavaScript performance.
Similarly, we’ve taken a look at how our developers are actually writing their Marko components and have found patterns that could be further optimized. Instead of focusing on benchmarks in this article, I want to focus on the details of optimizations that we have applied to Marko.
Marko is an isomorphic UI library that runs on both the server and in the browser. As Michael Rawlings mentioned in “Server-side Rendering Shootout”, when rendering on the server, Marko renders directly to a string representation of the document (HTML) that can be sent as the HTTP response.
When rendering in the browser, an HTML string would have to be parsed in order to update the DOM. For this reason, Marko compiles a view to a program that renders directly to a virtual document (VDOM) tree that can be used to efficiently update the real DOM when targeting the browser.
Given the following template:
The compiled output is optimized for streaming HTML output on the server:
The compiled output is optimized for virtual DOM rendering in the browser:
The Marko runtime is not distributed as a single JavaScript file. Instead, the Marko compiler generates a JavaScript module that will only import the parts of the runtime that are actually needed. This allows us to add new features to Marko without bloating existing applications. For example, given the following template:
In the above example, extra runtime code is needed to render the style
attribute based on the JavaScript object that is provided. The compiled code that imports the styleAttr
helper is shown below:
Compared to solutions based on JSX that exclusively do virtual DOM rendering, Marko has a huge advantage for server-side rendering. When rendering to a virtual DOM tree on the server it’s a two-step process to render HTML:
In contrast, Marko renders directly to an HTML stream in a single pass. There is no intermediate tree data structure.
Given the following template:
Marko will recognize that the template fragment produces the same output every time and it will thus create the virtual DOM node once as shown in the following compiled output:
Rendering a static sub-tree has virtually zero cost. In addition, Marko will skip diffing/patching static sub-trees.
Similarly, on the server, Marko will merge static parts of the template into a single string:
Marko will also optimize static attributes on dynamic elements.
Given the following template:
Marko will produce the following compiled output:
Notice that the attributes object is only created once and it is used for every render. In addition, no diffing/patching will happen for static attributes.
With Marko we favor doing as much at compile-time as possible. This has made our compiler more complex, but it gives us significant gains at runtime. We have ~90% code coverage and over 2,000 tests to ensure that the compiler is working correctly. In addition, in many cases the Marko compiler provides hints to the runtime for a given template so that the runtime can optimize for specific patterns. For example, Marko recognizes if an HTML element only has class
/id
/style
defined and the runtime optimizes for these virtual DOM nodes when doing diffing/patching (the Marko compiler generates code that flags simple virtual DOM nodes for targeted diffing/patching logic).
If you are building a UI component you will likely need to write code to handle various DOM events (click
, submit
, etc.). It is common for developers to write code that adds DOM event listeners using el.addEventListener(...)
or using a library such as jQuery. You can still do that when building UI components using Marko, but there is overhead in attaching listeners when lots of components are being initialized. Instead, Marko recommends using declarative event binding as shown below:
When using declarative event binding, no DOM event listeners are actually attached for events that bubble. Instead, Marko attaches a single listener on the root DOM element of the page for each DOM event that bubbles (done at startup). When Marko receives an event at the root it handles delegating the event to the appropriate components that are interested in that event. This is done by looking at the event.target
property to see where the event originated and then walking up the tree to find components that need to be notified. As a result, there is slightly more work that is done when a DOM event is captured at the root, but this approach uses much less memory and reduces the amount of work that is done during initialization. The extra overhead of delegating events to components will not be noticeable so it is a very beneficial optimization.
Interested in learning more about Marko? If so, you can get additional information on the Marko website. Join the conversation and contribute on GitHub and follow us on Twitter.
Cover image credit: Superhero by Zech Nelson from the Noun Project