BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles eBay's UI Framework Marko Adds Optimized Reactivity Model - Q&A with Marko's Development Team

eBay's UI Framework Marko Adds Optimized Reactivity Model - Q&A with Marko's Development Team

Key Takeaways

  • The Marko web application framework, which powers the majority of eBay.com, focuses heavily on performance.
  • Since its first stable release in 2014, Marko has been using its compiler to statically analyze templates and implement optimization strategies that are increasingly becoming mainstream (e.g., server-side rendering, progressive and asynchronous rendering, partial hydration).
  • The newly released Marko 5 continues in that direction with additional performance tuning, further ecosystem integration, and improved tooling.
  • The Marko team is building a new reactivity model that lets developers express application logic with less boilerplate, while the compiler takes care of generating code that avoids unnecessary rendering; and optimizing application bundles.

Marko is an isomorphic UI framework that started in earnest at eBay in 2012. The direct link between website performance and the bottom line of e-commerce companies resulted in Marko’s early focus on performance. Marko continues today to be heavily used at eBay, which currently employs most of the core members of the Marko team. Patrick Steele-Idem, who participated in the early development of Marko, explained:

At eBay, server-side rendering was very important, and we wanted support for UI components that provided encapsulation of rendering logic, client-side behavior and styling, and progressive and asynchronous HTML rendering (features that we had on our previous Java-based stack).

As the quest for performance is triggering an evolution of front-end frameworks towards assigning responsibilities away from the client, Marko and the strategies it employs to satisfy eBay’s requirements take an increased relevance. InfoQ interviewed Ryan CarniatoMichael Rawlings, and Dylan Piercey, members of the Marko development team, on the new Marko 5 release, Marko’s roadmap, and their vision of front-end development. The interview has been edited for clarity.

The Marko open-source project is hosted since 2017 by the OpenJS foundation —together with other known projects like Webpack, Node.js, Lodash, or ESLint

A Marko application typically consists of a series of template files that correspond to pages/routes of a web application. Marko’s template syntax allows developers to declaratively describe a page content and behavior. The template syntax extends logic-free, document-oriented HTML with primitives that specify/encapsulate logic and behavior (synchronous and asynchronous control flow, dynamic tags, custom tags, macros, JavaScript module imports, JavaScript code, and more). The following template code, extracted from a HackerNews clone, showcases some aspects of Marko’s template syntax:

import { getStories } from "../../lib/api"
static function getPage(query) {
  if (!query || !query.includes("page")) return 1;
  return +query.split("=")[1];
}

$ const page = getPage(input.query);
<app-layout>
  <div class="news-view">
    <await(getStories(input.params.stories || "top", page)) client-reorder>
      <@then|stories|>
        <div class="news-list-nav">
          <if(page > 1)>
            <a
              class="page-link"
              href=`/${input.pathname}?page=${page - 1}`
              aria-label="Previous Page">
              < prev
            </a>
          </if>
          <else>
            <span class="page-link disabled" aria-hidden="true">< prev</span>
          </else>
          <span>page ${page}</span>
          <if(stories.length === 30)>
            <a
              class="page-link"
              href=`/${input.pathname}?page=${page + 1}`
              aria-label="Next Page">
              more >
            </a>
          </if>
          <else>
            <span class="page-link" aria-hidden="true">more ></span>
          </else>
        </div>
        <main class="news-list">
          <ul>
            <for|story| of=stories>
              <story story=story/>
            </for>
          </ul>
        </main>
      </@then>
      <@placeholder>
        <div class="news-list-nav">Loading...</div>
      </@placeholder>
    </await>
  </div>
</app-layout>

InfoQ: Can you remind our readers what Marko is, in what context did it start, and what were its objectives at that time?

Michael: Marko is a UI framework for building websites and web applications. 

The Marko project predates Ryan, Dylan, and my involvement, but as the longest active maintainer, I’ll share what I know.

Marko was originally written by Patrick Steele-Idem shortly after eBay started moving to a Node.js stack. Given the importance of performance, they had settled on Dust, the only templating library for Node.js that supported streaming. 

Marko was developed to address some of the limitations of Dust as well as add a way to associate client-side code with specific templates to rein in jQuery, provide some much-needed structure, and provide reusable widgets. Version 1.0 was released in May 2014.

As the team has grown and changed, so has the framework and our vision. Since its inception, we’ve introduced state-driven updates, brought client components into the core library, and begun our journey towards a framework-as-a-language.

But it has also stayed true to many of its founding principles: HTML-first, render progressively, optimize at compile-time, don’t ship unnecessary code.

InfoQ: Before discussing the new release, let’s talk about the differentiating characteristics of Marko. We introduced Marko’s template syntax in the introduction of this article. Besides syntax, what separates Marko from other frameworks? Where does it shine?

Michael: I view Marko as a reimagination of HTML that goes beyond describing static documents. A big part of this is the simplicity of just writing HTML and adding in state and interactivity only in those places it’s needed.

Under the hood, Marko makes a lot of optimizations and as framework authors, we’re really excited to talk about this stuff and how it works. But I think it’s important to stress that these things we’re talking about aren’t “techniques” that you need to learn or anything. It’s great to understand what Marko is doing, but you don’t need to learn how to do partial hydration, for example, in Marko—it’s a core feature that’s on by default.

Dylan: It’s true. Marko does a lot for developers by default. With most modern isomorphic setups, the developer is left wrestling with a ton of configuration since there is a lot of complexity in managing the coordination between the client and server. 

Marko automatically serializes top-level input, automatically determines which components to hydrate, and automatically determines what code even gets sent to the browser. Sure, a lot of this can be achieved in other frameworks, through a lot of configuration, but in many cases, if performance optimization isn’t automated it is never done.

InfoQ: The last decades have seen a growing set of options for the architecture and design of web applications, among which are multiple-page applications, single-page applications, progressive web applications (PWAs), the JAMstack, and others. Does Marko have a specific type of application architecture that it supports or favors? Why is that?

Ryan: Marko did start as a server templating language, so its development reflects that origin. Multi-page sites and applications are definitely the core of what we do and where Marko’s optimizations are very apparent.

But a page is a page. So if someone wants to make client-routed single-page apps with Marko they can. Can’t promise the ecosystem for Marko is particularly strong there, but nothing stops you from going this way.

PWAs and JAMstack carry no particular opinion. It might be more typical to see single-page apps there but static site generation and service workers are technologies that work for anyone. Marko’s ability to analyze templates to ship no unneeded JavaScript is still really beneficial for JAMstack and offered by so few solutions.

InfoQ: To improve user experience, there is a recent trend in which the server owns a larger share of the responsibilities of a web application. The ideal vision consists of precomputing as much as possible and doing just enough work with just enough data as necessary at every point in time. Facebook Engineering summarized last year the optimization strategies they used for the redesign of the facebook.com website. Those included splitting and streaming both code and data to minimize client-side work, bandwidth, and latency. I understand that Marko has been incorporating some of these optimization strategies by default for some time—namely HTML streaming; and progressive/fine-grained hydration. Did I get it right? Can you walk us through the techniques involved, and the results they produce?

Dylan: To start I think it’s worth pointing out that streaming is a bit of an overloaded term. HTTP is a streaming protocol, so technically all web assets are streamed and many frameworks that suggest they have streaming are really just doing the minimum by breaking up long contiguous renders to avoid blocking the event loop. Perhaps a better term for what we sometimes call “streaming” in Marko is “progressive rendering.”

Avoiding blocking the event loop is important, but by using progressive rendering we can stream content to the client as the asynchronous dependencies resolve. Put simply, the user can see the content as soon as it becomes available, instead of waiting on all asynchronous data before even starting the rendering process. 

