<

ブラウザの仕組みを学ぶ

Web フロントエンジニアたるもの、ブラウザの仕組みに興味を持つのは自然の摂理です。本記事では、私がブラウザの仕組みを学んでいく過程を備忘録として残します。

みんな大好き Chrome

Web フロントエンジニアに愛されているブラウザといえば、IEChrome ですよね。 ブラウザで HTML,CSS,JS の動作確認するのは、日常茶飯事です。 ブラウザによって動作が異なることは、Web フロントエンジニアなら周知の事実です。 じゃあ、なんで動作が違うのかというと、

  • 「レンダリングエンジンが違うから〜」
  • 「Javascript エンジンが違うから〜」

ぐらいは知っているんじゃないかなと思います。 じゃあ、そのレンダリングエンジンってどういう仕組みで動いているのでしょうか。 気になりますよね。

Chromium について

Chromium も、たぶんご存じの方多いのかなと思いますので、簡単に説明します。

Chromium は、オープンソースのプロジェクト名であり、ブラウザ名でもあります。 Chrome は、Chromium を元に開発されています。 詳しい説明はChromium - Wikiを見てください。

オープンソースってことは、ソースコードが誰でも読めちゃうってことですよね。 だったら、ブラウザの動作を知ることができちゃうじゃないですか! わーい!😎

Chromium のリバースエンジニアリング

ではさっそく、Chromium のソースコードを見ていきましょう。

これです。

chromium/src
chromium/src

Chromium - Wikiによれば、Chromium のソースコードは約 3,500 万行あるそうです。 しかも、言語は C++。私はあまりそれを詳しくないのです 😞。

実際にソースコードをローカルマシン(Macbook Air)へチェックアウトし、ビルドをしてみました。 マシンが貧弱だというのもあるんですが、ビルドに半日ぐらいかかってしまいました。ヘトヘトです。 これじゃあさすがに、手軽にブラウザの動作確認はできそうにないです。

ブラウザの仕組み資料を読む

ちょっと趣向を変えて、次のような資料を読むことにしました。

では、さっそく見ていきます。 最初に目につくのが、ブラウザの主な構成要素です。

ブラウザの主な構成要素
ブラウザの主な構成要素

構成要素の内、ユーザーインターフェース、ブラウザエンジン、レンダリングエンジンに着目します。 それぞれ、次の役割があります。

  • ユーザーインターフェース
    • アドレスバーや戻る/進むボタンのような UI を担当
  • ブラウザエンジン
    • UI とレンダリングエンジンの間の処理を整理
  • レンダリングエンジン
    • 要求されたコンテンツ(HTML など)の表示を担当

ちなみに、Chromium のレンダリングエンジンには、webkit を使っていましたが、blink に変わりました。

ブラウザの基本的なフローは、次の図の通りです。

レンダリングの基本的なフロー
レンダリングの基本的なフロー
Webkitのメインフロー
Webkitのメインフロー
  1. Parsing HTML to construct the DOM tree
  2. Render tree construction
  3. Layout of the render tree
  4. Painting the render tree

それぞれ見ていきます。


① Parsing HTML to construct the DOM tree

1 は、HTML をレキサ(字句解析. ex:flex)・パーサ(構文解析. ex:bison)を使って DOM ツリーを構築します。

レキサでは、ステートマシンによって読み込み状態を管理しつつトークンを識別します。空白とかコメントなどは削除されます。

レキサから識別されたトークンをパーサに渡し、構文解析していきます。 HTML は DTD(Document Type Definition)で文脈自由文法なため、機械的に解析できます。 ただ、HTML は寛大な仕様で、次のようなパターンも許容するようになっています。

  • <br />の代わりの</br>
  • 迷子のテーブル
  • 入れ子のフォーム要素
  • 深すぎるタグ階層
  • 配置に誤りのある html または body 終了タグ

パーサから DOM(Document Object Model)を構築します。 DOM は、これまでの単なるテキストから、API を持たせたオブジェクトモデルを作ることで、 以降は DOM を使って処理しやすくなります。

サンプル マークアップのDOMツリー
サンプル マークアップのDOMツリー

