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.
Emerging performance trends
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.
The LinkedIn Feed
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.
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:
- 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.
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.
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:
- First Meaningful Paint, which approximates when the page’s primary content first appears.
- 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.
Once both prototypes were feature-complete, benchmarking using WebPagetestindicated 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 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)|
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.