Michael: While we’re defining terms, what we call “fine-grained hydration” is something that doesn’t exist yet in Marko or any other framework, though it is coming soon. What we have today, we call “partial hydration.” Typical JavaScript frameworks when server-rendering a page also send along all the data and run a similar render process again in the browser. This works, but it can be incredibly wasteful.

What Marko does is statically analyze your components to determine which are stateful and which can be rendered only on the server. From there we can break apart the page into multiple top-level stateful components and selectively serialize the data needed for those. In so doing, only those components are shipped and hydrated in the browser. And if the page is static, well, we don’t send any JavaScript. We discussed different hydration strategies and their benefits in a detailed blog post (Maybe you don’t need that SPA).

(Source: Maybe you don’t need that SPA)

Hydration in Marko also fits in nicely with progressive rendering: components are hydrated progressively as their HTML arrives from the server.

Ryan: It might be worth pointing out the mentioned article talks about a Facebook-specific solution to these performance optimizations, but these aren’t optimizations that come with React today. Marko applications have been enjoying these benefits for years.

InfoQ: Marko 5 has a new compiler. To give our readers some context, how is this compiler used in a typical workflow? What motivated the compiler evolution and what are the benefits derived from it for developers and end-users?

Dylan: The compiler runs in the background on any imported .marko files. It’s already set up in our starter projects, but even if you’re adding it to a new or existing project, it’s basically a one-liner. You know a compiler is doing well if you forget that it’s there.

The updated compiler was mainly driven by a desire to have a better understanding of the embedded JavaScript in templates so we can produce more optimized output and provide better error messages.

By leveraging Babel’s parser in the new compiler we can easily support bleeding-edge JavaScript including experimental syntaxes, and even in the future TypeScript. On top of that, Babel has a well-established API for writing plugins, which Marko has adopted to make extending the Marko compiler easier.

The compiler has evolved to use a multi-stage approach that allows it to handle multiple languages and versions, support userland plugins, access metadata from across templates, and ultimately output code for different platforms be it DOM for the browser, or HTML for the Server, or even things like WebComponents, other frameworks, or new runtimes.

InfoQ: What is FLUURT, what are the motivations behind it, and how does it fit with Marko? In particular, what is granular compile-time reactivity, and which problems does it solve/what benefits does it bring?

Michael: The FLUURT acronym kinda started as a joke, but seems to have stuck. It stands for “fast, lean, unified, update, and render target.” With the next iteration of Marko, we want to leverage the compiler as the primary abstraction for the browser rather than a runtime abstraction like our current VDOM. Svelte (probably the other most well-known compiler-as-a-framework) outputs targeted code, but there are multiple code paths for creation, mounting, updates, and hydration. This leads to a larger per-component size. This acronym has served as a guide for us to avoid similar compilation bloat. 

This new runtime will initially just be another compilation target for the Marko compiler, but we are designing it specifically around optimizations that we can make with the new compiler and changes that we are making to the language itself.

Assuming we do this right, the language-level changes are going to be the only visible change to our users (besides apps getting faster/smaller). The big change on the language side is that we’re bringing reactivity in as a primitive.

 

class {
 onCreate() {
   this.state = { count: 0 };
 }
 increment() {
   this.state.count++;
 }
}
<div>${state.count}</div>
<button on-click("increment")>
 increment
</button>

 

<let/count=0/>
<div>${count}</div>
<button on-click=()=>count++>
   increment
</button>

 

Before FLUURT

With FLUURT

Ryan: This reactivity is something we are all pretty excited about. We’ve seen compiled reactivity before with Svelte, but not fine-grained reactivity handled this way. If you don’t know what I mean by fine-grained, I’m talking about the approach that MobX/Vue reactivity takes. You can picture updates propagating like individual cells in a spreadsheet. The difference when you build your whole renderer on it, unlike Vue, Svelte, or React, is we can forgo the boundaries of the component model and treat changes to the whole application as a series of these pinpoint updates.

Picture some state created by a parent and passed down through a couple of child components before being assigned to a DOM element attribute. Updating that state causes no re-evaluation of the parent, nor any component down the tree. Only that one DOM binding where it is being used will be executed.