これまでは HTML の話をしていましたが、HTML と並行して CSS も同様に処理していきレンダーオブジェクトというオブジェクトを作っていきます。これは、スタイル情報を付与したオブジェクトになります。 基本的に、CSS と HTML は互いに独立しているので、並列処理が可能です。例えば、CSS を処理したことで、HTML が変化することはないはずです。

CSSの解析
CSSの解析

ただ、Javascript は話が違うので、Javascript が読み込まれた時点で HTML のパースを中断して Javascript のパースが開始されます。 また、Javascript が、まだ読み込まれていないスタイルシートの影響を受けそうな特定のスタイルプロパティにアクセスした場合、Javascript はブロックされます。


② Render tree construction

① の DOM とレンダーオブジェクトから、レンダーツリーを構築します。 DOM とレンダーオブジェクトは、1 対 1 という訳ではなく、例えば head 要素や、display:none;の要素もレンダーツリーに含まれません。 レンダーツリーの更新は、DOM ツリーが更新される度に行われます。

レンダーツリーと対応するDOMツリー
レンダーツリーと対応するDOMツリー

レンダーオブジェクトからスタイルを計算するのですが、ちょっと複雑です。 詳しくは、スタイルの計算を見てください。


③ Layout of the render tree

レンダーツリーから、レイアウト情報を計算していきます。 レイアウト情報とは、位置(x,y)とサイズ(width,height)です。

レンダーツリーのルートから再帰的にレイアウト情報を計算(layout メソッド)していきます。

  1. 親レンダラーが自身の幅を決定します。
  2. 親が子を確認して、
    1. 子レンダラーを配置します(x と y を設定します)。
    2. 必要な場合は子の layout メソッドを呼び出します。これにより、子の高さを計算します。
  3. 親は子の高さの累積、マージンの高さ、パディングを使用して、自身の高さを設定します。この高さは親レンダラーのさらに親によって使用されます。

レイアウト処理 参考

CSS ボックスモデルの図を参考までに共有しておきます。

CSS 基本ボックスモデル
CSS 基本ボックスモデル

④ Painting the render tree

ようやく描画します。 どこに描画するかという配置方法について考えることになります。 大きく分けて、3 つに分かれます。

  • 通常
    • オブジェクトはドキュメント内の場所に従って配置されます。つまり、レンダーツリー内の場所は DOM ツリー内の場所と同様になり、ボックスの種類や寸法に従ってレイアウトされます。
      • position:static,relative
  • フロート
    • オブジェクトは最初に通常のフローのようにレイアウトされてから、左右のできるだけ遠くに移動されます。
      • float:right,left
  • 絶対
    • オブジェクトはレンダーツリー内で DOM ツリーとは異なる場所に配置されます。
      • position:absolute,fixed

配置方法

配置方法が分かれば、今度は描画する形について考えます。ブロックボックスとインラインボックスです。

ブロックとインラインの配列
ブロックとインラインの配列

ブロックボックスは、短形の形であり垂直に並びます。 インラインボックスは、独自の形を持たず水平に並びます。

z-index のようなプロパティでは、スタッキングコンテキストという概念を知る必要があります。 詳しくは、重ね合わせコンテキスト - developer.mozilla.org をご確認ください。


ブラウザを自作してみる

前章では、資料を通してブラウザの動作が理解できました。 読むだけじゃなく、動かして理解してみたいとは思いませんか? そうです、自作してみましょう。

Rust 製の Servo というブラウザエンジンを開発している人が書いた、次のブラウザ自作に関する記事がとても分かりやすいです。

Toy ブラウザエンジン(mbrubeck)のメインフローが、これまでの話ととても似ています。

Toyブラウザエンジン(mbrubeck)のメインフロー
Toyブラウザエンジン(mbrubeck)のメインフロー

Style tree は、これまでの話でいうと Render tree だと思います。 Toy ブラウザエンジン(mbrubeck)に、次の HTML と CSS を読み込ませると、下記の画像のようなアウトプットになります。

<html>
  <head>
    <title>Test</title>
  </head>
  <div class="outer">
    <p class="inner">Hello, <span id="name">world!</span></p>
    <p class="inner" id="bye">Goodbye!</p>
  </div>
</html>
* {
  display: block;
}

span {
  display: inline;
}

html {
  width: 600px;
  padding: 10px;
  border-width: 1px;
  margin: auto;
  background: #ffffff;
}

