DEV Community

Jamund Ferguson
Jamund Ferguson

Posted on

One Cool Trick to Speed Up Your Website Performance (Not Really)

The truly greatest bang-for-your-buck performance impact I ever had was removing two lines of JavaScript.

My Background

When I was at Amazon, I worked in the Seller Central org building tools to help companies sell their products. The app I primarily worked on was a complex multi-part form broken into numerous tabs with dozens of inputs dynamically populated based on product type, customer characteristics and various choices made along the way. The app was built with React and Redux, and the backend was a custom Java SpringMVC-based framework.

The Problem

As a company, Amazon has a strong culture of web performance, but it also values shipping code rapidly. These competing interests resulted in friction; it could be deeply frustrating to see a month's worth of work improving page performance wiped out by an unintended negative side effect from a new feature. When I started as the sole frontend engineer on my team, and one of a handful in the org, my primary focus was on frontend architecture and web performance. It was my responsibility to come up with sustainable ways to hit those goals without compromising our ability to ship code. At the time, we were regularly missing our web performance targets. Most of the team members were smart backend devs, but few had much experience with React, or with optimizing frontend performance.

Failed Attempts

I came in, as many new hires do, wanting to be the hero who stepped in and neatly saved the day. I started by looking for the easy, high-yield performance wins: are we using an optimized lodash build for webpack? Are we bundle splitting? Exactly how many fetch polyfills do we have in our bundle? I'd worked on performance in React apps before, and I had my mental checklist ready. The problem, though, was that the low hanging fruit wasn't yielding enough actual benefit. We shaved off 10kb here, 100kb there. Our bundle size dropped from 1.8mb to 1.5mb, and eventually all the way down to just over 1mb, but we still couldn't hit our performance goals. We relied heavily on real user monitoring to understand how users experienced our site. We eventually found that due to how users interacted with our app, our cache hit rate was fairly high. The reductions to the size of our JS bundle were definitely a good thing, but they weren't giving us anywhere near the improvements in how users were experiencing our performance that we wanted. There had to be something else that could speed us up.

Breakthrough

The breakthrough came, as they sometimes do, after I'd exhausted my checklist and started exploring areas I was unfamiliar with. I was looking for new and different ways to analyze what was and wasn't working in our app, and that's when I stumbled on the coverage tab in Chrome's web inspector. Finding it is a convoluted process; it's buried two menus deep in the Chrome DevTools three-dot menu under "More Tools", or you can reach it by activating the Command Menu in DevTools with ⌘P, typing > to see other available actions, and then typing coverage. Seeing its results for a first time were a revelation and I got excited enough to tweet about it joyfully.

The Coverage tab can show you unused JS and CSS on your page. Once you get into the coverage panel, by default you'll see both JS and CSS files. But you can also filter to just CSS.

Show coverage select CSS

What I saw there was that over 98% of our main CSS file went unused. I also realized that CSS file, on its own, was over 1mb. I'd been grinding away, paring down our JS bundle to the smallest possible size, but the CSS file was right there actually having a larger impact! The CSS coverage below comes from a different website, but it follows a similar trend)

Very low CSS coverage

The Problem with Large CSS Files

While it's pretty common to discuss the downsides of large JS bundles, large CSS bundles are arguably worse! CSS is a render blocking resource which means the browser is going to wait for that CSS file to be downloaded, parsed and constructed into a CSSOM tree before rendering the contents of the page. Whereas JS files these days are usually added to the end of the <body> or included with the defer or async tags, CSS files are rarely loaded in parallel with the page render. That's why it's imperative that you keep unused CSS out of your main CSS bundle.

There has been talk for years about including only "above the fold" or critical-path CSS on initial page load, but despite several tools that can try to automate this process it's not foolproof. When it comes to just avoiding including unneeded CSS I think many would agree CSS-in-JS approaches and even CSS Modules do a better job at this compared to the ever-too-common approach of having one large Sass or LESS file that contains all of the styles anyone might ever need for your site.

Pinning Down the Problem

My team's initial approach to styling was to have a single large Sass file with dozens of dependent stylesheets @imported in. That made it quite difficult to figure out exactly what parts we were or weren't using, and I spent hours scouring our CSS files looking for unused styling. Nothing looked obviously wasteful, and I certainly couldn't find a whole extra mb of unused style. Where else could the CSS be coming from? Was it from a shared header/footer that included extra styles? Maybe a JS-based CSS import somewhere? I had to find out.

Searching through our JS code, I found only 4 or 5 CSS imports. Our webpack config made sure that all CSS imported from inside our JS files ended up bundled together in one large file. In our main JavaScript entry file (index.js), I found 2 CSS imports that looked particularly suspicious. This isn't the exact code, but it was something very similar:

import 'semantic-ui/dist/styles.min.css'
import 'semantic-ui/dist/styles.css'
Enter fullscreen mode Exit fullscreen mode

I had looked at this code and ignored it literally dozens of times. But given my new challenge to figure out where the extra CSS was coming from it stood out. Why were we importing this library at all? Did we even need it? And why were we importing it twice (both minified and non-minified)?

The first thing I did was comment out both of them. I ran npm run build and saw our CSS bundle drop from 1.25mb down to 30kb! It was ridiculous. This code was killing us. ☠️

Unfortunately, as you might predict, our website looked horrible after removing the CSS. We were relying on something in those CSS bundles. Next, I commented out each of them one at a time. Strangely, we needed to keep the non-minified one in there to avoid breaking the look & feel of the site, but at least I was making progress. We shaved off around 500kb of CSS just by removing one line.

Now began the more difficult part of removing our reliance on that UI library altogether.

What Was Left

Like many teams, we relied on an internal UI library that our app was already importing. I figured we could probably use that internal library to provide most, if not all, of the functionality we were getting from the external library.

An early approach I took was simply copy/pasting the whole built Semantic UI library CSS into a new file and then removing things we didn't need. That got me somewhere, but became increasingly difficult as the styles got more nested and complex. Eventually I removed the CSS imports completely, purposefully breaking the look of the site. That made it easy to identify which classes we were actually using. We took screenshots of the working site and then carefully compared them with the broken version.

It turns out we were primarily using three components:

  • The grid system
  • The navigation tabs
  • Modal dialogs

Once we figured which pieces of the library we were using, it was easy enough to search through our code base and see which components were relying on them. There were a lot that used the grid for example, but we had a drop-in replacement for those that only required a small class name change. In some other cases, we had to either add new CSS or move the HTML around a bit to get it to work with our other UI library. It ended up being about a month of work for a new team member to completely detach us from that external library. We carefully reviewed her work, compared before & after screenshots, and where there were minor style differences, ran it by a few team members to make sure the changes were close enough to the original to not block the change.

The Impact

After we shipped the changes we looked at our real user monitoring graphs and saw massive reductions in both our 50th and 90th percentile time to interactive measurements across the app. At the 90th percentile there was around half a second of reduction in TTI. After making so many changes that didn't seem to matter, it was so satisfying to finally have a solid performance win.

Removing that one UI library bundle probably ended up having a larger effect than any other single change I witnessed in my entire time working on web performance at Amazon.

The Takeaways

I've found it's very difficult to generalize web performance wins. How likely is it that your app is also double importing a large CSS library? You might as well check, but it's probably not happening. What I hope you take away from my experience here is the underlying factors that enabled us to find & fix this problem.

Don't just optimize to a checklist (Learn the tools!)

The easier part is process-related: you can't just optimize to a checklist. It's important to have checklists when you're working on performance work, because many apps can be improved by a straightforward, well-known list of simple improvements. You can and should leverage the work you've done in the past, and that the community around you has done to improve performance. But when you reach the end of your checklist, you need to develop the skillset to keep digging. Just because other apps you've worked on benefitted from change A or change B doesn't mean it will work in your next app. You have to understand your tools. You have to know the specific characteristics & architecture of your site. And you have to know your customers. Lighthouse probably told me early on in this process that I had too much CSS on the page. Without a clear understanding of how our CSS files were built and better tools for analysis I wasn't able to do much with that information. While checklists of common web performance mistakes can absolutely be helpful, teaching teammates how to use the tools available to analyze web performance in the specific, is much more powerful.

Have a strong web performance mandate

The other major takeaway, though, is about culture. To build performant applications, performance itself needs to be a first class KPI. I think many engineers enjoy optimizing things. It's really fun and difficult work. The results as we all know can be very inconsistent. I can't tell you how many times I promised to shave off 150ms from our experience, got that improvement when testing locally, but saw nothing or even a negative impact when the change actually went live. In many cases that can lead engineering or product managers to be weary of such promises. My org at Amazon had amazing leadership when it came to web performance. That mandate ensured that we had the buy-in we needed to keep going until we had the impact we wanted.


I don't expect this article to provide any magic bullets for those out there trying to optimize their apps, but I do hope it encourages you to keep digging until you find your own.


P.S. I want to give a shout out to my colleagues Scott Gifford and Michael Kirlin. Scott remains hugely influential engineer at Amazon in the web performance space and mentored me throughout my time there. Michael not only reviewed this article, but edited it extensively for clarity. Thank you friends!

Discussion (3)

Collapse
deathshadow60 profile image
deathshadow60

Honestly, you're being led down the garden path to failure by broken methodologies, garbage framework nonsense, and bad practices. I guarantee you're using more than double the markup needed thanks to the IDIOCY that are front-end frameworks like Semantic UI and bald faced lies like slopping presentation into the markup.

