2017年12月12日
超高速エンジンの内部:Quantum CSS(別名Stylo)- 前編
本記事は、原著者の許諾のもとに翻訳・掲載しております。
Project Quantumのことをお聞きになったことがあるでしょう。これはFirefoxを高速化するために、Firefoxの中身を大幅に書き換えたものです。実験的なブラウザ、Servoから部分的に交換を実施し、エンジンの他の部分の著しい改善を図っています。
このプロジェクトは、飛行中のジェット機でのジェットエンジンの取り替えに例えられます。現場でコンポーネントごとに変更を実施するので、各コンポーネントの準備が整い次第、Firefoxで効果を確認することができます。
また、Servoから採用した最初の重要なコンポーネントは、Quantum CSSと呼ばれる新しいCSSエンジン(以前の別名はStylo)で、現在はNightly版でテストすることが可能です。(編注:Firefox 57からはデフォルトで有効化されています) about:config
に行き、 layout.css.servo.enabled
をTrueに設定すれば、動作しているのを確かめることができます。
新しい素晴らしいCSSエンジンを生み出すために、4つの異なるブラウザから最新技術をこの新しいエンジンに結集しています。
注釈:
並行処理(Servo)
ルールツリー(Firefox)
スタイルの共有キャッシュ(ChromeとSafari)
巧みな調整と最適化(Styloチーム)
最新のハードウエアを利用して、マシンの全てのコアで作業を並列化します。つまり、2倍、あるいは4倍、もしくは18倍も速く実行できるということです。
それに加えて、他のブラウザから現存する最新式の最適化を組み合わせます。そのため、並列に実行しなかったとしても、非常に高速なCSSエンジンになるでしょう。
しかし、CSSエンジンは何を行うのでしょうか。まずCSSエンジンに着目し、どうやってブラウザの残りの部分に適合させているのかを見てみましょう。その後、Quantum CSSがエンジン全体をどのように高速化しているのかを見ていきます。
CSSエンジンは何をするのか?
CSSエンジンは、ブラウザのレンダリングエンジンの一部です。レンダリングエンジンは、WebサイトのHTMLやCSSファイルを取得して、スクリーン上のピクセルに変換します。
ブラウザは、それぞれレンダリングエンジンを備えています。ChromeではBlink、EdgeではEdgeHTML、SafariではWebKitと呼ばれています。またFirefoxではGeckoという名前です。
ファイルからピクセルを取得するには、基本的に、これら全てのレンダリングエンジンは同じことを行います。
1. ファイルをブラウザが理解できるようなオブジェクトにパースします。このオブジェクトにはDOMも含まれます。この時点で、DOMはページ構造を知り、要素間の親子関係が分ります。しかし、その要素がどのようなものかということは分りません。
2. 要素がどのようなものであるべきかを把握します。各DOMノードに対して、CSSエンジンはどのCSSルールを適用するかを割り出します。それから、そのDOMノードに対してそれぞれのCSSプロパティの値を算出します。
3. 各ノードのディメンションと、スクリーンのどこに配置するかを算出します。画面上に表示する事項ごとに、ボックスが作成されます。このボックスは単にDOMのノードを表すだけではありません。DOMノード内部の事項、例えばテキスト行も表しています。
4. 様々なボックスを描画します。この描画は、複数のレイヤで生じる可能性があります。これは、タマネギの皮のように紙を層状に重ねる旧式の手書きアニメーションのようなものだと考えています。他のレイヤで再描画することなく、1つのレイヤだけを書き換えることが可能です。
5. これらの別々に描画されたレイヤを集め、transformのようなコンポジタのみのプロパティを適用し、1つの画像に変換します。ようするに、まとめて重ねたレイヤの写真を撮るようなことです。この画像はスクリーン上でレンダリングされます。
つまり、CSSエンジンは、スタイルの計算を始めるに当たり、次の2つを持っているということです。
- DOMツリー
- スタイルルールのリスト
CSSエンジンはDOMノードを1つずつ処理し、各DOMノード用のスタイルを割り出します。この処理の一部分として、スタイルシートがプロパティの値を宣言していなくても、CSSエンジンはDOMノードに全てのCSSプロパティの値を与えます。
これは、フォームの項目を全て記入しようとするようなものだと思います。それぞれのDOMノードにつき1枚のフォームを記入しなければならず、更にフォームの各項目について、回答を記入する必要があります。
そのためには、CSSエンジンは2つの処理をしなければなりません。
- 対象ノードに適用するルールを割り出します。別名、 セレクタマッチング
- 親の値、または初期値を用いて欠落している値を埋めます。別名、 カスケード
セレクタマッチング
このステップのために、DOMノードにマッチするあらゆるルールをリストに追加します。というのも、複数のルールがマッチする可能性があり、同じプロパティに対して複数の宣言があるかもしれないからです。
注釈:次は、divの内部にメッセージクラスと一緒にpタグがある…。
だから、これはマッチする…
これは…
更に、ブラウザ自体が初期値のCSS(ユーザエージェントスタイルシートと呼ばれています)をいくつか追加します。CSSエンジンは、選択すべき値を、どのように見つけるのでしょうか。
ここで出番となるのが、詳細度ルールです。CSSエンジンは基本的に、スプレッドシートを生成します。生成後、異なる列に基づいて宣言を並べ替えます。
注釈:作者のスタイルシートはユーザエージェントスタイルシートに優先される
より詳細度の高いセレクタが優先される
最も高い詳細度を持つルールが優先されます。このスプレッドシートに基づいて、CSSエンジンは記入可能な値を埋めます。
残りの項目の値については、カスケードを使用します。
カスケード
カスケードによって、CSSによる書き込みと維持管理が一層容易になります。カスケードのおかげで、bodyに color
プロパティを設定できます。そして、 p
、 span
そして li
要素内の全てのテキストが、(詳細度がより高いオーバーライドをしない限り)そのcolorを使用すると分かります。
そのためには、まずCSSエンジンが、フォームの空欄をチェックします。初期設定でプロパティが継承される場合、CSSエンジンはツリーをさかのぼり、先祖が値を持っているかどうか確認します。値を持っている先祖が1つもないか、プロパティが継承されていなければ、初期値を取得します。
これで、このDOMノードについては、全てのスタイルが計算されました。
注釈:スタイル構造の共有
ここまでご紹介してきたフォームは、少々、説明にふさわしいとは言いえないものでした。CSSには何百ものプロパティがあります。CSSエンジンが、各DOMノードの各プロパティに対する値をずっと保持していれば、じきにメモリ不足になってしまいます。
値を保持する代わりに、CSSエンジンは通常、スタイル構造の共有と呼ばれる処理を行います。通常、スタイル構造を呼び出す異なるオブジェクトの中に(フォントのプロパティのように)一緒に使われるデータを格納しておきます。その時、同じオブジェクト内に全てのプロパティを持つのではなく、計算されたスタイルオブジェクトは、ポインタを持っているだけです。対象となるDOMノード用の適正な値を持ったスタイル構造へのポインタが、それぞれのカテゴリについて存在します。
これは、メモリと時間の節約につながります。(兄弟ノードのような)似たプロパティを持つノードは、ノードが共有しているプロパティ用の同一構造をポイントするだけでいいのです。そして、多数のプロパティが継承されているので、先祖ノードは、詳細度の高いオーバーライドを指定していないノードならば、どんな子孫ノードとも構造を共有できます。
それでは、どのように高速化するのか?
最適化していない状態でのスタイルの計算は、以下の図のようになります。
注釈:セレクタのマッチ->詳細度によって宣言をソート->プロパティの値の計算
ここでは、多くの処理が行われています。それは、最初のページをロードする時だけ必要なわけではありません。複数の要素間を行き来したり、DOMに変更を加えたり、スタイルの再計算を引き起こしたり、ユーザがページに干渉するたびに何度も処理が発生します。
注釈:内部レンダリング->スタイルの再計算->スタイルの再計算
つまり、CSSのスタイルの計算は、最適化の有力な候補であるということです。過去20年間、各種ブラウザは、様々な最適化の方策を試みてきました。新たな超高速エンジンを作り出すにあたり、Quantum CSSは、各種エンジンのいい所を寄せ集めて組み合わせているのです。
それでは、それらが一体となってどのように動くのか、詳細を見ていきましょう。
後編は コチラ です。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa