ClojureScript

拡張されたコード分割と読み込み

2017年7月10日
David Nolen

これはスニークプレビューシリーズの最初の投稿です。

クライアントアプリケーションのサイズが大きくなるにつれて、論理画面の読み込み時間を最適化することが望ましくなります。ネットワークリクエストを最小限に抑えながら、読み込まれるコードは、機能する画面を作成するために絶対に必要なものだけに制限する必要があります。Webpackのようなツールが JavaScript の主流でこの最適化手法を普及させてきましたが、Google Closure Compiler と Library は、長年にわたり Google Closure Modules の形でこの同じ最適化戦略をサポートしてきました。

Google Closure Modules は、Webpack や Rollup のようなツールに比べて、いくつかの独自の利点も提供しています。これについては、この投稿の技術セクションで説明します。簡単に言うと、すべてのソースをモジュールに最適に割り当てた後、Google Closure Compiler はデッドコードの削除(ツリーシェイキング)とモジュール間のコード移動を使用して、真に最適な分割を生成します。

ClojureScript は、しばらくの間、この機能との基本的な統合を提供してきましたが、次のリリースでは、コード分割とこれらの分割の非同期読み込みのサポートが大幅に強化され、包括的になります。

用語

Webpack の用語に精通している場合は、以下の説明では、このコンテキストにおける**モジュール**は**コード分割**または**チャンク**を指すことに注意してください。

**エントリポイント**とは、アプリケーションへの論理的なエントリポイント(ログイン、ユーザー、管理者など)を表すソースファイルを指します。

拡張コード分割

ソースのモジュール割り当てを手動で最適化する必要はもうありません。すべてのソースは、アプリケーションの依存関係グラフに基づいてモジュールに最適に割り当てられます。手動割り当てが多いモジュールがある場合は、これらを削除する必要があります。名前空間のワイルドカードマッチングを使用していた場合も、これはもう必要ありません。入力を特定のモジュールに割り当てる方法の詳細については、以下の技術的な説明を参照してください。

具体的には、以下はアンチパターンです

{:modules
  {:vendor {:output-to "..."
            :entries '#{cljsjs.react reagent.* re-frame.*}}
   :main   {:output-to "..."
            :entries '#{myapp.core}
            :depends-on [:vendor]}}

以前は、ソース(この場合は `re-frame`)とその依存関係を手動でモジュールに固定する必要がありました。必要なのは以下だけです

{:modules
  {:vendor {:output-to "..."
            :entries '#{re-frame.core}
   :main   {:output-to "..."
            :entries '#{myapp.core}
            :depends-on [:vendor]}}

もう1つの重要な強化点は、`:modules` がすべての最適化設定で機能するようになったことです。すべてのコンパイルモードで `:modules` の動作を統一することで、開発と本番間のビルド構成に関する付随的な複雑さを少し解消します。

cljs.loader

`cljs.loader` 名前空間の導入により、モジュール分割の非同期読み込みが標準化されました。アプリケーションのエントリポイントがユーザーの操作によって別のモジュールの読み込みを呼び出す必要がある場合、`cljs.loader` を使用して行うことができます。

`cljs.loader` は、最適化レベルに関係なく、`:modules` グラフに自動的に初期化される共有 Google Closure ModuleManager シングルトンを提供します。

以下は、`cljs.loader` 機能の簡単な例です

(ns views.user
 (:require [cljs.loader :as loader]
           [goog.dom :as gdom]
           [goog.events :as events])
 (:import [goog.events EventType]))

(events/listen (gdom/getElement "admin") EventType.CLICK
  (fn [e]
    (loader/load :admin
      (fn [e]
        ((resolve 'views.admin/init!))))))

(loader/set-loaded! :user)

この例では、コンパイラがこのコード分割に存在しない機能について文句を言うことなく、モジュール境界を越えて呼び出す方法を示しています。これは、最近標準ライブラリに静的 `resolve` が含まれたおかげで可能になりました。

拡張された `:modules` 機能の完全なウォークスルーについては、新しいガイドを参照してください。

技術的な説明

以下では、拡張モジュール機能の興味深い技術的な詳細をいくつか紹介します。

モジュール割り当て

このセクションでは、すべてのソースファイルをモジュールに自動的に割り当てるために使用されるアルゴリズムについて簡単に説明します。

次のような単純化されたモジュール記述があるとします

{:modules {:module-a {:entries '#{foo.core}}
           :module-b {:entries '#{bar.core}}}

これは、暗黙的なベースモジュール `:cljs-base` を含むモジュール記述に変換されます。

{:modules {:cljs-base {:entries []}
           :module-a  {:entries '#{foo.core}
                       :depends-on [:cljs-base]}
           :module-b  {:entries '#{bar.core}
                       :depends-on [:cljs-base]}}

次に、グラフ内のすべてのモジュールの深さを計算します

{:modules {:cljs-base {:entries [] :depth 0}
           :module-a  {:entries '#{foo.core} :depth 1
                       :depends-on [:cljs-base]}
           :module-b  {:entries '#{bar.core} :depth 1
                       :depends-on [:cljs-base]}}

次に、これを使用して、依存するすべての入力から可能なモジュール割り当てのセットへのマッピングを計算します。たとえば、`foo.core` のすべての依存関係を見つけ、標準ライブラリである `cljs.core` も含めて、それらが `:module-a` に入ると仮定します。

しかし、もちろん `:module-b` も `cljs.core` を自身に割り当てます。したがって、`cljs.core` モジュール割り当ては `[:module-a :module-b]` です。ただし、1つだけ選択できます。選択するには、まずすべての共通の親モジュールを見つけます。見つかったら、最大の `:depth` 値を持つモジュールを選択します。

最後に、すべての孤立したものは `:cljs-base` に割り当てられます。

Webpack に精通している読者は、このアプローチでは分割と分割読み込みを2つの独立した懸念事項として扱っていることに気付くでしょう。したがって、分割定義では、ソースの編集や追加のプラグインの導入は必要ありません。

モジュール間のコード移動

自動モジュール割り当ては、ユーザーの期待に一致するコード分割を生成するためにコードを上にプッシュします。ただし、それだけにとどまると、大きなチャンスを逃してしまいます。デッドコードの削除に加えて、Google Closure Compiler はもう1つの便利な最適化、つまり**モジュール間のコード移動**を採用しています。副作用のない個々のプログラム値(関数とメソッドを含む)は、モジュールグラフを*下に*移動できます。

Clojure のような関数型プログラミング言語はこの種の最適化に適しており、ClojureScript コンパイラは多くの場合、この機能を活用するために注意深くコードを生成します。

実際には、これは、`:cljs-base` に存在する一部の関数とその依存関係が `:module-a` でのみ使用される場合、それらはすべて `:module-a` に戻されることを意味します。

結論

Google は、2010年に出版されたClosure: The Definitive Guideでこれらの機能を文書化しましたが、これらは依然として最先端であると考えています。次のリリースでこれらの拡張機能を試してみてください!