Code

Lighter than Lightweight: How We Built the Same App Twice with Preact and Glimmer.js

The beauty of the web is that there is no “install” step. Someone, somewhere taps a link to your site, and moments later it appears instantly in front of them.

At least, that’s the idea—but not all devices and networks are created equally. Sites that feel fast on a desktop computer with broadband can feel downright slow on a mobile phone with spotty 3G service.

So how do you build sites that stay fast no matter what? To help us better understand, we built a prototype application with two popular JavaScript libraries, Preact and Glimmer.js, that embody different philosophies of how to maximize web performance.

Emerging performance trends

Over the past few years, there has been excitement in the web development community about the idea of using lightweight libraries—those that prioritize small file size above all else—to build websites that load instantly. Advocates of this approach explain that every byte of JavaScript is a performance liability—and many popular frameworks should be considered “too big” even before you’ve written a single line of app code. Detractors of this philosophy argue that prioritizing file size over robustness stops paying dividends—and can become a liability—as an application grows. They argue that skimping on abstractions and infrastructure in the beginning just means pushing additional complexity into the app itself.

One exciting trend to emerge recently has been the idea of using compilers to “bend the curve” of tradeoffs between file size and robustness. A compiler can analyze your entire application and, theoretically, produce optimized output and assets that include only what you need.

This approach has its skeptics, too, who argue that compilers will have trouble beating a hand-tuned lightweight library. If the compiled output is still larger than the lightweight library, the argument goes, any performance gains will be wiped out.

Unfortunately, it’s very difficult to test these competing philosophies in the real world. If you want to compare how performance changes as complexity grows, how do you test that without building the same app twice? If you’re really motivated to answer this question, you could try to build the same app twice. But is that feasible? Well, that’s exactly what we did here at LinkedIn.

The need for feed

This project first started with the goal of building a prototype that optimized for page load time at all costs, to help us calibrate what “theoretical maximum” performance looked like.

Since the first thing members see when they visit LinkedIn is the feed, we thought a reimplementation of this page would be the ideal prototype for experiments around initial load time. It’s also a complex page with many different features, which was important for verifying that performance improvements held as the application grew.

preactglimmer2

The LinkedIn Feed


Some of the engineers on our team believed Preact would help us build the fastest site possible. Preact is an open source JavaScript library that implements a subset of the React component API. Many developers in the JavaScript community consider Preact to be the gold standard for building fast sites, and for good reason. What sets Preact apart is its truly remarkable file size. While the standard build of React is about 30kb minified and gzipped, Preact is just 3kb—10x smaller—for very similar functionality.

Other engineers on our team believed that Glimmer.js would yield a faster experience for the app we were building. Like Preact, Glimmer.js is an open source JavaScript library for building UI components for the web. It is built on the same rendering engine as Ember.js, but unlike Ember, provides just the UI layer—no routing, data loading, or other features. Even though Glimmer is about 20kb larger than Preact, it compiles an application’s HTML templates into compact bytecode that gets executed in the browser. Preact, like React, relies on using JavaScript to build Virtual DOM. Glimmer’s bytecode, besides being smaller than the equivalent JSX output, does not require a parse step. The engineers advocating for Glimmer believed that, given an app with a real-world feature set, the faster template downloading and parsing speed would make up for any difference in library size.

LinkedIn is a data-driven company and we wanted to let data help us answer this question. Instead of choosing between Preact and Glimmer.js, we instead built our prototype application in two different “flavors.” Then, we measured how their performance stacked up in the real world.

preactglimmer3

Our prototype: a reimplementation of the LinkedIn feed in Preact and Glimmer.js “flavors”
 

The tech stack

Given the experimental nature of this project, we took the opportunity to test-drive several other exciting frontend technologies to see if they would help us achieve our load time goals.

In addition to Preact and Glimmer.js, our prototype used:

  • TypeScript, a typed superset of JavaScript
  • webpack, a compiler and runtime for JavaScript to produce optimized static assets
  • CSS Blocks, a component-oriented, highly-optimizable subset of CSS

To reduce the cost of building the same app twice, we shared as much implementation as possible between the Preact and Glimmer flavors. All of our code went into a single repository that we organized into packages. Components for each flavor went into either the glimmer-ui or preact-ui package. Things like routing, data fetching, performance monitoring, and CSS were shared across both flavors.

preactglimmer4

When it came time to deploy, we used webpack to create optimized Glimmer and Preact builds, with any code unused by a particular flavor left out of its bundle.

Measuring up

