abcdefGets

ゲッツ!

JavascriptのObjectリテラルとJSON.parseについて

V8のJSON.parseについて

最近(ちょっと前か)話題のオブジェクトリテラルよりもJSON.parseのほうが早い件について。
その理由を内部実装の観点から書く。

また注意点を最後に書いた。

話題のブログは以下から https://v8.dev/blog/cost-of-javascript-2019

パースについて

V8はjavascriptコードをパースするにあたって、Lazy Parseを行っている。

Lazy Parse

javascriptはブラウザという特殊な環境で実行される言語であるため、パースにも少々工夫が必要になる。
基本的にV8はすべてのソースコードをパースしない。
一旦グローバルスコープにあるものだけをちゃんとパースして、それ以外はPreParserというパーサで関数だけをかき集める。

function foo() {
}

function bar() {
  function baz() {
    const value = 100;
  }
}

この例だと、foobarbazという関数が存在していることはパースするが、const value = 100は一切パースせずASTを生成しない。
bazが呼び出されて初めてconst value = 100がパースされる。

How to skip parsing

ではどのようにコードのパースをスキップしているかというと、V8のパーサは手書きの再帰下降構文解析器になっており、
ParserFunctionのようなメソッドが一杯あるクラスになっている。

さらにパーサはPreParserParserに分かれており、それをテンプレート引数で受け取って実行するParserBaseクラスが各パースのエントリーポイントを定義している

template <typename Impl>
class ParserBase<Impl> {
  ...
 protected:
  ...
  ExpressionT ParseFunctionExpression();
  ...
 private:
  Impl* impl() {return parser_;}
  Impl* parser_;
};

のような感じでParserBaseがパーサのBNFに対応する各パース段階のエントリーポイントを持つ。
その中でimpl()->ParserFunctionLiteral()のような形で実際のパーサを呼び出してパース処理を行っている。

Parser and PreParser

さて実際にパース処理を行うパーサは2種類ある。
PreParserはASTを生成せずに関数名やその他情報を集めるだけのパーサで、Parserは実際にASTの生成を行うパーサとなっている。
ここで大事なのがPreParserの実装である。

PreParserはASTは生成しないものの実際に構文のパースは行う。
そのため、V8ではjavascriptソースコードは2回パースされることとなる。

ここでJSON.parseの特殊性が影響してくる。

JSON.parse

JSON.parseの第一引数には文字列のJSONオブジェクトが渡される。
これがキモになる。何故かと言うと、文字列のパースコストは非常に低いのだ。
なぜなら文字列はパース段階ではなく、スキャン段階でトークン化されるため、何度パースされても文字列リテラルトークンを判定するだけで処理が完了する。
そのため、パースの負荷が非常に低くなる。

また、JSON.parse自体もランタイムで処理が行われるために、実際に呼び出されるまで処理が行われず、不要なパースをすべてスキップすることができる。
すなわち手動でLazy Parsingをしているに等しい状態となっている訳だ。

これらの要因が組み合わさってオブジェクトリテラルをべた書きするよりも、JSON.parseの方が早くなるという不思議な現象が発生する。

注意点

ただしこの方法には注意点があって、あくまでこの手法はstartup timeの高速化しかできない
実際の実行時間はJSON.parseのほうが遅くなるので、FMPの高速化には寄与するかもしれないが、すべてのオブジェクトリテラルをこれで置き換えると結構遅くなりそうなので注意。
あくまでパースのLazy化と考えたほうが良い。

ちなみにJSON.parseが遅いのはASTを作らず、毎回パースの手間がかかるから。
一度しか実行されないケースに関してはそこまで速度の差はでない。

一応ベンチマーク

https://jsperf.com/json-parse-vs-object-literal-in-parser

自分の環境では80%くらいJSON.parseが遅い