Optimization

CSS at Scale: LinkedIn’s New Open Source Projects Take on Stylesheet Performance

Browsers use Cascading Style Sheets (CSS) to control the appearance of websites. From borders, fonts, and colors to layout, images, and animations, there are roughly 500 different style properties that can be declared with CSS. These properties are what make the visual diversity of the internet possible for your handheld device, desktop browser, and even your printer. Understanding the myriad style properties is a challenge for any front-end developer, but if you want to start an argument, bring up the question of how CSS selectors (the queries that assign style declarations to HTML elements) should be structured.

The trade-offs for maintainable stylesheets

The art of writing CSS lies in how these selectors are structured. Well-structured selectors simplify the long-term maintenance of an application, facilitating the evolution of the common aspects of a design while keeping unintended changes rare or impossible. As such, most applications have a system for keeping styles isolated to their intended components, which makes for a much more predictable development process.

However, the benefits of isolation come at a cost. Strategies for namespacing selectors to prevent accidental matching can incur penalties to performance and developer experience:

  • Hashed class names don’t compress well
  • Scoped selectors are more costly to match
  • Prefixed class names are longer and annoyingly repetitive

Beyond simple namespacing, it’s natural to repeat style declarations in different selectors that happen to have the same style property set to the same value, but not for any reason clear enough to create coupling through a shared concern. In other cases, it’s because it keeps the markup free of style concerns or the need to apply many class names to each element. DRY (Don’t Repeat Yourself) is a good principle, but taken too far, you will get chafing. Where repetition does becomes a maintenance burden, stylesheet preprocessors like Sass and PostCSS can help developers share common concerns of a design across selectors that are purposefully isolated to a single component. Projects that employ preprocessors in their isolation strategy generally have more selectors where each matches relatively few elements, and a higher percentage of duplicative style declarations.

It might seem that all those selectors would be a frequent performance problem, but selector matching performance is so fast, and the CSS declarations are so friendly to compression algorithms, that the effort required to optimize a CSS file is usually better spent working on other parts of your application with a greater return on investment. Recently, a blog post by Jens Oliver Meiert revisited the subject of selector performance and declared: Performance of CSS Selectors Is Still Irrelevant.

"Selector performance is not something to optimize for. Selector performance is irrelevant. The price we pay for optimizing for selector performance is, indeed, terrible: We micro-manage our work for gains that aren’t even noticeable." – Jens Oliver Meiert

There’s definitely wisdom to that—most CSS selectors can be matched against a normal document in less than 10 nanoseconds, so it’s hard to justify giving any single selector much attention. However, even tiny amounts of time can become significant when repeated enough times.

Almost a year and a half ago, as we were about to launch the new redesign of www.linkedin.com, one of our resident browser performance experts, Kris Selden, came to the team with detailed trace data from Chrome’s profiler. This data proved our CSS selectors and transfer times had become a serious performance drag. Because of our scale, and the cumulative result of over-using Sass mixins to share style properties, we had managed to produce an astonishing 3.2MB (330KB compressed) of CSS with 27,000+ selectors averaging 2.18 combinators per selector. The complexity and sheer volume of selectors had pushed our selector matching times above 600ms during initial page load.

Since then, we have been devoting serious resources to improving our site’s performance. Like many developers, we initially took a very manual approach to fixing our CSS infrastructure through process, training, and conventions. But we have also been investing in technology to help take our stylesheets to the next level. As we start to roll these out within LinkedIn, we believe now is the perfect time to make two new tools available as open source software.

Announcing CSS Blocks and OptiCSS

css-at-scale-2

CSS Blocks (GitHub link) is an ergonomic, component-oriented CSS authoring system that produces high-performance stylesheets. CSS Blocks breathes new power and ease-of-use into the CSS ecosystem by analyzing and rewriting not just your stylesheets, but your templates too! This enables a slew of new features, developer delight experiences, and optimizations that were previously impossible. CSS Blocks, through a process called “static analysis,” can look at your project and know with certainty that any given CSS declaration will, will not, or might under certain conditions be used on any given element in your templates. This determinism lets you supercharge your CSS build with powerful stylesheet performance techniques like code-splitting, critical inlining, and tree shaking, and allows us to optimize selectors and file size with OptiCSS. It also enables productivity features like deep IDE integration, and build-time errors for common coding mistakes like typos and ambiguous resolutions of style declarations.

