惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
Security Latest
Security Latest
C
Cisco Blogs
P
Palo Alto Networks Blog
Know Your Adversary
Know Your Adversary
Project Zero
Project Zero
C
Cyber Attacks, Cyber Crime and Cyber Security
NISL@THU
NISL@THU
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
S
Secure Thoughts
P
Privacy International News Feed
V
Vulnerabilities – Threatpost
D
Docker
Google Online Security Blog
Google Online Security Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Recent Announcements
Recent Announcements
T
The Exploit Database - CXSecurity.com
G
Google Developers Blog
Schneier on Security
Schneier on Security
小众软件
小众软件
爱范儿
爱范儿
GbyAI
GbyAI
J
Java Code Geeks
T
Tailwind CSS Blog
Cisco Talos Blog
Cisco Talos Blog
The Hacker News
The Hacker News
D
DataBreaches.Net
Blog — PlanetScale
Blog — PlanetScale
TaoSecurity Blog
TaoSecurity Blog
MyScale Blog
MyScale Blog
B
Blog RSS Feed
Cyberwarzone
Cyberwarzone
有赞技术团队
有赞技术团队
Martin Fowler
Martin Fowler
C
CXSECURITY Database RSS Feed - CXSecurity.com
S
Securelist
L
Lohrmann on Cybersecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Y
Y Combinator Blog
S
Schneier on Security
Latest news
Latest news
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - 叶小钗
F
Fortinet All Blogs
M
MIT News - Artificial intelligence
PCI Perspectives
PCI Perspectives
V
V2EX
V2EX - 技术
V2EX - 技术
O
OpenAI News
W
WeLiveSecurity

CSS Wizardry

Front-End’s Missing Metric: The TBT Window Meet Your Users Where They Are with Obs.js Better Browser Caching with No-Vary-Search font-family Doesn’t Fall Back the Way You Think What Is CSS Containment and How Can I Use It? When All You Can Do Is All or Nothing, Do Nothing Obs.js: Context-Aware Web Performance for Everyone Low- and Mid-Tier Mobile for the Real World (2025) The Fastest Site in the Tour de France Making Sense of the Performance Extensibility API Why Do We Have a Cache-Control Request Header? HTML Is Not a Programming Language… Build for the Web, Build on the Web, Build with the Web Licensing Code on CSS Wizardry A Layered Approach to Speculation Rules Designing (and Evolving) a New Web Performance Score Core Web Vitals Colours The Ultimate Contract Templates for Tech Consultants: Protect Your Business and Get Paid Optimising for High Latency Environments Cache Grab: How Much Are You Leaving on the Table? blocking=render: Why would you do that?! Correctly Configure (Pre) Connections The Three Cs: 🤝 Concatenate, 🗜️ Compress, 🗳️ Cache What Is the Maximum max-age? How to Clear Cache and Cookies on a Customer’s Device The Ultimate Low-Quality Image Placeholder Technique Core Web Vitals for Search Engine Optimisation: What Do We Need to Know? The HTTP/1-liness of HTTP/2 In Defence of DOM­Content­Loaded Site-Speed Topography Remapped Why Not document.write()? Speeding Up Async Snippets Measure What You Impact, Not What You Influence Optimising Largest Contentful Paint Measuring Web Performance in Mobile Safari Site-Speed Topography Speed Up Google Fonts Real-World Effectiveness of Brotli Performance Budgets, Pragmatically Lazy Pre-Browsing with Prefetch Making Cloud.typography Fast(er) Time to First Byte: What It Is and How to Improve It Self-Host Your Static Assets Tips for Technical Interviews Cache-Control for Civilians Bandwidth or Latency: When to Optimise for Which ITCSS × Skillshare What If? CSS and Network Performance The Three Types of Performance Testing Getting to Know a Legacy Codebase Image Inconsistencies: How and When Browsers Download Images Identifying, Auditing, and Discussing Third Parties My Digital Music Setup Measuring the Hard-to-Measure Finding Dead CSS The Fallacies of Distributed Computing (Applied to Front-End Performance) Ten Years Old Relative Requirements Airplanes and Ashtrays Performance and Resilience: Stress-Testing Third Parties Refactoring Tunnels Little Things I Like to Do with Git Writing Tidy Code Configuring Git and Vim Base64 Encoding & Performance, Part 2: Gathering Data Base64 Encoding & Performance, Part 1: What’s Up with Base64? Code Smells in CSS Revisited Typography for Developers Moving CSS Wizardry onto HTTPS and HTTP/2 Ack for CSS Developers A New Year, a New Focus Preparing Vim for Apple’s Touch Bar Choosing the Correct Average CSS Shorthand Syntax Considered an Anti-Pattern CSS Wizardry Newsletter Nesting Your BEM? Improving Perceived Performance with Multiple Background Images Continue Normalising Your CSS Pure CSS Content Filter Pragmatic, Practical, and Progressive Theming with Custom Properties Refactoring CSS: The Three I’s Speaker’s Checklist: Before and After Your Talk Improving Your CSS with Parker The Importance of !important: Forcing Immutability in CSS Mixins Better for Performance Managing Typography on Large Apps White October Events Workshop Partnership BEMIT: Taking the BEM Naming Convention a Step Further Travelling Like You Want to, When You Have To Contextual Styling: UI Components, Nesting, and Implementation Detail Subtleties with Self-Chained Classes Cyclomatic Complexity: Logic in CSS Immutable CSS Can CSS Be Too Modular? More Transparent UI Code with Namespaces When to use @extend; when to use a mixin The Specificity Graph CSS Wizardry Ltd.: Year 1 in review CSS Guidelines 2.0.0
Critical CSS? Not So Fast!
Harry Roberts · 2022-09-07 · via CSS Wizardry

Written by on CSS Wizardry.

Table of Contents

Independent writing is brought to you via my wonderful Supporters.

  1. Overview
  2. Challenges of Implementing Critical CSS
  3. Identifying CSS as Your Performance Bottleneck
    1. Ensure CSS Remains Your Biggest Bottleneck
  4. You’re Only Solving the Fetch
    1. It’s a Race
  5. Moving Away From media
    1. Problems With print
    2. A Better Option
    3. Pitfalls and Concerns
  6. Debugging Critical CSS
  7. So What Am I Saying?

I have long held very strong opinions about the Critical CSS pattern. In theory, in a perfect world, with all things being equal, it’s demonstrably a Good Idea™. However, in practice, in the real world, it often falls short as a fragile and expensive technique to implement, which seldom provides the benefits that many developers expect.

Let’s look at why.

N.B. Critical CSS when defined as ‘the styles needed to render the initial viewport’.

Overview

Critical CSS is not as straightforward as we’d like, and there is a lot to consider before we get started with it. It is worth doing if:

  • CSS is your biggest blocker, or;
  • you plan to tackle everything around it at the same time;
    • i.e. other render-blocking resources;
  • it can be done trivially or from the outset;
    • retrofitting Critical CSS is difficult and error prone;
  • you maintain it and everything around it;
    • it’s all to easy to (re)introduce render-blocking regressions;
  • you load the non-Critical CSS sensibly;
    • current methods can be no better than just leaving your CSS as-is.

Challenges of Implementing Critical CSS

…particularly when we talk about retrofitting it. Reliably extracting the relevant ‘critical’ styles is based, first and foremost, on some brittle assumptions: what viewport (or fold, remember that?) do we deem critical? How do we treat off-screen or un-interacted elements (think dropdown or flayout navs, etc.)? How do we automate it?

Honestly, in this scenario, my advice is almost always: don’t bother trying to retrofit Critical CSS—just hash-n-cache1 2 the living daylights out of your existing CSS bundles until you replatform and do it differently next time.

Implementing Critical CSS on a brand new project becomes markedly easier, especially with the correct3 CSS-in-JS solution that bundles and componentises CSS by default, but that still doesn’t guarantee it will be any faster.

Let’s look at the performance implications of getting Critical CSS right.

Identifying CSS as Your Performance Bottleneck

Critical CSS only helps if CSS is your biggest render-blocking bottleneck, and quite often, it isn’t. In my opinion, there is often a large over-focus on CSS as the most important render-blocking resource, and people often forget that any synchronous work at all in the <head> is render blocking. Heck, the <head> itself is completely synchronous. To that end, you need to think of it as optimising your <head>, and not just optimising your CSS, which is only one part of it.

Let’s look at a demo in which CSS is not the biggest render-blocking resource. We actually have a synchronous JS file that takes longer than the CSS does4:

<head>

  <link rel="stylesheet"
        href="/app.css"
        onload="performance.mark('css loaded')" />

  <script src="/app.js"></script>

  <script>performance.mark('head finished')</script>

</head>

When we view a waterfall of this simple page, we see that both the CSS and JS are synchronous, render-blocking files. The CSS arrives before the JS, but we don’t get our Start Render (the first of the two vertical green lines) until the JS has finished. The CSS still has a lot of headroom—it’s the JS that’s pushing out Start Render.

N.B. The following waterfalls have two vertical purple bars. Each of these represents a performance.mark() that signifies the completed downloading of the CSS or the end of the <head>. Pay attention to where they land, and if they sit on top of either each other or anything else.

