30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

ブラウザのレンダリングとスタイルシートについて

スタイルシートのダウンロードや実行が終わるまで、ブラウザはレンダリングをブロックする。
そのため、スタイルシートの配信やダウンロードを最適化することは、ウェブサイトのパフォーマンス向上につながる。
この記事では、スタイルシートのダウンロードがレンダリングにどのような影響を与えるのかを見ていく。

なお、scriptタグとレンダリングの関係については、以下の記事にまとめてある。

numb86-tech.hatenablog.com

本記事の内容は、サーバは Node.js のv14.13.0、クライアントは Google Chrome のv86.0.4240.111で動作確認している。

スタイルシートによる、レンダリングのブロック

まず、以下の 3 つの HTML を用意する。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="/base.css">
</head>
<body>
  <div>
    <a href="/">top</a><br>
    <a href="/red">red</a><br>
    <a href="/blue">blue</a><br>
    This page is top.
  </div>
  <hr>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="/base.css">
</head>
<body>
  <div>
    <a href="/">top</a><br>
    <a href="/red">red</a><br>
    <a href="/blue">blue</a><br>
    This page is red.
  </div>
  <hr>
  <p id="abc">abc</p>
  <p id="xyz">xyz</p>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="/base.css">
</head>
<body>
  <div>
    <a href="/">top</a><br>
    <a href="/red">red</a><br>
    <a href="/blue">blue</a><br>
    This page is blue.
  </div>
  <hr>
  <p id="abc">abc</p>
  <p id="xyz">xyz</p>
</body>
</html>

base.cssは取り敢えず、空のファイルにしておく。つまり、スタイルに影響を与えない。

これら 4 つのファイルを、以下のコードで配信する。

const http = require('http');
const fs = require('fs');

http.createServer((req, res) => {
  switch(true) {
    case /^\/$/.test(req.url):
      fs.readFile('./index.html', 'utf-8', (err, data) => {
        res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
        res.write(data);
        res.end();
      })
      break;
    case /^\/red$/.test(req.url):
      fs.readFile('./red.html', 'utf-8', (err, data) => {
        res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
        res.write(data);
        res.end();
      })
      break;
    case /^\/blue$/.test(req.url):
      fs.readFile('./blue.html', 'utf-8', (err, data) => {
        res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
        res.write(data);
        res.end();
      })
      break;
    case /^\/base\.css$/.test(req.url):
      fs.readFile('./base.css', 'utf-8', (err, data) => {
        setTimeout(() => {
          res.writeHead(200, {'Content-Type': 'text/css'});
          res.write(data);
          res.end();
        }, 3000);
      })
      break;
    default:
      res.writeHead(404);
      res.end();
  };
}).listen(8080);

HTML ファイルは即座に返すが、base.css3秒かかるようにしてある。

こうすると、HTML ファイルの表示に3秒以上かかるようになる。
つまり、link要素で読み込んでいるスタイルシートのダウンロードが終わらないと、レンダリングが行われないということである。

f:id:numb_86:20201104142710g:plain

レンダリングはブロックされるが、後続のスタイルシートのダウンロードはブロックされない。
以下のようなケースで、base.cssのダウンロードに3秒かかり、1.cssのダウンロードに1秒かかるとする。
その場合、base.cssのダウンロード開始の直後に1.cssのダウンロードが始まる。base.cssのダウンロードが終わるまで1.cssのダウンロードが始まらない、ということにはならない。

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="/base.css">
  <link rel="stylesheet" href="/1.css">
</head>

但し、全てのスタイルシートのダウンロードが終わらないとレンダリングは行われないので、最低でも3秒経過しないと(base.cssのダウンロードが終わらないと)、レンダリングは行われない。

media 属性を正しく使ってレンダリング速度を改善する

link要素にはmedia属性を指定することが可能で、これを使うことで、スタイルを適用する対象を指定できる。
例えばmedia="print"とすると、そのスタイルシートは印刷時にのみ適用される。

以下のbase.css2.cssを用意した上で、それをblue.htmlで読み込ませてみる。

/* base.css */
a {
  color: deepskyblue;
}
/* 2.css */
p {
  color: blue;
}
<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="/base.css">
  <link rel="stylesheet" href="/2.css" media="print">
</head>

そうすると、2.cssの内容は画面上には反映されず、印刷時にのみ反映される。

f:id:numb_86:20201104142958p:plain

f:id:numb_86:20201104143012p:plain

そして、画面上に影響を与えないことが分かっているスタイルシートは、レンダリングをブロックしない。
例えば、base.cssのダウンロードに2秒、2.cssのダウンロードに5秒かかる場合は、2.cssのダウンロードを待たず、base.cssをダウンロードした時点でレンダリングが行われる。

f:id:numb_86:20201104142756g:plain

スタイルシートから参照された他のリソースについて

