This content originally appeared on DEV Community and was authored by Jamund Ferguson
The truly greatest bang-for-your-buck performance impact I ever had was removing two lines of JavaScript.
My Role at Amazon
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.
Like many parts of Amazon, our org had a strong culture of web performance. We had clearly defined web performance targets and each time they were missed, a "path to green" statement had to be written indicating how we'd get back underneath them. Amazon is also known for not letting bureaucracy get in the way of shipping code. So there was a constant battle between shipping new features and working toward our web performance goals. This is a pretty normal and healthy tension you'd expect to find in a lot of companies, but it was sometimes frustrating to work for a sprint or two on some important performance enhancement only to see it wiped away by the unintended negative performance impact of a highly anticipated new feature.
As the only front-end engineer on my team, when I started, and one of a very small number in my org, my tenure at Amazon was focused primarily on front-end architecture and web performance. I was advising my teammates on how to write maintainable front-end code and trying to come up with sustainable ways to meet our web performance goals without slowing down our ability to ship code.
Attempting to Improve Web Performance
Not surprisingly when I joined the team we were not meeting our web performance targets. Most of the team members were smart backend devs with one or two that had some recent React experience. They had been working on this app for over a year, but weren't the team that had originally built it.
One of the first things I did after getting our app running locally was to look for well-known performance wins (low hanging fruit). Do we have the optimized lodash build for webpack? Are we bundle splitting? Exactly how many fetch
polyfills do we have in our bundle? Can we create a separate build for modern browsers vs. old browsers? All of these common problems come up when building modern React and webpack apps. I'd dealt with before and thought I knew exactly what was needed to fix them. The only problem is that it wasn't enough.
As I started working on some of those improvements, we began shaving off 10kb here, 100kb there. Our JavaScript bundle was dropping from 1.8mb to 1.5mb and then even down to around 1mb. But what was frustrating is that the impact these changes had on the customer wasn't enough. That org relied heavily on Real User Monitoring to keep track of how users were experiencing web performance. All of those minor webpack optimizations just weren't making a dent with many of our users. It turns out because of our specific use case (people adding one product, then another, then another) our cache hit rate is pretty high. The long download times of our big JS bundle wasn't killing us as much as it might in other scenarios. And all of this relatively minor tweaking, just wasn't having the impact that I'd like. Given the complexities of our dynamic form it was fairly difficult to split our code into much smaller bundles without a large refactor. So I was kind of stuck.
After spending a month or two on low impact things that I had hoped were going to make me the web performance hero, none of them seemed to have the impact I had hoped for. At this point I was fairly frustrated and trying to figure out new and better ways to analyze what was & wasn't working. That's when I stumbled on the coverage tab in Chrome's web inspector.
It was the most convoluted multi-step process to find it. Press ⌘P
and then >
and then type show coverage
. I had heard about this, but I had no idea it did CSS as well or that CSS would even be a web performance bottleneck. I was so excited when I found this I even tweeted about it.
TIL about the "show coverage" option in Chrome Web Inspector 🤯 Includes both CSS and JS00:18 AM - 02 May 2020
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.
What I saw there was that over 98% of our main CSS file went unused. In addition, our CSS file was over 1mb. Honestly the way that the red and blue lines worked it took me a few double-takes to realize it was really that bad. I thought, hey okay, we're using 98% of the CSS. But no, it was unused. We were using only 2% of the included CSS. Ouch! (The CSS coverage below comes from a different website, but it follows a similar trend)
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
The approach to styling my team had been using was basically having a large Sass file with dozens of dependent stylesheets @import
ed in. Obviously that makes it really hard to figure out which CSS was needed or not needed. I spent hours scouring our CSS files for what we weren't using. Nothing actually looked so obviously wasteful, not 1mb worth of waste anyway. 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 somewher)? 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'
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, our website looked horrible after removing the CSS. Which can probably be expected. We were at least relying on something in those CSS bundles. Next I commented out each of them one at a time. For whatever reason I think we actually needed to keep the non-minified one in there to avoid breaking the look & feel of the site, but at least it was something. 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. Was it all CSS or was there going to be a JS component as well?
What Was Left
Like most teams we relied on an internal UI library that our app was already importing. It was very likely that the tiny bits of functionality we relied upon from this external library were already covered by our internal one.
An early approach I took was simply copy/pasting the whole Semantic UI library CSS into a new file and then just 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 and purposefully broke the look of the site. That made it really easy to identify which classes we actually were using. Side by side screenshots for example were very helpful.
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 ourselves 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 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. It was around half a second of reduction (at the 90th percentile). 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.
- Having web performance goals
- Don't just optimize to a checklist (Learn the tools!)
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 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.
The other realization that has become more apparent to me over time is that optimizing to a checklist doesn't cut it. 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 together 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.
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.
This content originally appeared on DEV Community and was authored by Jamund Ferguson
Jamund Ferguson | Sciencx (2021-12-13T20:00:36+00:00) One Cool Trick to Speed Up Your Website Performance (Not Really). Retrieved from https://www.scien.cx/2021/12/13/one-cool-trick-to-speed-up-your-website-performance-not-really/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.