OptiCSS (GitHub link; pronounced “optics”) is a general-purpose CSS optimization library. It takes in stylesheets and static analysis information that efficiently describes the application’s markup and its dynamic states. OptiCSS performs a number of powerful rewrites on your CSS, and outputs a brand new stylesheet with entirely different, drastically minified CSS classes, along with instructions for an automated rewriter to modify your application templates at build time to use these new classes so everything looks and functions exactly as it did before. OptiCSS is a very low-level library, but we wanted to make it available to all CSS framework developers, not just CSS Blocks, so the benefits of a general-purpose, template-aware CSS optimizer can be shared and everyone can benefit through collaboration.

With CSS Blocks, and OptiCSS running at its core, you get to write ergonomic CSS and let the build take care of making your stylesheets properly scoped, screaming fast, and fantastically small.

Fantastically small? We were able to vet these tools in a production test, and saw fantastic results. The current linkedin.com homepage clocks in at 1.9MB of CSS (156KB compressed). After re-building a fully-functional version of the homepage with CSS Blocks, we were able to serve the same page with just 38KB of CSS. To be clear: that's the uncompressed size. After compression, that CSS file weighed in at less than 9KB! And, because CSS Blocks enables code-splitting, we were able to achieve a 94% reduction of our compressed CSS delivered for initial render.

The problem of CSS performance may not be on your radar, or maybe the solutions you’ve considered looked too expensive and had too little benefit. Setting performance aside, CSS Blocks is a fantastic developer experience that will make the engineers using it happier and more productive. We think it’s worth adopting even without the performance features. It also just happens to help you write CSS that starts tiny and stays tiny as you scale your product and the size of your team.

How does it work?

CSS Blocks is an evolution of many existing solutions and best practices in front-end development, bringing them all together to achieve something truly unique.

Styles in an application are organized into blocks. Each block contains class selectors that target different elements that work together to create a single unit of design; each class can be in one or more different states. The rules for selectors in a CSS block are based on BEM, except with a more developer-friendly syntax and automated scoping. In development, the classes and state attributes of a CSS block are compiled to BEM-style class names.

The JavaScript and/or templates that reference the block’s styles are analyzed at build time, so that we know exactly what styles are used on what elements. Each application framework can have its own syntax for referencing and manipulating styles dynamically in a block so that it feels natural and matches that framework’s conventions. Where possible, CSS Blocks took inspiration from existing solutions for accessing styles in components, such as classnames and css-modules. The parsed block styles and template analysis information are passed to OptiCSS, where a number of optimizations are performed:

  • Identifiers are rewritten to be smaller and friendlier to compression algorithms
  • Selectors and declarations that cannot match any element are removed
  • Duplicative style declarations are coalesced into a new class selector; styles that used that selector will now use the new class when appropriate

When OptiCSS has done its job, the CSS file has many classes that are used to efficiently assign one or more declarations repeatedly throughout the application. In effect, the optimizer creates an optimal set of atomic css classes for your application.

The JavaScript and/or templates are then processed so that the new selectors will be used correctly wherever the original styles would have be used.

css-at-scale-3

Example source and output of CSS Blocks. Text content here.
 

Development of CSS Blocks and OptiCSS

Following the realization that selector performance and file size were hurting our site speed, we needed to make progress quickly to alleviate the issue. The team worked hard to establish new best practices, including converting our application to use BEM-style selectors and to prefer shared class names in our templates over mixins in our stylesheets. The choice to adopt BEM was controversial; many developers disliked the ergonomics of it and found it too constraining. There were several meetings, but eventually a decision was made to proceed.

A small army of front-end engineers worked hard to perform the migration away from repeatedly invoking Sass mixins. There was one week where I code reviewed more than 80 pull requests! Thanks to everyone’s hard work, we shaved a little more than 1s off of our initial load time. After that, our CSS grew more slowly and we continued to improve our load times by employing other strategies, like lazy loading stylesheets. Despite those improvements, our measurements still showed considerable time being spent on CSS; we needed to keep working on this problem, but at least we had bought ourselves some time to step back and think about things more holistically.

That BEM project had been operating in parallel with several other performance projects. When that initiative ended, a small team of engineers was spun up to think about what a performance-first approach to building our web applications at LinkedIn might look like. Tom Dale recently wrote about how that project led us to build the LinkedIn homepage experience in two different UI frameworks. Both of those front-ends were built using CSS Blocks and OptiCSS.