head {
  display: none;
}

.outer {
  background: #00ccff;
  border-color: #666666;
  border-width: 2px;
  margin: 50px;
  padding: 50px;
}

.inner {
  border-color: #cc0000;
  border-width: 4px;
  height: 100px;
  margin-bottom: 20px;
  width: 500px;
}

.inner#bye {
  background: #ffff00;
}

span#name {
  background: red;
  color: white;
}
Toyブラウザエンジン(mbrubeck)のアウトプット
Toyブラウザエンジン(mbrubeck)のアウトプット

次のリンクにある自作ブラウザエンジンは、mbrubeck/robinsonを参考にして作られたものだそうです。

Toy ブラウザエンジン(askerry)に、次の HTML と CSS を読み込ませると、下記の画像のようなアウトプットになります。 見たら分かると思いますが、とても高機能です。

<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title>Browser Test</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <link rel="stylesheet" href="demo.css" />
  </head>

  <body>
    <div id="page">
      <header class="header">
        <h1>Toy Browser Engine</h1>
      </header>
      <div id="main">
        <div id="navbar">
          <a href="#" class="navitem"> Home </a>
          <a href="#" class="navitem"> About </a>
          <a href="#" class="navitem"> Some random stuff </a>
          <a href="#" class="navitem"> Conclusion </a>
          <img class="img" src="images/otters.jpg" />
        </div>
        <div id="content">
          <h2>What is this?</h2>
          This is a <b>toy</b> browser engine, implemented for
          <span>fun </span> <img class="icon" src="images/fun.png" /> and
          <span>glory <img class="icon" src="images/glory.png" /></span>.
          <h2>Why would anyone do this?</h2>
          This seems pretty pointless! But I had a few goals:
          <ul>
            <li>Something to build to learn C++</li>
            <li>Learn more about how browsers work</li>
            <li>Make something I've never made before</li>
          </ul>
          <h2>What can it do?</h2>
          <p>
            Currently, the engine can parse a subset of HTML and build a DOM
            tree. It can also parse a small subset of CSS (sometimes
            incorrectly) and use simple selector matching to apply styles to
            elements.
          </p>
          <p>
            It supports <em>very basic</em> rendering of boxes, images, and text
            with simple block and inline layouts.
          </p>
        </div>
      </div>
    </div>
  </body>
</html>
/* https://github.com/askerry/toy-browser/blob/master/examples/demo.css */
body {
  font-family: Arial, sans-serif;
  background-color: #bfc0c0;
  color: #253237;
  font-size: 16px;
}
#page {
  padding: 20;
  /* width: 800px; */
  margin: auto;
}

header {
  padding: 10px;
  padding-left: 20px;
  background-color: #434371;
  color: #4acebd;
}

span {
  color: #4acebd;
}

#main {
  background-color: white;
  display: flex;
}

#navbar {
  width: 180px;
  padding: 30px;
  background-color: #4acebd;
  height: 500px;
}

.navitem {
  display: block;
  text-align: center;
  background-color: #434371;
  color: #4acebd;
  margin-top: 5px;
  margin-bottom: 5px;
  padding: 10px;
  border-radius: 4px;
  border-style: solid;
  border-width: 2px;
  border-color: #253237;
}

#content {
  padding: 20px;
  width: 500;
}

.img {
  width: 180px;
}

.icon {
  width: 2em;
}

h2 {
  color: #434371;
}

li {
  margin-bottom: 5px;
}
Toyブラウザエンジン(askerry)のアウトプット
Toyブラウザエンジン(askerry)のアウトプット

私としては、こちらの方が興味があるので、まずこちらを知り、それを Rust 版で作り直したいなと思います。

C++ を学ぶ

さて、C++を学ぶために、次のサイトをざっと眺めてみます。

自作ブラウザのソースコード

askerry/toy-browserのメインコード(main.cc)を載せます。