CSS and Scripting doesn't start loading in parallel until after the HTML finishes, so on top of garbage like Failwind, Bootcrap, "Non-Semantic claiming to be semantic", etc, etc, violating the separation of concerns, you end up having all of the endless pointless classes for NOTHING and endless pointless meaningless DIV slowing down the page load. And NO, throwing more code at it isn't the answer.

I'm an accessibility and efficiency consultant, and I'm CONSTANTLY having to tell clients that their choices of technologies -- like "semantic UI" -- are the root of their problems.

Look no further than their "homepage" example for proof that Semantic UI is utterly and totally devoid of semantic markup practices!

semantic-ui.com/examples/homepage....

Style slopped in the markup, dozens of separate files in both CSS and "JS for nothing" slowing the page loads due to handshaking, blocking scripts in the head, endless pointless DIV for nothing, a relative lack of proper semantics on things like menus, DIV doing heading's job, numbered headings around things that shouldn't be headings, gibberish heading orders on the things that should be headings... It is a train wreck laundry list of how to build a website. Thus their 9.34k of markup to do not even 4k's job...

Much less the 700k of CSS when sight few legitimate websites have ANY need of more than 48k of CSS in at most two files per media target.

Made all the more comical by their lack of media targets, since like all other front-end frameworks they don't know what that is, and basically say "screen users only F*** everyone else"

THEN you wonder why you're having performance problems.

NONE of these "front end frameworks" are easier, simpler, or better. No matter how many lying propagandists claim otherwise, or how deeply people not qualified to write a single blasted line of HTML run off the cliff like lemmings. ALL claims of being "easier", or "better for collaboration" and so forth are nothing but BALD FACED LIES! They are a monument to the pesky 3i of web development: Ignorance, Incompetence, and Ineptitude. You look at the code for any examples of these "frameworks" it is plainly evident the people who CREATED these systems aren't qualified to write websites, much less have the unmitigated GALL to tell others how to do so!

You want a strong performance mandate? Don't use frameworks, don't use classes to say what things look like, leverage semantic markup, selectors and combinators instead of pissing classes on everything, maintain the separation of concerns.

It's good you mentioned the "moving CSS into the markup" and "loading CSS not used on the current page" too, since both of those are at BEST placebo bullshit, at worst just plain 3i bullshit. The reason being that:

1) few if any websites should have more than 48k of CSS per media target (aka media="screen"),

2) keeping it out of the markup means you are PRE-CACHING SUB-PAGES!

Think about it. If you move above the fold style into every page, it has to be loaded on EVERY page-load and visit. Moving it out of the markup means it is cached for re-visits. loading it all ahead of time means all sub-pages are faster loading due to size. And remember, if you're dynamically generating that markup that smaller size means less overhead for the back-end, simpler code to work with on the back-end, less time packing and unpacking the markup, AND all the LINK and SCRIPT start loading sooner!

Much less since we pre-cache problems like FOUC and other things people dive for JavaScript for in terms of things like SPA? In a lot of cases could be avoided if you skipped the client side scripted trash entirely!

I guarantee you that given what you've said, your performance problems stem from you and your co-workers never having learned how to use HTML or CSS properly. That might seem harsh, but I do such efficiency/speed optimizations for a living, and I'm always coming across the same foolishness. I'd love to give you one of my front-end audits for what you have just to show you how badly you've had the rose coloured glasses slapped on.

I've written about this a lot over on Medium.

medium.com/codex/so-you-want-to-ma...

medium.com/codex/stop-fighting-the...

Collapse
xjamundx profile image
Jamund Ferguson Author

This is an epic rant and I respect that. I do want to share one anecdote that pushes back against your general sentiment, just a little. After working on the react-based add product page for a year or so and making a bunch of performance improvements I got invited to help out on another page that was primarily server rendered and used jquery for the interactivity. You know what? It was a really slow and there were almost no easy wins. What there were instead was a bunch of poor architecture choices that needed refactoring with not enough time to do it. Old school problems like not using document fragments when appending in a loop kept popping up. You don't see that anymore when using tools like react, because they manage a lot of low-level optimizations for you. I agree the modern web eco-system makes it way too easy to pull in massive dependencies that are potentially both insecure and slow. But the flip side is often dealing with a make shift framework written by burnt out co-workers long ago who put far less thought into it than even the web framework ™️ snake oil salesman of today. Pick your poison. Do you want the one found at the supermarket in the shiny bottle or some home-brewed moonshine? They'll both kill you at the end of the day.

Collapse
keeran_raaj profile image
Raj Kiran Chaudhary

Thanks a lot @xjamundx . Didn't know about coverage. Knew something new regarding performance and optimization.