During this homepage experiment, the team worked to ensure that we could style our web applications with a focus on performance. We wanted to make sure that such applications could thrive in high-latency, low-bandwidth environments with expensive data. Based on what we saw from some of the fastest websites in India, we gave ourselves a first page view budget of 10KB of compressed CSS. Considering that our website currently serves 1.9MB of CSS (156KB compressed), this goal seemed wildly optimistic—but, as you read above, we ultimately beat the CSS file size budget by more than 10%!

Beyond the performance goals, we wanted to make sure that, wherever possible, our existing use cases would be supported and that the numerous annoyances that we saw in our developers’ workflow could be obviated. We asked ourselves:

  • Was there a way to write CSS that kept our styles clean, with low specificity, without redundancy in our output or our authored selectors?
  • Could we allow for composition and inheritance of styles?
  • Could there be a way to write selectors without specificity wars or manually tweaking document orders?
  • How could we support our design system’s needs to distribute a style library, evolve designs, and ensure adherence to our style guide where it matters while still enabling applications to experiment?

On the performance front, we wondered whether we could use tooling on our CSS that would produce styles as small and as fast as the sites that use atomic-style classes, without significantly changing the way we authored the styles for application components. We wondered whether, as friendly as CSS is to compression algorithms, could we do even better?

We evaluated the state of the art in CSS frameworks, including recent developments in the React and Vue communities towards using CSS-in-JS, and we saw a lot of fantastic ideas there. In those environments, CSS was able to participate in performance-centric capabilities like code-splitting and inlining critical styles. The authoring systems themselves made sure that the styles associated with removed functionality would be removed as well. We wanted all these great capabilities too.

Nothing existed like what we started to imagine, and so we brought all of those ideas together into a CSS framework based on static analysis and where the whole application would be analyzed and optimized. Because of the dramatically different syntaxes of Preact and Glimmer applications (the two UI frameworks used in our homepage experiment), CSS Blocks was designed to accommodate different types of template analyzers and rewriters.

The results of our initial production deployment of CSS Blocks went so much better than we could have expected. The way the framework shaped the code we wrote resulted in dramatic benefits to our stylesheets and promoted decoupled styles and components. Even before we had OptiCSS working and enabled, CSS Blocks was paying dividends for the engineers building the application.

Collaborate with us

We still need to produce some tools and remaining features that we think are needed to scale up from supporting a small team of engineers to supporting several hundred front-end engineers across various teams and applications. But from this point forward, we want to invite the community to be part of that maturation process so that we do not optimize for LinkedIn’s own needs and use cases at the expense of others. We want everyone to benefit from the same performance gains for their website, developer experience, and user experience.

CSS Blocks is opinionated and we understand that it may not be the right choice for many teams. But we think that OptiCSS, and the new approach that it takes with template analysis and rewriting, is a game changer for front-end development. It would be a shame to couple it to just one flavor of stylesheet authoring. As such, we built OptiCSS so that it is very configurable and flexible. It is designed to work with many different styling frameworks, including frameworks that would qualify as “CSS-in-JS.” In some cases, those frameworks are well-positioned to produce output that is even friendlier to optimization than CSS Blocks. We want to collaborate with other CSS and styling frameworks to make OptiCSS a foundational technology that can power the next generation of CSS authoring.

These projects are still under active development. Today’s first public release is an alpha release for developers to preview, kick the tires, and decide if these projects are as exciting for you as they are for us. If they are, we hope you’ll join us as a collaborator.

Until we release 1.0, the APIs are unstable and minor releases may have backwards incompatibilities. We urge you to wait for our 1.0 release before using CSS Blocks in production. We think that, with your help and feedback, we can ship the 1.0 release later this year. If you're not currently in a position to help fix the bugs that are blocking you or to be involved by building other important infrastructure, integrations, or tools, we'd ask that you wait to adopt CSS Blocks until our 1.0 release. At that point, we'll be in a position to offer you the level of debugging tools and support that you (and our engineers here at LinkedIn) need.

At this point, you probably want to know a whole lot more about the code and how this framework and optimizer work, so we invite you to continue your investigation at our GitHub repos for CSS Blocks and OptiCSS, where you will find documentation and fairly well-commented code.

Acknowledgments

Special thanks to Adam Miller, who has worked tirelessly with me on these projects since almost the beginning.

Thanks to our team members Sarah Clatterbuck, James Baker, Kris Baxter, Diego Buthay, Tom Dale, Chad Hietala, Casey Klimkowsky, Kacey Mack, Asa Kusuma, Mark Pascual, Marius Seritan, Mahir Shah, and Sara Todd.