こんにちは。アウモ株式会社でインターンをしておりますイワミ(@B_Sardine)と申します。aumo では主に記事メディアを中心とした開発をしています。
前回の記事のに引き続き今回は第三弾ということで、CriticalCSS の導入について述べていこうと思います。
目次
Critical CSS とは?
前回のCLS についての話でも触れましたが、基本的にCLS 以外の Core Web Vitals において、ユーザーがサイトを訪れた際スクロールせずに見える範囲である Above the fold におけるパフォーマンスは非常に重要です。いかに早くこの領域を描画することができるかが UX 及びパフォーマンススコアに関わってきます。これを妨げる要素として、レンダリングブロックがあります。
レンダリングブロックとは、<head>
タグ内で外部の CSS や JavaScript を指定すると、その読み込みが終わるまでレンダリングが行われないというものです。
これの対策としては、Critical CSS を導入しました。
Critical CSSとは…
- Above the fold を描画するのに必要な CSS 要素のみを抜き出し、
<head>
でインライン化して読み込み - それ以外の CSS は非同期読み込み
という手法です。
レンダリングブロックを避けるためには <head>
で style を読まないのが一番ですが、そうすると Above the fold にスタイルが当たらないまま描画されてしまいます(JavaScript は <body>
の最後で読み込むなどありますが、CSS ではそれができません)。そこで、この Above the fold の部分に必要な CSS のみを抽出し、それだけを <head>
で読み込もうというのが CriticalCSS です。利点としては以下のようなものがあります。
<head>
で読み込む CSS を最小限にできるため、レンダリングブロックを減らせる- インラインで埋め込むことで外部 CSS を初期描画時にリクエストして取得する必要がなくなる
実装
今回は、Google のエンジニアリングマネージャーでもある Addy Osmani 氏の addyosmani/critical を使用しました。
流れとしては以下です。
- gulpfile などで
critical
を読み込み - Above the fold を検出すための html・CSS を入力する
- Above the fold で必要な CriticalCSS が出力される
- CriticalCSS をインラインで読み込む
critical の使用例
addyosmani/critical の README に記載されている Usage は以下のようになります。
const critical = require('critical');
critical.generate({
// Inline the generated critical-path CSS
// - true generates HTML
// - false generates CSS
inline: true,
// Your base directory
base: 'dist/',
// HTML source
html: '<html>...</html>',
// HTML source file
src: 'index.html',
// Your CSS Files (optional)
css: ['dist/styles/main.css'],
// Viewport width
width: 1300,
// Viewport height
height: 900,
// Output results to file
target: {
css: 'critical.css',
html: 'index-critical.html',
uncritical: 'uncritical.css',
},
// Extract inlined styles from referenced stylesheets
extract: true,
// ignore CSS rules
ignore: {
atrule: ['@font-face'],
rule: [/some-regexp/],
decl: (node, value) => /big-image\.png/.test(value),
},
});
この例では、インラインで埋め込みたい対象のhtml と CritiacalCSS を作成する元となる CSS を入力し、そのまま埋め込まれた状態の html である index-critical.html や critical css, uncritical cssを出力しています。
width と height は、CriticalCSS を抽出する際に対象とする Above the fold のサイズを指定しています。
aumo での使用例 – 生成 –
aumo での実装において一点、例とは違うつまずいたポイントとして、Critical
の基本使用例では 静的な HTML などを入力とするように書かれている点です。
aumo の記事サイトは Ruby on Rails で作成しているのですが、SSR なのでページ全体の静的な HTML などが存在しません。そこで、開発時のlocalhost に対してのリンクを入力として与え、オプションとして userAgent
を与えることで、PC, SP の HTML を取得、それを CriticalCSS に引数として入れて生成を行いました。
# build critical css function
critical_css_build = (device) ->
# check base url
base_url_check = (cb_1,cb_2) ->
request "http://[::1]:#{RAILS_PORT}", (err, res, body) ->
if err == null
ipv6_flag = true
cb_1(ipv6_flag,cb_2)
else
ipv6_flag = false
cb_1(ipv6_flag,cb_2)
# set base url
base_url_set = (ipv6_flag,cb_2) ->
if ipv6_flag
base_url = "http://[::1]:#{RAILS_PORT}"
cb_2(base_url)
else
base_url = "http://127.0.0.1:#{RAILS_PORT}"
cb_2(base_url)
# generate critical css
generate_critical_css = (base_url) ->
# set general params
params = {
base: "#{ASSETS_DIR}",
css: ["stylesheets/style-#{device}.css"],
minify: true,
}
# set device params
if device == 'sp'
params.width = 400
params.height = 800
params.userAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1"
else if device == 'pc'
params.width = 1920
params.height = 1080
# for root
params.src = base_url
params.target = {css: "stylesheets/critical-root-#{device}.css"}
params.target = {uncritical: "stylesheets/uncritical-root-#{device}.css"}
critical.generate params
# for article pages
params.src = base_url + "/articles/575454"
params.target = {css: "stylesheets/critical-#{device}.css"}
params.target = {uncritical: "stylesheets/uncritical-#{device}.css"}
critical.generate params
# main stream
base_url_check(base_url_set,generate_critical_css)
CriticalCSS を参照する際の記事は適当に選択しました。また、今回は記事ページとトップページにおいて適用することを考えていたため、それぞれ分けて生成しました。
細かいつまずきとして、CriticalCSS 生成の際の localhost ですが、自分は Rails のサーバーをオプションコマンドなしで起動していました。
bundle exec rails s
これで Chrome などのブラウザを用いて http://localhost:{指定したport}/ でアクセスできると思います。一方、Gulpfile の CriticalCSS 生成部分でも同様に http://localhost:{指定したport}/ を指定していました。これで localhost で立てたサーバーを Gulp が読んでくれ、それをもとに CriticalCSS を生成できるはずでした。しかし、Gulpが一向に localhost を読んでくれません。
実は、Rails サーバーはIPを指定せずに起動した場合のデフォルトの localhost は IPv4 の 127.0.0.1 ではなく、IPv6 の [::1] で起動しているようでした。対してGulp は IPv4 のlocalhost (127.0.0.1)を見ていたのでした。(Rails 5.2.3 を使用)
なので、 Gulp 側での指定先を http://[::1]:{指定したport}/ にするか、 Rails を以下のように起動する必要がありました。
bundle exec rails s -b 0.0.0.0
これで無事、Gulp から Rails サーバーのファイルを読むことができました。しかし、この先自分以外もこのトラブルに引っかかる可能性があったので、CriticalCSS の Task を走らせた際に IPv4 なのか IPv6 なのかを判別するようにしました。
# check base url
base_url_check = (cb_1,cb_2) ->
request "http://[::1]:#{RAILS_PORT}", (err, res, body) ->
if err == null
ipv6_flag = true
cb_1(ipv6_flag,cb_2)
else
ipv6_flag = false
cb_1(ipv6_flag,cb_2)
IPv6 の localhost である http://[::1]:#{RAILS_PORT} で試しにリクエストを行い、問題ない場合は IPv6 で、エラーが返ってくる場合は IPv4 の 127.0.0.1 で CriticalCSS の Task を行いました。
aumo での使用例 – インライン展開 –
これで critical-sp.css みたいなファイルが生成されました。このまま <head>
でこの css を同期読み込みし、その他を非同期で読み込むとしてもいいですが、今回は本来の CriticalCSS に従うべくインライン展開します。
とは言っても、ファイルを展開して <style>
に入れる helper を作り、slim 上で呼び出してあげるだけです。
asset_helper.rb
def gulp_sp_critical_root_asset_inline_style_tag(file)
if Rails.env.production? or Rails.env.staging1? or Rails.env.qa? or Rails.env.internal?
path = SP_CRITICSL_ROOT_REV_MANIFEST[file]
c = File.open(Rails.root.to_s + "/public/#{assets_dir}/stylesheets/#{path}").read
else
c = File.open(Rails.root.to_s + "/public/#{assets_dir}/stylesheets/#{file}").read
end
content_tag :style, c
end
_html_head.slim
- if current_page?(root_path)
= gulp_sp_critical_root_asset_inline_style_tag('critical-root-sp.css')
- elsif @article
= gulp_sp_critical_asset_inline_style_tag('critical-sp.css')
- else
= stylesheet_link_tag gulp_sp_asset_path('style-sp.css'), media: 'all', 'data-turbolinks-track' => true
これで無事、以下の画像のように<head>
内で CriticalCSS がインライン展開された状態で送られました。
これにより、必須な同期読み込み部分は最小限に減らし、かつその部分もインライン化しているためリクエスト数も増やさないことができました。
まとめ
最近は Next.js が実験的に自動的なCriticalCSS の適用をマージしていたり、CSSinJS のライブラリである styled-components ではページにレンダリングされたコンポーネントを追跡し、それらのスタイルだけを自動的に挿入する Automatic critical CSS
の記述があるなど、ライブラリ側が行ってくれることが増えてきたようです。また、web.dev でも解説されています。
最近はこの CriticalCSS に関する記事を見かける機会が少しずつ増えてきた気がしているので、読んでいただいた方の参考になれば幸いです。ありがとうございました。