BinaryASTでJavaScriptのロードを高速化

## TL; DR

JavaScriptファイルをプリコンパイルしておける機能。
ただし、まだ実験段階なので2019年5月時点で実用は出来ません。
サポートしているのは**[Firefox Nightly](https://www.mozilla.org/en-US/firefox/channel/desktop/#nightly)**版のみ。

## BinaryASTとは

BinaryASTは、JavaScriptの初回ロードを高速化します。
初回ロードの高速化というと、ダウンロード速度の方をイメージしてしまいますが、
ダウンロード後(あるいはキャッシュ読み込み後)のブラウザでのパース作業を高速化します。

## ASTについて

ブラウザで実行する通常のJavaScriptコードの場合、ソースはコードの構文構造を記述する**[抽象構文木(Abstract Syntax Tree, AST)](https://ja.wikipedia.org/wiki/抽象構文木)**と呼ばれる中間表現に解析されます。
この表現はバイトコードまたはネイティブのマシンコードにコンパイルして実行することができます。

```javascript:生のJavaScript
const hoge = 3 + 4;
```

```json:AST表現されたJavaScript
{
    "type": "Program",
    "body": [
        {
            "type": "VariableDeclaration",
            "declarations": [
                {
                    "type": "VariableDeclarator",
                    "id": {
                        "type": "Identifier",
                        "name": "hoge"
                    },
                    "init": {
                        "type": "BinaryExpression",
                        "operator": "+",
                        "left": {
                            "type": "Literal",
                            "value": 3,
                            "raw": "3"
                        },
                        "right": {
                            "type": "Literal",
                            "value": 4,
                            "raw": "4"
                        }
                    }
                }
            ],
            "kind": "const"
        }
    ],
    "sourceType": "script"
}
```

JavaScriptコードからASTへの変換は以下のページで試せます。
http://esprima.org/demo/parse.html

## 高速化の仕組み

BinaryASTは、元のJavaScriptコードとそれに関連付けられたデータ構造をバイナリ形式で表現します。
これによりクライアントでの上記パース作業を簡略化しようという仕組みです。
モバイル端末などのローエンドなクライアントほど効果が出ます。

* 適用前
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27443/7f89349e-fee8-ef43-4d97-dbf68c6d49be.png)

* 適用後
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27443/2997305a-437f-4c98-3fed-6aed744b267e.png)

## pre-encode

エンコーダーはRustで実装されているようです。試しにエンコードしてみます。
お試しなのでサクッとDockerで実行します。

```console:Rustインストール
docker run --rm -it ubuntu
apt-get update
apt-get upgrade
apt-get install curl
curl https://sh.rustup.rs -sSf | sh
```

```console:1を選択
1) Proceed with installation (default)
```

```console:cargoにパスを通す
source ~/.cargo/env
```

```console:nodejsとnpmをインストール
apt-get install nodejs npm
```

```console:binastを取ってくる
apt-get install git
git clone https://github.com/binast/binjs-ref
```

```console:エンコーダーとデコーダー
cd binjs-ref
cargo run --bin binjs_encode -- --help
cargo run --bin binjs_decode -- --help
```

ここまでで環境準備は整いました。
エンコードを試してみます。

```javascript:hoge.js
const hoge = 3 + 4;
console.log(hoge);
```

```console:エンコード実行
cargo run --bin binjs_encode -- -i hoge.js -o output
cd output
ll
total 28
drwxr-xr-x  2 root root 4096 May 19 04:33 ./
drwxr-xr-x 15 root root 4096 May 19 04:33 ../
-rw-r--r--  1 root root  379 May 19 04:33 hoge.binjs
-rw-r--r--  1 root root   10 May 19 04:33 hoge.grammar
-rw-r--r--  1 root root   39 May 19 04:33 hoge.js
-rw-r--r--  1 root root   10 May 19 04:33 hoge.strings
-rw-r--r--  1 root root   10 May 19 04:33 hoge.tree
```

binjsファイルが出来ていますね。

```text:hoge.binjs 
BINJS[GRAMMAR]identity;(IdentifierExpression0LiteralNumericExpression(AssertedDeclaredName2AssertedScriptGlobalScope BinaryExpression"BindingIdentifier CallExpression&ExpressionStatement
    Script,StaticMemberExpression&VariableDeclaration$VariableDeclarator[STRINGS]identity;P
                                                                                          hogeconsolelog+
constconst lexical[TREE]identity;Z                                                                                                                                                  @@
```

デコーダーも試してみましょう。

```console:デコード実行
cargo run --bin binjs_decode -- ./output/hoge.binjs decode_hoge.js
```

スペースや改行は消されましたが、戻っていますね。
行末のセミコロンも消されているので、minifyされているんでしょうか。

```javascript:decode_hoge.js
const hoge=3+4;console.log(hoge)
```

ついでにダンプも実行してみます。

```console:ダンプ実行
cargo run --bin binjs_dump -- ./output/hoge.binjs
```

<details><summary>ダンプ結果</summary><div>

```console:stdout
Attempting to decode as multipart.

    0 : 10                      # Script {
      :                         # .scope
    1 : 06                      # AssertedScriptGlobalScope {
      :                         # .declaredNames
    2 : 02                      # list (length=1) [
    3 : 04                      # AssertedDeclaredName {
      :                         # .name
    4 : 00                      # string="hoge"
      :                         # .kind
    5 : 0a                      # string="const lexical"
      :                         # .isCaptured
    6 : 00                      # bool=false
      :                         # }
      :                         # ]
      :                         # .hasDirectEval
    7 : 00                      # bool=false
      :                         # }
      :                         # .directives
    8 : 00                      # list (length=0) []
      :                         # .statements
    9 : 04                      # list (length=2) [
   10 : 14                      # VariableDeclaration {
      :                         # .kind
   11 : 08                      # string="const"
      :                         # .declarators
   12 : 02                      # list (length=1) [
   13 : 16                      # VariableDeclarator {
      :                         # .binding
   14 : 0a                      # BindingIdentifier {
      :                         # .name
   15 : 00                      # string="hoge"
      :                         # }
      :                         # .init
   16 : 08                      # BinaryExpression {
      :                         # .operator
   17 : 06                      # string="+"
      :                         # .left
   18 : 02                      # LiteralNumericExpression {
      :                         # .value
   19 : 00 00 00 00 00 00 08 40 # float=3
      :                         # }
      :                         # .right
   27 : 02                      # LiteralNumericExpression {
      :                         # .value
   28 : 00 00 00 00 00 00 10 40 # float=4
      :                         # }
      :                         # }
      :                         # }
      :                         # ]
      :                         # }
   36 : 0e                      # ExpressionStatement {
      :                         # .expression
   37 : 0c                      # CallExpression {
      :                         # .callee
   38 : 12                      # StaticMemberExpression {
      :                         # .object
   39 : 00                      # IdentifierExpression {
      :                         # .name
   40 : 02                      # string="console"
      :                         # }
      :                         # .property
   41 : 04                      # string="log"
      :                         # }
      :                         # .arguments
   42 : 02                      # list (length=1) [
   43 : 00                      # IdentifierExpression {
      :                         # .name
   44 : 00                      # string="hoge"
      :                         # }
      :                         # ]
      :                         # }
      :                         # }
      :                         # ]
      :                         # }
```
</div></details>

## サーバーへの配置

**[binast-cf-worker](https://github.com/cloudflare/binast-cf-worker)** を見る感じ、リクエストヘッダーの`Accept`に`application/javascript-binast`が含まれていれば、`Content-Type`で`application/javascript-binast`を指定した.binjsファイルを返せば良さそうです。

上記のサンプルでは、クライアント判定NGの場合は通常の.jsファイルを返すようにしていますが、今回はお試しなので、.binjsファイルなら無理やり`application/javascript-binast`にしてみます。

また、**[httpsでないとbinASTは有効にならない](https://bugzilla.mozilla.org/show_bug.cgi?id=1444956)**ようですので、ローカルでテストするときは注意。
とりあえずオレオレ証明書で回避しておきます。

```console
docker run --rm --name binjstest -v ~/output:/usr/share/nginx/html:ro -d -p 8080:80 nginx
docker exec -it binjstest bash

apt-get update
apt-get install openssl
openssl genrsa 2048 > server.key
openssl req -new -key server.key > server.csr
openssl x509 -days 3650 -req -signkey server.key < server.csr > server.crt
mkdir /etc/nginx/ssl
mv server* /etc/nginx/ssl

apt-get install vim
vim /etc/nginx/conf.d/default.conf

vim /etc/nginx/mime.types

nginx -s reload
```

```conf:/etc/nginx/conf.d/default.conf
server {
    listen 80 ssl;
    server_name localhost;

    ssl_certificate_key /etc/nginx/ssl/server.key;
    ssl_certificate /etc/nginx/ssl/server.crt;
         :
```

```conf:/etc/nginx/mime.types
types {
    text/html                                        html htm shtml;
    text/css                                         css;
    text/xml                                         xml;
    image/gif                                        gif;
    image/jpeg                                       jpeg jpg;
    application/javascript                           js;
    application/javascript-binast                    binjs;
         :
```

```html:~/output/hoge.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <script type="text/javascript" src="hoge.binjs"></script>
  </head>
  <body>
  </body>
</html>
```

## クライアント準備

現在BinaryASTをサポートしているのは**[Firefox Nightly](https://www.mozilla.org/en-US/firefox/channel/desktop/#nightly)**版のみとのことで、インストールします。

about:configにて以下を設定します。
`dom.script_loader.binast_encoding.domain.restrict`を`false`
`dom.script_loader.binast_encoding.enabled`を`true`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27443/af81218b-8dfc-9068-08b9-e2b2f848b526.png)

## hoge.htmlをブラウザで表示

以下のエラー。えぇ、プレビュー版はconstは使えないのかよ！

```console
SyntaxError: BinAST Parsing Error: Const is not supported in this preview release     hoge.binjs
```

constをvarに変えて再度エンコード。デプロイして再確認。
やっとできました。ちゃんとBinary表現になっていますね。結果も正しく動いています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27443/057cd54f-b846-a73b-806d-0da40e3d7517.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27443/32dfc4f2-1aa2-7b01-c98f-73da8b59099c.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27443/accaf111-2fdd-7952-e234-b9b6528e3604.png)

## まとめ

情報が少ない中、何とかここまでたどり着けました。
今回は短いソースなので、ファイルサイズも逆に大きくなってしまいましたが、次回は大きなファイルでどれだけ変わるかやってみようと思います。（そこが重要）

ちなみに、公式デモサイトである下記は、正しくmime-typeを返していない為binjsになっていないようです。(2019/05/23)
https://serve-binjs.that-test.site/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27443/5dda8758-246f-2ae8-4a90-2e775e9ae6c7.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27443/86c9f1f8-1be6-d935-22cc-e2620173ecd7.png)

参考元: https://blog.cloudflare.com/binary-ast/

## 2019/06/07 追記

公式デモサイトが修正されたようで、正しくbinjsで取得されるようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27443/be120310-5e8c-29a2-2f4d-b325e0f8265d.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27443/d9dbcdd0-a6de-6cc6-02af-b4ee19504510.png)