For the developer, this means you don’t have to think about how the way you’ve structured your components is going to affect performance—only the nodes that need to update will update. For the library, not only are updates 1 to 1 but knowing the dynamic parts means even view creation can be extracted to a few performant template clones. No popular framework has access to this sort of performance in the browser.

The effect is more than performance though. Fluurt’s tag primitives might seem odd at first glance until you recognize that it means they can live (their whole lifecycle) at the scope they are nested independent of any concept of components. This allows for a sort of cut-and-paste refactoring story. Behaviors can be co-located with their display logic in a way that contiguous blocks of markup can be moved and composed at will. (More details in Marko: Designing a UI Language)

Combining this with our compiler analysis has a significant impact on hydration as well. We can achieve “fine-grained hydration” by understanding which parts of the template are dynamic and prune whole portions of the components and control flows from the browser bundle using the dependency graph. Worst case the result might be the same as current Partial Hydration. Best case previously nested branches components are now able to be pruned. But most importantly, it has the same effect of not forcing developers to think about it. This is only achievable with the union of fine-grained reactivity, explicit language semantics, and cross template analysis.

(Source: Maybe you don’t need that SPA)

InfoQ: What is next on the roadmap?

Ryan: Well, there is still a lot to do. FLUURT is still under active development. There are a lot of nuances in the cross template analysis and more work to be done on hydration. Honestly, we could work on the ideas here for years, but we intend you won’t have to wait that long to get your hands on it.

We also have done the work now to modernize Marko’s build setup, so there are a ton of integrations to be done here. The ones we really want to see are TypeScript and Prettier. Also, integrations with new bundle-less bundlers like Vite and Snowpack would put Marko in a place that is more familiar with more recent trends in frontend development.

Finally, Marko’s server rendering performance makes it well suited for Edge computing. Better support for serverless deployments and edge workers is something we want to address soon.

InfoQ: What do you think is important in web development now and what is going to be important in the next few years?

Dylan: Users are going to expect more and more from their frameworks so tooling will have to keep up with that demand. With the ever-increasing requirements of frontend applications, frameworks that can hide this complexity while delivering best-in-class performance are essential. In short, Marko.

Ryan: Server rendering is a topic getting a renewed focus. But this isn’t our first time around. We’ve seen this back and forth at least a couple of times now, so it will be important to learn from the lessons of the past, to prevent making the same mistakes again. That’s how we’ve approached reactivity, and that’s how the greater ecosystem needs to look at re-incorporating server rendering. Marko has a lot of experience here and I hope to see our technological approach continue to lead the pack.

Michael: Compile-to-JS (or WebAssembly) isn’t going away, in fact, it’s going to increase. We’ve seen a recent surge of interest around “no build” tools, but the reality is these tools still have build steps, they’re just getting faster and less intrusive. Our compiler abstractions will become less leaky and fade into the background rather than what you typically see today with developers very much having to understand and spend time to properly configure their tools.

If it sounds like we’re just describing where Marko is headed, you’d be right, but what would we be doing if we weren’t building the future we want to see?

About the Interviewees

Ryan Carniato is a Staff Engineer working on Marko on the eBayUI team. He is a JavaScript performance enthusiast and is passionate about reactivity. He enjoys being involved in open-source both inside and outside of eBay. You can follow him on Twitter @RyanCarniato

Michael Rawlings is a Staff Engineer on the eBayUI team, where he works closely with product teams to improve the way front-end applications are built. He enjoys building tools that improve the developer experience and make it simpler to build scalable and performant apps. Follow him on Twitter @mlrawlings

Dylan Piercey is a Staff Engineer with a passion for improving the developer experience, innovating the web platform, and solving complex technical problems. He is a part of the eBayUI team, where he contributes to many open-source projects and provides technical guidance and support across the eBay organization. You can follow him on Twitter @dylan_piercey

Rate this Article

Adoption
Style

BT