@importを使うと他のスタイルシートを参照することができる。
その場合、参照されたスタイルシートのダウンロードも、レンダリングをブロックすることになる。

以下のスタイルシートをlink要素で読み込んでいる場合、まずbase.cssがダウンロードされ、それが終わると1.cssがダウンロードされる。
そして、1.cssのダウンロードが完了するまで、レンダリングは行われない。

/* base.css */
@import url('/1.css');

画像の参照については、レンダリングをブロックしない。そのため画像のダウンロードに時間がかかる場合、画像が反映される前のコンテンツが表示されてしまう可能性がある。

以下のbase.cssを HTML で読み込んだ場合、2.cssのダウンロードが終わった時点で、レンダリングと1.pngのダウンロードが開始される。
1.pngのダウンロードに1秒かかるようにしてあるため、その間は、背景画像がない状態で表示される。

/* base.css */
@import url('/2.css');

a {
  color: deepskyblue;
}

div {
  background-image: url('/1.png');
}
/* 2.css */
p {
  color: blue;
}

f:id:numb_86:20201104143242g:plain

処理の流れは以下の通り。

  1. HTML ファイルに書かれたlink要素に基づいてbase.cssのダウンロードを開始する
  2. base.cssのダウンロード完了後、2.cssのダウンロードを開始する
  3. 2.cssのダウンロード完了後、1.pngのダウンロードを開始する
  4. それと同時に、レンダリングを開始する
  5. 1.pngのダウンロードが完了し、画面に反映される

script 要素と DOMContentLoaded イベント

DOMContentLoadedという、パースが完了したときに発生するイベントがある。
このイベントの発生タイミングにも、スタイルシートは影響を与える。さらに、script要素の有無によっても、挙動が変化する。

script タグがない場合

HTML のパースが終わった時点で、DOMContentLoadedが発生する。スタイルシートのダウンロードが終わっていなくても関係ない。
但し画面のレンダリングについては、既述したようにスタイルシートのダウンロードが終わってから行われる。

<head><script><link></head>

head要素内にscript要素とlink要素があり、scriptが先にある場合。
スクリプトのダウンロードと実行が終わった時点で、DOMContentLoadedが発生する。スタイルシートのダウンロードが終わっているかどうかには、左右されない。
レンダリングは、両方のダウンロードと実行が終わった段階で行われる。

<head><link><script></head>

先程とは逆に、head要素のなかでlink要素が先にある場合。
必ず、スタイルシートのダウンロードが終わってから、スクリプトを実行する。スクリプトのダウンロードが先に終わっていた場合は、実行を保留する。
そしてスクリプトの実行が終わってからDOMContentLoadedが発生し、レンダリングもそのタイミングで行われる。
スタイルシートのダウンロードが先に終わっていたとしてもレンダリングは行われず、必ず、スクリプトの実行が完了するのを待つ。

<head><link></head><body><script></body>

head要素にはscript要素がなく、body要素のなかにある場合。
スタイルシートのダウンロードが先に終わった場合は、その時点でレンダリングが行われる。その後、スクリプトのダウンロードと実行が終わった時点で、DOMContentLoadedが発生する。
スクリプトのダウンロードが先に終わった場合は、スタイルシートのダウンロードが終わるまで、実行を保留する。スタイルシートのダウンロードが終わった段階でスクリプトを実行し、実行完了のタイミングでDOMContentLoadedやレンダリングが発生する。

スタイルシートの非同期読み込み

スタイルシートを非同期に読み込ませることで、レンダリングのブロックを防ぐことができる。
しかし注意しないと、スタイルが適用される前のコンテンツが表示されてしまう可能性がある。

非同期読み込みの方法はいくつがある。
例えば、以下のように JavaScript で動的にlink要素を追加すると、非同期で読み込まれるようになる。

const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '/1.css';
document.head.appendChild(link);

1.cssのダウンロードに時間がかかる場合、その間、1.cssで指定されているスタイルは適用されない。
状況によっては、JavaScript のダウンロードや実行を待っている間も、スタイルが適用されていないコンテンツが表示される可能性がある。

以下は、script.jsのダウンロードに4秒、そしてscript.js内の「link要素追加」のコードが実行されるまでに3秒かかり、1.cssのダウンロードに2秒かかる場合の実行結果である。

f:id:numb_86:20201104143323g:plain

base.cssのダウンロードは1秒で終わるので、その時点でレンダリングが行われる。
そこから、1.cssのダウンロードが終わるまで、8秒かかる。その間はスタイルが適用されていない状態のp要素が表示されてしまう。

以下のようにpreloadを使うことでも非同期に読み込めるが、やはり同様の問題は発生する。

<link rel="preload" href="/2.css" as="style" onload="this.onload=null; this.rel='stylesheet'">

f:id:numb_86:20201104143420g:plain

そのため、非同期読み込みを利用する場合は、非同期に読み込ませるべきスタイルシートとそうでないスタイルシートを適切に区別する必要がある。