Page load is a continuum, not a single point in time. While it’s tempting to pick a single number to compare “page load time,” we wanted to capture and compare the holistic experience of each flavor.

Chrome exposes two performance metrics that better approximate the user experience:

  1. First Meaningful Paint, which approximates when the page’s primary content first appears.
  2. Time to Interactive, which measures how long it takes for the main thread to become idle enough to reliably respond to user interaction.

When available, we reported these metrics using LinkedIn’s Real User Monitoring (RUM) infrastructure. For browsers that don’t expose these metrics, we implemented approximations that measured when the HTML page had finished loading and when the application had finished starting up, indicating the page was fully interactive.

The results

Once both prototypes were feature-complete, benchmarking using WebPagetest indicated that both flavors performed neck-and-neck, even on a smartphone with a simulated 3G network. However, experience has taught us that synthetic benchmarks don’t always tell the full story. We know the only way to be confident in how these prototypes behaved for members was to test in the real world.

We also created two variants of each flavor: one that used server-side rendering (SSR) to generate initial HTML on the server, and another that performed all rendering on the client in JavaScript. We hypothesized that client-side rendering may be faster in some cases by parallelizing the work of booting the app and fetching API data.

We ran the four variations of the prototype for one week to collect performance metrics from real members, which we fed into our Real User Monitoring (RUM) system. At the end of the week, we saw the following results:

Variant First Meaningful Paint (90th %ile) Render/Rehydrate Complete (90th %ile) Time to Interactive (90th %ile)
Glimmer SSR 2533 4112 6323
Glimmer CSR 4029 4000 5709
Preact SSR 2938 4263 5749
Preact CSR 4096 4043 6257

Time in milliseconds; lower is better. SSR = Server-side rendered, CSR = Client-side rendered. Render time measured in CSR mode, rehydration time measured in SSR mode.


Both flavors performed very well. The SSR variants in particular showed dramatic improvement in First Meaningful Paint, reducing the time before a member sees useful content by over one second compared to the CSR variants.

Based on our test, Glimmer.js seemed to slightly outperform Preact at achieving First Meaningful Paint and Rehydrating/Rendering the page, in both the server-side rendered and client-side rendered variants. Preact seemed to slightly outperform Glimmer.js in Time to Interactive (TTI), which measures how quickly the CPU becomes idle after page load. Performing a root cause analysis of this difference has proven difficult because it is much more variable than the other metrics, and is sensitive to confounding factors such as ads and members beginning to interact with the page.

Either way, overall load time performance is very close—within hundreds of milliseconds—between the two flavors.

So what does it mean?

Ultimately, both Preact and Glimmer.js are fantastic tools for building fast, modern web applications. Both technologies allowed us to build an app that beat the initial load time of the control version. The difference in performance between the two flavors was so small as to be effectively imperceptible.

Both libraries offer their own unique benefits. Our team noted that Preact’s use of JSX offered productivity benefits, particularly thanks to integration with TypeScript. On the other hand, Glimmer’s unique architecture will allow it to take advantage of WebAssembly. This opens up a tantalizing avenue of exploration for improving load time even more in the future.

One takeaway from our experiment is that file size, while important, is not the whole story. Many people use library size as a proxy for how fast their application will load. Based on our findings, apps built with a larger library can load as fast or faster than those built with a smaller library, by amortizing the file size difference via savings in other areas.

It’s important to reiterate that we focused only on initial load times. There are other important performance metrics, such as responsiveness to interaction, that were out of scope for this experiment.

We are grateful for the rare opportunity to conduct this test, where we were given the time and resources to build two versions of the same app with the same team. We are also grateful to be in the unique position of having had maintainers from both Preact and Glimmer.js on the team, helping guide each implementation.

For us, the outcome of this experiment is particularly exciting because Glimmer.js shares a rendering engine with Ember.js, which is used to build LinkedIn today. We are in the process of taking the learnings from our prototype and contributing them back to Ember.js, so that we can improve the performance of our existing app and bring our learnings to the rest of the community.

Acknowledgements

It took a multi-disciplinary team of motivated engineers and managers from across LinkedIn to execute on this experiment. I’d like to thank the following people for their invaluable contributions to the project: Sarah Clatterbuck, James Baker, Kris Baxter, Diego Buthay, Chris Eppstein, Jeba Emmanuel, Chad Hietala, Jeremy Kao, Anand Kishore, Casey Klimkowski, Kacey Mack, Adam Miller, Asa Kusuma, Caitlin O’Connor, Mark Pascual, Karthik Ramgopal, Felipe Salum, Marius Seritan, Mahir Shah, and Sara Todd.