クリティカルCSSインライン化の効果を実装して確認してみた

# はじめに
クリティカルCSS（ファーストビューで必要なCSS）をHTMLファイルにインライン化する方法があります。本記事ではこの方法によるパフォーマンスへの影響を確認してみます。


# ゴール
クリティカルCSSをHTMLファイルにインライン化したパターンと外部ファイルとして読み込むパターンにおいて、Largest Contentful Paint (LCP)の指標を比較し、パフォーマンスの違いを比較してみる。


# 環境
express：4.21.2

# ディレクトリ構造

```
critical-css-demo/
├── package.json
├── server.js
└── src/
    ├── inline/
    │   ├── index.html
    │   └── styles.css
    └── external/
        ├── index.html
        └── styles.css
```

# クリティカルCSSとは？
 > キーワード:クリティカル CSS は、ユーザーにコンテンツをできるだけ早く表示するために、スクロールせずに見える範囲のコンテンツの CSS を抽出する手法です。
 引用：https://web.dev/articles/extract-critical-css
 
抽出したクリティカルをHTMLにインライン化して記載する事で、ファーストビューで使用するCSSのリクエストがなくなり、ページの表示を早くできます。


# インライン化パターン

クリティカルCSSをHTMLにインライン化して記載してみます。

```html:index.html
<!DOCTYPE html>
<html>
<head>
    <title>Critical CSS Demo - Inline Version</title>
    <style>
        .first-view {
            height: 100vh;
            background: linear-gradient(45deg, #1a2a6c, #b21f1f);
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            color: white;
        }

        .first-view-text {
            font-size: 1.5rem;
            margin: 2rem 0;
        }

        .cta-button {
            padding: 1rem 2rem;
            font-size: 1.2rem;
            background: #fff;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            transition: transform 0.3s;
        }
    </style>
    <link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
    <noscript><link rel="stylesheet" href="styles.css"></noscript>
</head> 
<body>
    <div class="first-view">
        <h1>サンプルテキスト</h1>
        <p class="first-view-text">ほげほげテキスト</p>
        <button class="cta-button">サンプルボタン</button>
    </div>
    
    <div class="content">
        <div class="card">
            <h2>カード1</h2>
            <p>スクロールして表示されるコンテンツ</p>
        </div>
        <div class="card">
            <h2>カード2</h2>
            <p>これも初期ビューポート外</p>
        </div>
    </div>
</body>
</html>
```

参考：
- preload：https://web.dev/articles/preload-critical-assets#preloading_css_files
- noscript：https://web.dev/articles/defer-non-critical-css#optimize

</details>

<details><summary>ファーストビュー部分以外のCSS</summary>

```css:styles.css
.cta-button:hover {
  transform: scale(1.05);
}

.content {
  padding: 4rem 2rem;
  max-width: 1200px;
  margin: 0 auto;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 2rem;
}

.card {
  padding: 2rem;
  background: #f5f5f5;
  border-radius: 10px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  transition: transform 0.3s;
}

.card:hover {
  transform: translateY(-5px);
}
```

</details>

# 外部CSS参照パターン

ファーストビューで使用するCSSは外部ファイルに記載します。

```html:index.html
<!DOCTYPE html>
<html>
<head>
    <title>Critical CSS Demo - External Version</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <div class="first-view">
        <h1>サンプルテキスト</h1>
        <p class="first-view-text">ほげほげテキスト</p>
        <button class="cta-button">サンプルボタン</button>
    </div>
    
    <div class="content">
        <div class="card">
            <h2>カード1</h2>
            <p>スクロールして表示されるコンテンツ</p>
        </div>
        <div class="card">
            <h2>カード2</h2>
            <p>これも初期ビューポート外</p>
        </div>
    </div>
</body>
</html>
```

<details><summary>styles.css</summary>