/* https://github.com/askerry/toy-browser/blob/master/src/main.cc */
namespace {

void renderWindow(int width, int height, const style::StyledNode &sn,
                  sf::RenderWindow *window) {
  layout::Dimensions viewport;
  viewport.content.width = width;
  viewport.content.height = height;
  // Create layout tree for the specified viewport dimensions.
  std::unique_ptr<layout::LayoutElement> layout_root =
      layout::layout_tree(sn, viewport);
  // Paint to window.
  paint(*layout_root, viewport.content, window);
}

int windowLoop(const style::StyledNode &sn) {
  // Create browser window.
  std::unique_ptr<sf::RenderWindow> window(new sf::RenderWindow());
  window->create(sf::VideoMode(FLAGS_window_width, FLAGS_window_height),
                 "Toy Browser", sf::Style::Close | sf::Style::Resize);
  window->setPosition(sf::Vector2i(0, 0));
  window->clear(sf::Color::Black);
  // Render initial window contents.
  renderWindow(FLAGS_window_width, FLAGS_window_height, sn, window.get());
  // Run the main event loop as long as the window is open.
  while (window->isOpen()) {
    sf::Event event;
    while (window->pollEvent(event)) {
      switch (event.type) {
        case sf::Event::Closed:
          window->close();
          break;

        case sf::Event::KeyPressed:
          logger::debug("keypress: " + std::to_string(event.key.code));
          break;

        case sf::Event::Resized:
          logger::debug("new width: " + std::to_string(event.size.width));
          logger::debug("new height: " + std::to_string(event.size.height));
          window->clear(sf::Color::Black);
          renderWindow(event.size.width, event.size.height, sn, window.get());
          break;

        case sf::Event::TextEntered:
          if (event.text.unicode < 128) {
            logger::debug(
                "ASCII character typed: " +
                std::to_string(static_cast<char>(event.text.unicode)));
          }
          break;

        default:
          break;
      }
    }
  }
  return 0;
}
}  // namespace
int main(int argc, char **argv) {
  gflags::ParseCommandLineFlags(&argc, &argv, true);

  // Parse HTML and CSS files.
  const std::string source = io::readFile(FLAGS_html_file);
  std::unique_ptr<dom::Node> root = html_parser::parseHtml(source);
  const std::string css = io::readFile(FLAGS_css_file);
  const std::unique_ptr<css::StyleSheet const> stylesheet = css::parseCss(css);

  // Initialize font registry singleton.
  text_render::FontRegistry *registry =
      text_render::FontRegistry::getInstance();

  // Align styles with DOM nodes.
  std::unique_ptr<style::StyledNode> styled_node =
      style::styleTree(*root, stylesheet, style::PropertyMap());

  // Run main browser window loop.
  windowLoop(*styled_node);

  // Delete styled node and clear font registry.
  styled_node.reset();
  registry->clear();
  return 0;
}

次のとおり、これまで学んできたメインフローと、C++がとても似ていることが分かります。

  1. HTML と CSS をパース
// Parse HTML and CSS files.
const std::string source = io::readFile(FLAGS_html_file);
std::unique_ptr<dom::Node> root = html_parser::parseHtml(source);
const std::string css = io::readFile(FLAGS_css_file);
const std::unique_ptr<css::StyleSheet const> stylesheet = css::parseCss(css);
  1. 1 の結果から Style tree(Render tree)を構築
// Align styles with DOM nodes.
std::unique_ptr<style::StyledNode> styled_node =
    style::styleTree(*root, stylesheet, style::PropertyMap());
  1. 2 の結果から Layout tree を構築
// Create layout tree for the specified viewport dimensions.
std::unique_ptr<layout::LayoutElement> layout_root =
    layout::layout_tree(sn, viewport);
  1. 3 を paint という描画
// Paint to window.
paint(*layout_root, viewport.content, window);

Re: ブラウザの仕組み資料を読む

もう一度、ブラウザの仕組み: 最新ウェブブラウザの内部構造 を読むと、初めて読んだときに比べて、深く理解できるんじゃないかなと思います。

最後に

ブラウザの動作について資料や自作を通して理解を深めました。

ブラウザの動作が分かれば、ブラウザに優しい Web フロントエンド開発ができると思います。

(今度こそ Chromium のリバースエンジニアリングができるかもしれません。)

その他

Chromium のアドベントカレンダーがありました。参考までにざっと見てみると良いでしょう。 Chromium Browser Advent Calendar 2017

Tags

ブラウザのレイアウトとペイントを知る

2022-07-03

ブラウザのレンダリングエンジンにおけるレイアウトやペイントについて気になったので、調べました。その内容をまとめます。レンダリングエンジンは、Chrome の Blink を題材とします。...

TikTokスクレイプ基盤をGCP上で構築してハマったこと

2021-08-28

TikTokへスクレイプするバッチをGCP上で構築しました。GCP構築のシステム設計話と、その構築時に、ハマったことを共有します。...

リモートワークになってから『気軽にすぐ聞く』ことが難しくなった

2021-03-10

リモートワークが普及しつつある今、オンラインでの仕事に慣れているエンジニアも多いのではないでしょうか。私も、そのエンジニアの一人であり、約1年はリモートワークしています。そんな中、久々に会社へ出社すると、気軽に話しかける楽さ を実感しました。この体験について、深堀りしたいと思います。...

20代後半エンジニアである私がこれから学ぶべきこと

2020-10-29

私は、現在26歳のWebエンジニアです。これまでの技術に対する学び方と、これからの技術に対する学び方について、少し考えたいと思っています。...

Micro Frontends を調べたすべて

2020-10-07

Micro Frontendsに関わる記事を100件以上読みました(参考記事に記載しています)。そこから得たMicro Frontendsについてこの投稿に記録します。また、調査メモについて、次のリポジトリに残しています。...

ZoomのMeetingを自動生成するGASライブラリ zoom-meeting-creator を作った

2020-06-06

みなさん、Zoom使っていますか? ZoomのMeetingを自動生成するGASライブラリを公開しましたので、そのきっかけと使い方について紹介しようと思います。...

アカウント画像一括更新ツールを作ったので、紹介と学びについて

2020-06-04

GoogleやGithubなど、様々なサービスのプロフィール情報(画像, etc)を一括更新するツール、puppeteer-account-manager を開発しました。開発の目的や、開発から得た知見を紹介します。...

Micro Frontends を学んだすべて

2020-05-04

Micro FrontendsというWebフロントエンドアーキテクチャがあります。このアーキテクチャを知るために、書籍を読み、簡単なサンプルWebアプリを開発しました。そこから学んだことをすべて議事録として残したいと思います。...

TwitterにあるLinkを収集するツール Cotlin で、世界中のプレゼンテーション資料を知ろう

2020-03-15

Twitterに投稿されているLinkを収集するツール Cotlin を作りました。Collect links in tweet から、Cotlinという名前にしました。Androidのアレに似ています。...

Google Apps Script で FetchAllとRedirctURL の組み合わせは悪い

2020-02-24

Google Apps Script (以下、GAS)で、困ったことがあったので備忘録として残しておこうと思います。...

GMailをGCalendarに登録するサービス rMinc を作ってみた

2020-02-17

ターゲットユーザー * GMailとGCalendarを使っている人 メールを開くって面倒じゃないですか? 例えば、次のようなメールを受信していたとします。* アマゾンで商品を購入した際、お届け予定日が記載されたメール * 映画館(TOHOシネマ)でネット予約した際、上映日が記載されたメール * ホテルをネット予約した際、宿泊日が記載されたメール...

1コマ漫画検索サービスTiqav2 (Algolia + Cloudinary + Google Cloud Vision API) 作ってみた

2020-02-08

画像で会話って楽しい 皆さん、チャットツールでコミュニケーションするとき、絵文字や画像って使ってますか?僕はよく使ってます。人とコミュニケーションするのに、文字だけだと堅苦しいイメージですよね。例えば、『OKです、それで先に進めて下さい。』というフレーズだけだと、相手がどのような感情なのか読み取りにくいです。...

フィリピンに行ってきたら、日本は良いなって思うようになった

2019-10-27

2019年10月11日~2019年10月15日の5日間、フィリピンに行ってきました。日本人男性(前職の先輩:Kikuchi)とフィリピン人女性が結婚するため、その結婚式旅行に同伴させて頂きました。Kikuchiさんとは、私が新人の頃に大変お世話になった方なので、お祝いの気持ちが込み上げてきました。:)...

技術書典7で初執筆した経験をすべて公開

2019-09-06

技術書典7で初執筆しました。記事の目的 * 執筆でどういったことをしたのかの備忘録 * 執筆を考えている人の助けになりたい実際に販売する本は↓のものです。...