Note that the CSS file is marked as blocking (see the orange cross), and thus carries Highest priority and hits the network first.

If we were to implement Critical CSS on this page by:

  1. inlining the above-the-fold CSS, and;
  2. asynchronously/lazily loading the remainder of the CSS…
<head>

  <style id="critical-css">
    h1 { font-size: calc(72 * var(--slow-css-loaded)); }
  </style>

  <link rel="stylesheet"
        href="/non-critical.css"
        media="print"
        onload="performance.mark('css loaded'); this.media='all'" />

  <script src="/app.js"></script>

  <script>performance.mark('head finished')</script>

</head>

…we’d see absolutely no improvement! And why would we? Our CSS wasn’t holding back Start Render, so making it asynchronous will have zero impact. Start Render remains unchanged because we tackled the wrong problem.

Note that the CSS is now fetched as a non-blocking, Lowest priority request, and hits the network after the JavaScript.

In both cases—‘Blocking’ and ‘Critical CSS’ respectively—Start Render came in at exactly the same time. Critical CSS made no difference:

Both of the above exhibit the same visual behaviour because the CSS was never the problem anyway—it is the JavaScript that is blocking rendering.

In a reduced test case like this, it’s blindingly obvious that Critical CSS is a wasted effort. We only have two files to focus on, and they’re both being artificially slowed down to force the output that helps prove my point. But the exact same principles carry through to real websites—your websites. With many different potentially-blocking resources in-flight at the same time, you need to be sure that it’s your CSS that’s actually the problem.

In our case, CSS was not the bottleneck.

Let’s take a look at what would happen if the CSS was our biggest blocker:

Again, both files are render blocking. However, note that both purple lines sit on top of each other—css loaded and head finished are synonymous.

Above, we can clearly see that CSS is the asset type pushing out our Start Render. Does moving to Critical CSS—inlining the important stuff and loading the rest asynchronously—make a difference?

Now, head finished and Start Render are identical; css loaded is later. It worked!

We can see now that Critical CSS has helped! But all it’s really served to do is highlight the next issue—the JS. That’s what we need to tackle next in order to keep making steps in the right direction.

Note the change in font-size. More on this phenomenon later.

Ensure CSS is actually the thing holding you back before you start optimising it.

Ensure CSS Remains Your Biggest Bottleneck

This all seems quite obvious: don’t optimise CSS if it’s not a problem. But what presents a slightly more pernicious issue are the regressions that can happen after you successfully implement Critical CSS…

If you do identify that CSS is your biggest bottleneck, you need to keep it that way. If the business approves the time and money for the engineering effort to implement Critical CSS, you can’t then let them drop a synchronous, third-party JS file into the <head> a few weeks later. It will completely moot all of the Critical CSS work! It’s an all-or-nothing thing.

Honestly, I cannot stress this enough. One wrong decision can undo everything.

You’re Only Solving the Fetch

The next problem is with splitting the application of CSS into two parts.

When you use the media-switching pattern5 to fetch a CSS file asynchronously, all you’re doing is making the network time asynchronous—the runtime is still always a synchronous operation, and we need to be careful not to inadvertently reintroduce that overhead back onto the Critical Path.

By switching from an asynchronous media type (i.e. media=print) to a synchronous media type (e.g. media=all) based on when the file arrives, you introduce a race condition: what if the file arrives sooner than we expected? And gets turned back into a blocking stylesheet before Start Render?

It’s a Race

Let’s take some very exaggerated but very simple math:

If it takes 1s to parse your <head> and 0.5s to asynchronously fetch your non-Critical CSS, then the CSS will be turned back into a synchronous file 0.5s before you were ready to go anyway.

We’ve fetched the file asynchronously but had zero impact on performance, because anything synchronous in the <head> is render-blocking by definition. We’ve achieved nothing. The fetch being asynchronous is completely irrelevant because it happened during synchronous time anyway. We want to ensure that the non-Critical styles are not applied during—or as part of—a blocking phase.

How do we do that?

One option is to ditch the media-switcher altogether. Let’s think about it: if our non-Critical styles are not needed for Start Render, they don’t need to be render blocking—they didn’t ought to be in the <head> at all.

The answer is surprisingly simple: Rather than trying to race against our <head> time, let’s move the non-Critical CSS out of the <head> entirely. If we move CSS out of the <head>, it no longer blocks rendering of the entire page; it only blocks rendering of subsequent content.

Why would we ever put non-Critical CSS in the <head> in the first place?!

Problems With print

As a brief aside…

Another problem we have is that CSS files requested with media=print get given Lowest priority, which can lead to too-slow fetch times. You can read more about that in a previous post.

Even though the CSS is non-Critical, waiting over 12s is unacceptable.

By adopting the following method for non-Critical CSS, we also manage to circumvent this issue.

A Better Option

Rather than having a racy and nondeterministic method of loading our non-Critical CSS, let’s regain some control. Let’s put our non-Critical CSS at the </body>:

<head>

  <style id="critical-css">
    h1 { font-size: calc(72 * var(--slow-css-loaded)); }
  </style>

  <script src="/app.js"></script>

  <script>performance.mark('head finished')</script>

</head>
<body>

  ...


  <link rel="stylesheet"
        href="/non-critical.css"
        onload="performance.mark('css loaded')" />
</body>

What happens now?

Note a large gap between Start Render and Visually Complete. More on that in the next section.

Start Render is the fastest it’s ever been! 2.1s. We must have beaten the race condition. Nice!

Pitfalls and Concerns

There are a few things to be wary of with the </body> method.

Firstly, because the stylesheet is defined so late, it, naturally, gets requested quite late. For the most part, this is exactly what we want, but in the event that it’s too late, we could lean on Priority Hints to help out.

Secondly, because HTML is parsed line-by-line, the stylesheet will not be applied to the page until the parser actually gets to it. This means that from the point of applying the in-<head> Critical CSS to the non-Critical CSS at the </body>, the page will be mostly unstyled. This means that if a user scrolls, there is a strong possibility they might see a flash of unstyled content (FOUC), and the chance of Layout Shifts increases significantly. This is especially true if someone links directly to an in-page fragment identifier.

Further, even if the non-Critical CSS comes from HTTP cache very, very quickly, it will only ever be applied as slowly as the HTML is parsed. In effect, </body> CSS is applied around the DOMContentLoaded event. That’s kinda late. This means that speeding up the file’s fetch is unlikely to help it be applied to the document any sooner. This could lead to lots of dead, unstyled time, and the issue only gets worse the larger the page. You can see this in the screenshot above: Start Render is at 2.1s, but the non-Critical CSS is applied at 2.9s. Your mileage will vary, but the best advice I have here is to make very, very sure that your non-Critical styles do not change anything above the fold.

Finally, you’re effectively rendering the page twice: once with Critical CSS, and a second time with Critical CSS plus non-Critical CSS (the CSSOM is cumulative, not additive). This means your runtime costs for Recalculate Style, Layout, and Paint will increase. Perhaps significantly.

It’s important to make sure that these trade-offs are worth it. Test everything.

Debugging Critical CSS

If we’re battling through all of this—and it is a battle—how do we know if Critical CSS is actually working?

Honestly, the simplest way I’ve found to work out—locally, at least—if Critical CSS is working effectively is to do something that will visually break the page if Critical CSS works correctly (it sounds counter-intuitive, but it’s the simplest to achieve).

We want to make sure that asynchronous CSS isn’t applied at Start Render. It needs to be applied any time after Start Render, but before the user scrolls down enough to see a FOUC. To that end, add something like this to your non-Critical CSS file:

* {
  color: red !important;
}

The best techniques are always low-fidelity. And almost always use an !important.

If your first paint is all red, we know the CSS was applied too soon. If the first paint is not red, and turns red later, we know the CSS was applied sometime after first paint, which is exactly what we want to see.

This is what the change in font-size that I mentioned earlier was designed for. The reason I didn’t change color is because Slowfil.es only provides one CSS declaration that I can apply to the page. The principle is still the exact same.

So What Am I Saying?

There’s a lot to consider in this post, so to recap:

  • Generally, don’t bother retrofitting Critical CSS.
    • If you want to, make sure it’s the right thing to focus on.
    • If you manage it, you really need to maintain it.
  • Make sensible choices about your CSS for new projects.
    • A good CSS-in-JS solution should handle most of it.
  • Don’t turn your non-Critical CSS back into a synchronous resource.
    • The media=print hack is pretty flawed.
    • Move non-Critical CSS out of the <head> entirely.
    • Place your non-Critical CSS at the </body>.
  • Be very, very certain that your non-Critical CSS doesn’t (re)style anything above the fold.
  • Generally, don’t bother retrofitting Critical CSS.

Many thanks to Ryan Townsend and Andy Davies for proofreading.



Frequently Asked Questions

Is Critical CSS worth implementing?

Critical CSS can improve site speed, but only if CSS is genuinely your biggest performance bottleneck and it is maintained properly. Otherwise, the complexity may not justify the performance gains.