```css:styles.css
.first-view {
  height: 100vh;
  background: linear-gradient(45deg, #1a2a6c, #b21f1f);
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  color: white;
}

.first-view-text {
  font-size: 1.5rem;
  margin: 2rem 0;
}

.cta-button {
  padding: 1rem 2rem;
  font-size: 1.2rem;
  background: #fff;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  transition: transform 0.3s;
}

.cta-button:hover {
  transform: scale(1.05);
}

.content {
  padding: 4rem 2rem;
  max-width: 1200px;
  margin: 0 auto;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 2rem;
}

.card {
  padding: 2rem;
  background: #f5f5f5;
  border-radius: 10px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  transition: transform 0.3s;
}

.card:hover {
  transform: translateY(-5px);
}
```
</details>


# サーバ側

 expressでサーバを準備します。

```javascript:server.js
import express from 'express';

const app = express();
const port = 3000;

app.use(express.static('src'));

app.listen(port, () => {
    console.log(`
サーバー起動\n
- インライン化パターン: http://localhost:${port}/inline/
- 外部CSS参照パターン: http://localhost:${port}/external/
    `);
});
```


# 動作を確認してみる

サーバを立ち上げてブラウザからページを確認します。

```
npm start
```

ブラウザでページを開けたので DevToolsでLCPを確認してみます。

ネットワーク速度の違いでどれくらいパフォーマンスが変わるのか興味があったので、スロットリングさせて確認してみました。

:::note info
**スロットリングについて**

DevToolsのNetworkタブにある項目で、ネットワーク速度を制限して検証できる機能を指しています。
参考：https://developer.chrome.com/docs/devtools/network/reference?hl=ja#throttling

:::

ここでは測定結果を確認したものを一例として記載しています。

**▼ スロットリングなし**

- 外部CSS参照パターン（左画像）：0.06s
- CSSをインライン化したパターン（右画像）：0.06s

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1247619/ebb52329-a925-3b2d-2612-9cfccbd428df.png)

スロットリングさせてない状態だとLCP、FCPに差はそんなに見られませんでした。

**▼ スロットリングあり**

続いてネットワークを高速4Gにスロットリングさせて比較してみます。
- 外部CSS参照パターン（左画像）：0.42s
- CSSをインライン化したパターン（右画像）：0.24s

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1247619/ef6d77d9-9f86-70bb-3dda-965d93a2d952.png)

 外部CSS参照パターンでは、styles.cssがレンダリングのブロッカーになっています。

ネットワークを低速4Gにスロットリングさせて比較してみます。
- 外部CSS参照パターン（左画像）：1.22s
- CSSをインライン化したパターン（右画像）：0.64s

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1247619/bc4763ae-5840-cba8-27e0-44774c1b5a21.png)


ネットワークを3Gにスロットリングさせて比較してみます。
- 外部CSS参照パターン（左画像）：4.11s
- CSSをインライン化したパターン（右画像）：2.11s

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1247619/32647faf-c327-5d98-6f53-2b52de0d3cfc.png)

CSSをインライン化パターンでは、ファーストビューに必要なCSSがHTMLに直接埋め込まれているため、外部CSSファイルの読み込みを待つ必要がありません。よってstyles.cssがブロッカーにならずFCP、LCPの時間短縮されています。
低速なネットワーク環境で差がみられるのでモバイルユーザーの体験改善に有効かと思いました。

:::note info

web.devではLCPが2.5s以内を良好なスコアとしこれに収めることを推奨しています。

- 良好な LCP スコアとは：
https://web.dev/articles/lcp?hl=ja#what-is-a-good-lcp-score

:::


# クリティカルCSSを作成するツール

クリティカルCSSを抽出するツールとして以下のようなものがあります。

- Critical Path CSS Generator

https://jonassebastianohlsson.com/criticalpathcssgenerator/

  
- critical

https://www.npmjs.com/package/critical

参考：https://web.dev/articles/extract-critical-css#criticalcss

# 参考


https://web.dev/articles/lcp?hl=ja#what-is-a-good-lcp-score


https://web.dev/articles/extract-critical-css

https://web.dev/articles/preload-critical-assets#preloading_css_files

https://web.dev/articles/defer-non-critical-css