スタイルシートのダウンロードや実行が終わるまで、ブラウザはレンダリングをブロックする。
そのため、スタイルシートの配信やダウンロードを最適化することは、ウェブサイトのパフォーマンス向上につながる。
この記事では、スタイルシートのダウンロードがレンダリングにどのような影響を与えるのかを見ていく。
なお、script
タグとレンダリングの関係については、以下の記事にまとめてある。
本記事の内容は、サーバは 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.css
は3
秒かかるようにしてある。
こうすると、HTML ファイルの表示に3
秒以上かかるようになる。
つまり、link
要素で読み込んでいるスタイルシートのダウンロードが終わらないと、レンダリングが行われないということである。
レンダリングはブロックされるが、後続のスタイルシートのダウンロードはブロックされない。
以下のようなケースで、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.css
と2.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
の内容は画面上には反映されず、印刷時にのみ反映される。
そして、画面上に影響を与えないことが分かっているスタイルシートは、レンダリングをブロックしない。
例えば、base.css
のダウンロードに2
秒、2.css
のダウンロードに5
秒かかる場合は、2.css
のダウンロードを待たず、base.css
をダウンロードした時点でレンダリングが行われる。
スタイルシートから参照された他のリソースについて
@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; }
処理の流れは以下の通り。
- HTML ファイルに書かれた
link
要素に基づいてbase.css
のダウンロードを開始する base.css
のダウンロード完了後、2.css
のダウンロードを開始する2.css
のダウンロード完了後、1.png
のダウンロードを開始する- それと同時に、レンダリングを開始する
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
秒かかる場合の実行結果である。
base.css
のダウンロードは1
秒で終わるので、その時点でレンダリングが行われる。
そこから、1.css
のダウンロードが終わるまで、8
秒かかる。その間はスタイルが適用されていない状態のp
要素が表示されてしまう。
以下のようにpreload
を使うことでも非同期に読み込めるが、やはり同様の問題は発生する。
<link rel="preload" href="/2.css" as="style" onload="this.onload=null; this.rel='stylesheet'">
そのため、非同期読み込みを利用する場合は、非同期に読み込ませるべきスタイルシートとそうでないスタイルシートを適切に区別する必要がある。